From 2f2c5dfabda6d83533d2a7d1e99f1aac0fab2daf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Prieto?= Date: Tue, 17 Jun 2025 22:18:30 +0200 Subject: [PATCH] Start refactoring of wounds calculation --- .../fourtykcalculator/DefensiveProfile.java | 19 ++- .../UnitAttackModifiers.java | 4 + .../games/fourtykcalculator/Weapon.java | 130 ++++++++---------- .../WeaponAttackResolution.java | 1 + 4 files changed, 68 insertions(+), 86 deletions(-) diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java index 969ea43..fd23916 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java @@ -3,10 +3,7 @@ package ninja.thefirearchmage.games.fourtykcalculator; import lombok.Getter; import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple3; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; @Getter public class DefensiveProfile { @@ -25,31 +22,33 @@ public class DefensiveProfile { private boolean fishFor6NormalSaves; private boolean hasStealth; private boolean isInStealthRange; + private boolean hasWoundMalusIfStrengthHigherThanToughness; private Tuple3 damageReductionModifier; private Tuple3 apReduction; public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave) { - this(toughness, wounds, bodies, normalSave, Optional.empty(), Optional.empty(), + this(toughness, wounds, bodies, normalSave, Optional.empty(), new ArrayList<>(), new HashSet<>(), false, false, false, - false, true); + false, true, false); } public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave, Optional invulnerableSave, - Optional feelNoPain, Set unitTypeKeywords, + List feelNoPain, Set unitTypeKeywords, boolean isInCover, boolean hasRerollOneNormalSaves, boolean hasRerollFailedNormalSaves, - boolean hasRerollAnyNormalSaves, boolean isVisible) { + boolean hasRerollAnyNormalSaves, boolean isVisible, boolean hasWoundMalusIfStrengthHigherThanToughness) { this.toughness = toughness; this.normalSave = normalSave; this.wounds = wounds; this.bodies = bodies; this.invulnerableSave = invulnerableSave; - this.feelNoPain = feelNoPain; + this.feelNoPainEffects = feelNoPain; this.unitTypeKeywords = unitTypeKeywords; this.isInCover = isInCover; this.hasRerollOneNormalSaves = hasRerollOneNormalSaves; this.hasRerollFailedNormalSaves = hasRerollFailedNormalSaves; this.hasRerollAnyNormalSaves = hasRerollAnyNormalSaves; this.isVisible = isVisible; + this.hasWoundMalusIfStrengthHigherThanToughness = hasWoundMalusIfStrengthHigherThanToughness; } public DefensiveProfile withUnitTypes(Set types) { @@ -68,7 +67,7 @@ public class DefensiveProfile { return this; } public DefensiveProfile withFeelNoPainConfig(FeelNoPainEffect feelNoPainConfig) { - this.feelNoPain = Optional.of(feelNoPainConfig); + this.feelNoPainEffects.add(feelNoPainConfig); return this; } public DefensiveProfile setInCover(boolean inCover) { diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java index f3e38ec..2d84451 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java @@ -71,4 +71,8 @@ public class UnitAttackModifiers { private static boolean setContainsAnyOf(Set oneSet, Set otherSet, T otherValue) { return oneSet.stream().anyMatch(i -> i.equals(otherValue) || otherSet.contains(i)); } + + public boolean lethalHitsAppliesTo(Weapon weapon) { + return lethalHitsModifier._1 && lethalHitsModifier._2.appliesTo(weapon); + } } diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java index ba7fdbe..820e70f 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java @@ -5,6 +5,7 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple; import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2; import java.util.*; +import java.util.stream.Collectors; public class Weapon { @Getter @@ -73,7 +74,7 @@ public class Weapon { var weaponAttackResolution = new WeaponAttackResolution(); var passingHits = calculatePassingHits(target, unitModifiers, weaponAmount, weaponAttackResolution); - var passingWounds = calculatePassingWounds(weaponAmount, passingHits); + var passingWounds = calculatePassingWounds(target, unitModifiers, weaponAmount, passingHits, weaponAttackResolution); var normalWounds = passingWounds._1; var mortalWounds = passingWounds._2; @@ -93,7 +94,7 @@ public class Weapon { weaponAttackResolution.setHitsWithoutRerolls(passingHits); // Add hits from rerolls - var hitRerolls = prepareHitRerolls(hitRollGoal, unitAttackModifiers, target); + var hitRerolls = prepareRerolls(RerollStageType.HIT, hitRollGoal, unitAttackModifiers, target); if(hitRerolls.isPresent()) { var rerolls = hitRerolls.get(); var successfulHitFromRerolls = weaponAttacks * hitFailureProbability * rerolls.getSuccessProbability(); @@ -140,22 +141,24 @@ public class Weapon { /** * Returns a tuple of normal passing wounds and mortal wounds so that the caller can handle them differently. */ - private Tuple2 calculatePassingWounds(Weapon weapon, int amountOfWeapons, double passingHits) { - double lethalHitsCount = calculateLethalHits(weapon, amountOfWeapons); + private Tuple2 calculatePassingWounds(DefensiveProfile target, UnitAttackModifiers unitAttackModifiers, + int amountOfWeapons, double passingHits, + WeaponAttackResolution weaponAttackResolution) { + double lethalHitsCount = calculateLethalHits(amountOfWeapons, unitAttackModifiers, target, weaponAttackResolution); var normalHits = passingHits - lethalHitsCount; - results.setWeaponStat(weapon, WeaponStat.NON_LETHAL_HITS_COUNT, normalHits); - int woundRollGoal = calculateWoundRollGoal(weapon); + int woundRollGoal = calculateWoundRollGoal(target, unitAttackModifiers); int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(woundRollGoal); - var passingNormalWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); - results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_WITHOUT_REROLL, passingNormalWounds+lethalHitsCount); - var woundsRerolls = prepareWoundRerolls(woundRollGoal); + var passingNormalWounds = normalHits * woundSuccessProbabilityNumerator / 6; + weaponAttackResolution.setNormalWoundsWithoutRerolls(passingNormalWounds + lethalHitsCount); + + var woundsRerolls = prepareRerolls(RerollStageType.WOUND, woundRollGoal, unitAttackModifiers, target); if(woundsRerolls.isPresent()) { var rerolls = woundsRerolls.get(); - var failedWounds = normalHits * ((double) (1 - woundSuccessProbabilityNumerator) / PROBABILITY_DENOMINATOR_D6); - var extraWoundsFromRerolls = failedWounds * rerolls.getSuccessProbability() / PROBABILITY_DENOMINATOR_D6; - results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_FROM_REROLL, extraWoundsFromRerolls); + var failedWounds = normalHits * (6 - woundSuccessProbabilityNumerator) / 6; + var extraWoundsFromRerolls = failedWounds * rerolls.getSuccessProbability() / 6; + weaponAttackResolution.setNormalWoundsFromRerolls(extraWoundsFromRerolls); passingNormalWounds += extraWoundsFromRerolls; } @@ -182,50 +185,42 @@ public class Weapon { return Tuple.of(passingNormalWounds, passingMortalWounds); } - private double calculateLethalHits(Weapon weapon, int amountOfWeapons) { + private double calculateLethalHits(int amountOfWeapons, UnitAttackModifiers unitAttackModifiers, + DefensiveProfile target, WeaponAttackResolution weaponAttackResolution) { var lethalHitsCount = 0d; + var hasLethalHits = this.hasLethalHits || unitAttackModifiers.lethalHitsAppliesTo(this); + if(hasLethalHits) { + double weaponAttacks = calculateWeaponAttacks(amountOfWeapons, unitAttackModifiers, target, weaponAttackResolution); + var lethalHitsProbability = calculateProbabilityNumeratorFrom(unitAttackModifiers.getCriticalHitValue()) / 6; - if(weapon.hasLethalHits()) { - double weaponAttacks = calculateBasicWeaponAttacks(weapon, amountOfWeapons); - var lethalHitsProbabilityNumerator = calculateProbabilityNumeratorFrom(criticalHitValue); - - lethalHitsCount = weaponAttacks * ((double) lethalHitsProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); - results.setWeaponStat(weapon, WeaponStat.LETHAL_HITS_COUNT, lethalHitsCount); + lethalHitsCount = weaponAttacks * lethalHitsProbability; + // If we're fishing for hit criticals, the lethal hits will be higher + if(unitAttackModifiers.isFishForCriticalHits()) { + var lethalHitsFailureProbability = (6 - (lethalHitsProbability * 6)) / 6; + var extraLethalHitsFromFishingCriticals = weaponAttacks * lethalHitsFailureProbability * lethalHitsProbability; + weaponAttackResolution.setLethalHitsFromFishingCriticals(extraLethalHitsFromFishingCriticals); + lethalHitsCount += extraLethalHitsFromFishingCriticals; + } + weaponAttackResolution.setNormalWoundsFromLethalHits(lethalHitsCount); } return lethalHitsCount; } - private Optional prepareHitRerolls(int hitRollGoal, UnitAttackModifiers unitAttackModifiers, - DefensiveProfile target) { - var hasAnyHitReroll = unitAttackModifiers.hasRerollFor(RerollStageType.HIT, RerollType.ANY, - target.getUnitTypeKeywords(), this.rangeType); - var hasOnesHitsReroll = unitAttackModifiers.hasRerollFor(RerollStageType.HIT, RerollType.ON_ONES, - target.getUnitTypeKeywords(), this.rangeType); - var hasFailedHitsReroll = unitAttackModifiers.hasRerollFor(RerollStageType.HIT, RerollType.ON_FAILURE, - target.getUnitTypeKeywords(), this.rangeType); + private Optional prepareRerolls(RerollStageType stageType, int rollGoal, + UnitAttackModifiers unitAttackModifiers, DefensiveProfile target) { + var hasAnyReroll = unitAttackModifiers.hasRerollFor(stageType, RerollType.ANY, target.getUnitTypeKeywords(), this.rangeType); + var hasOnesReroll = unitAttackModifiers.hasRerollFor(stageType, RerollType.ON_ONES, target.getUnitTypeKeywords(), this.rangeType); + var hasFailedReroll = unitAttackModifiers.hasRerollFor(stageType, RerollType.ON_FAILURE, target.getUnitTypeKeywords(), this.rangeType); - // Priorize rerolling failed/any hits over ones - if(hasAnyHitReroll || hasFailedHitsReroll) { - return hasAnyHitReroll ? unitAttackModifiers.getRerollFor(RerollStageType.HIT, RerollType.ANY, - target.getUnitTypeKeywords(), this.rangeType, hitRollGoal) : - unitAttackModifiers.getRerollFor(RerollStageType.HIT, RerollType.ON_FAILURE, - target.getUnitTypeKeywords(), this.rangeType, hitRollGoal); - } - if(hasOnesHitsReroll) { - return unitAttackModifiers.getRerollFor(RerollStageType.HIT, RerollType.ON_ONES, - target.getUnitTypeKeywords(), this.rangeType, hitRollGoal); + if (hasAnyReroll || hasFailedReroll) { + return hasAnyReroll ? + unitAttackModifiers.getRerollFor(stageType, RerollType.ANY, target.getUnitTypeKeywords(), this.rangeType, rollGoal) + : unitAttackModifiers.getRerollFor(stageType, RerollType.ON_FAILURE, target.getUnitTypeKeywords(), this.rangeType, rollGoal); } - return Optional.empty(); - } - - private Optional prepareWoundRerolls(int woundRollGoal) { - if(hasRerollWoundsOnes) { - return Optional.of(Reroll.rerollOnOnes()); - } - if(hasRerollWoundsFailures || hasRerollWoundsAny) { - return Optional.of(Reroll.rerollFailures(woundRollGoal)); + if (hasOnesReroll) { + return unitAttackModifiers.getRerollFor(stageType, RerollType.ON_ONES, target.getUnitTypeKeywords(), this.rangeType, rollGoal); } return Optional.empty(); @@ -323,21 +318,21 @@ public class Weapon { } - private int calculateWoundRollGoal(Weapon weapon) { + private int calculateWoundRollGoal(DefensiveProfile target, UnitAttackModifiers unitAttackModifiers) { var woundRollGoal = 4; - var finalWoundPenalty = hasWoundRollPenalty ? 1 : 0; - var toughness = defensiveProfile.getToughness(); - var strength = weapon.getStrength(); + var finalWoundPenalty = unitAttackModifiers.getWoundMalusModifier()._1 ? 1 : 0; + var toughness = target.getToughness(); - if(strength > toughness) { - finalWoundPenalty += woundRollPenaltyIfStrengthIsHigher ? 1 : 0; + if(this.strength > toughness) { + finalWoundPenalty += target.isHasWoundMalusIfStrengthHigherThanToughness() ? 1 : 0; woundRollGoal = strength >= toughness*2 ? 2 : 3; } else if(strength < toughness) { woundRollGoal = toughness*2 >= strength ? 6 : 5; } - var finalWoundBonus = hasWoundRollBonus ? 1 : 0; - if(attackerCharged && weapon.hasLance() && weapon.getRangeType() == WeaponRangeType.MELEE) { + var finalWoundBonus = unitAttackModifiers.getWoundBonusModifier()._1 ? 1 : 0; + var hasLance = (this.hasLance || unitAttackModifiers.isLanceModifier()) && rangeType == WeaponRangeType.MELEE; + if(unitAttackModifiers.isUnitCharged() && hasLance) { finalWoundBonus++; } @@ -348,32 +343,15 @@ public class Weapon { woundRollGoal -= finalWoundRollModifier; // Now apply any anti-X if it is better than the current wound roll - var antiWoundRoll = weapon.getBestAntiFor(defensiveProfile); + var antiWoundRoll = antiUnitEffects.stream() + .filter(effect -> target.getUnitTypeKeywords().contains(effect.getUnitType())) + .mapToInt(AntiUnitEffect::getWoundRollValue) + .min() + .stream().boxed().findFirst(); // Necessary ugliness if(antiWoundRoll.isPresent()) { woundRollGoal = Math.min(woundRollGoal, antiWoundRoll.get()); } return woundRollGoal; } - - /** - * Returns whether this weapon is anti for any of the given types - */ - public boolean isAntiFor(Set unitTypes) { - var antiUnitTypes = antiStats.keySet(); - return unitTypes.stream().anyMatch(antiUnitTypes::contains); - } - public Optional getBestAntiFor(DefensiveProfile target) { - var bestAnti = Optional.empty(); - if(isAntiFor(target.getUnitTypeKeywords())) { - var antiUnitTypes = antiStats.keySet(); - var minAnti = target.getUnitTypeKeywords().stream() - .filter(antiUnitTypes::contains) - .mapToInt(type -> antiStats.get(type)) - .min().getAsInt(); - bestAnti = Optional.of(minAnti); - } - - return bestAnti; - } } diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java index 402a57a..5145556 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java @@ -24,5 +24,6 @@ public class WeaponAttackResolution { private int attacksFromRapidFire; private int attacksFromModifiers; private int attacksFromBlast; + private double lethalHitsFromFishingCriticals; private SaveResolution saveResolution; }