diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java index b81b4d5..969ea43 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java @@ -95,7 +95,7 @@ public class DefensiveProfile { resolution.setPreventedNormalWoundsFromSavesWithoutRerolls(savedNormalWounds); var saveRerolls = calculateBestReroll(saveRollGoal); if(saveRerolls.isPresent()) { - var rerollProbability = saveRerolls.get().getProbabilityNumerator(); + var rerollProbability = saveRerolls.get().getSuccessProbability(); var savedNormalWoundsFromRerolls = normalWounds*(1 - saveProbabilityNumerator)/6 * rerollProbability/6; resolution.setPreventedNormalWoundsFromSavesRerolls(savedNormalWoundsFromRerolls); savedNormalWounds += savedNormalWoundsFromRerolls; diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Reroll.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Reroll.java index 1450ce8..9e81fbb 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Reroll.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Reroll.java @@ -9,41 +9,38 @@ import java.util.stream.IntStream; /** * Assumes a D6 */ -@Getter @AllArgsConstructor +@Getter @Setter @AllArgsConstructor @NoArgsConstructor public class Reroll { private RerollStageType rerollStageType; private RerollType type; - private final Set rerollableValues; + private Set rerollableValues; private Set validTargetTypes; + private Set validWeaponRanges; - public static Reroll rerollOnOnes(RerollStageType rerollStageType, Set validTargetTypes) { - return new Reroll(rerollStageType, RerollType.ON_ONES, Set.of(1), validTargetTypes); + public static Reroll rerollOnOnes(RerollStageType rerollStageType, Set validTargetTypes, Set validWeaponRanges) { + return new Reroll(rerollStageType, RerollType.ON_ONES, Set.of(1), validTargetTypes, validWeaponRanges); } public static Reroll rerollFailures(RerollStageType rerollStageType, int targetHit, - Set validTargetTypes) { + Set validTargetTypes, Set validWeaponRanges) { var rerollableValues = IntStream.range(1, targetHit).boxed() .collect(Collectors.toSet()); - return new Reroll(rerollStageType, RerollType.ON_FAILURE, rerollableValues, validTargetTypes); + return new Reroll(rerollStageType, RerollType.ON_FAILURE, rerollableValues, validTargetTypes, validWeaponRanges); } public static Reroll rerollOnNot(RerollStageType rerollStageType, Set allowedValues, - Set validTargetTypes) { + Set validTargetTypes, Set validWeaponRanges) { var rerollableValues = IntStream.range(1,7).filter(i-> !allowedValues.contains(i)) .boxed().collect(Collectors.toSet()); - return new Reroll(rerollStageType, RerollType.ANY, rerollableValues, validTargetTypes); + return new Reroll(rerollStageType, RerollType.ANY, rerollableValues, validTargetTypes, validWeaponRanges); } /** * From a D6 returns what proportion of rerollable values this reroll is. */ - public double getProbabilityNumerator() { + public double getSuccessProbability() { return (double) rerollableValues.size() / 6; } - - public boolean appliesTo(UnitTypeKeyword target) { - return this.validTargetTypes.contains(target); - } } diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/RerollStageType.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/RerollStageType.java index 5b47ab6..1af6a87 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/RerollStageType.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/RerollStageType.java @@ -1,6 +1,7 @@ package ninja.thefirearchmage.games.fourtykcalculator; public enum RerollStageType { + ATTACKS, HIT, WOUND, DAMAGE; diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java index 369bc4e..f3e38ec 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitAttackModifiers.java @@ -7,6 +7,10 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2 import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple3; import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * Models what modifiers a unit that is performing the attacking provides to its weapons. @@ -18,9 +22,8 @@ public class UnitAttackModifiers { private Tuple2 lethalHitsModifier; private int criticalHitValue = 6; private boolean lanceModifier; - private List hitRerollsModifier; - private List woundRerollsModifier; - private List damageRerollsModifier; + // These rerolls do not contain actual values, they must be generated from the method getRerollFor + private List rerollsModifier; private Tuple3 apModifier; private Tuple2 woundBonusModifier; private Tuple2 woundMalusModifier; @@ -43,5 +46,29 @@ public class UnitAttackModifiers { private boolean isPsychic; private boolean canRerollHazardous; - public boolean hasReroll + public boolean hasRerollFor(RerollStageType stageType, RerollType type, + Set targetsType, WeaponRangeType weaponRangeType) { + return rerollsModifier.stream() + .filter( r -> setContainsAnyOf(r.getValidTargetTypes(), targetsType, UnitTypeKeyword.ALL)) + .filter( r -> setContainsAnyOf(r.getValidWeaponRanges(), Set.of(weaponRangeType), WeaponRangeType.ALL)) + .filter(r -> r.getRerollStageType() == stageType) + .anyMatch(r -> r.getType() == type); + } + + public Optional getRerollFor(RerollStageType stageType, RerollType type, + Set targetsType, WeaponRangeType weaponRangeType, int rollGoal) { + if(hasRerollFor(stageType, type, targetsType, weaponRangeType)){ + return Optional.of(switch(type) { + case ON_ONES -> Reroll.rerollOnOnes(stageType, targetsType, Set.of(weaponRangeType)); + case ON_FAILURE -> Reroll.rerollFailures(stageType, rollGoal, targetsType, Set.of(weaponRangeType)); + case ANY -> Reroll.rerollOnNot(stageType, IntStream.range(rollGoal,7).boxed().collect(Collectors.toSet()), targetsType, Set.of(weaponRangeType)); + }); + } + + return Optional.empty(); + } + + private static boolean setContainsAnyOf(Set oneSet, Set otherSet, T otherValue) { + return oneSet.stream().anyMatch(i -> i.equals(otherValue) || otherSet.contains(i)); + } } diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java index d9870da..ba7fdbe 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java @@ -84,33 +84,35 @@ public class Weapon { private double calculatePassingHits(DefensiveProfile target, UnitAttackModifiers unitAttackModifiers, int amountOfWeapons, WeaponAttackResolution weaponAttackResolution) { int hitRollGoal = calculateHitRollGoal(target, unitAttackModifiers); - int hitSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(hitRollGoal); - int weaponAttacks = calculateWeaponAttacks(amountOfWeapons, unitAttackModifiers, weaponAttackResolution); - - // calculate sustained hits - var sustainedHits = calculateSustainedHits(weaponAttacks, unitAttackModifiers, weaponAttackResolution); - weaponAttackResolution.setHitsFromSustainedHits(sustainedHits); - weaponAttackResolution.setTotalAttacks(weaponAttacks+sustainedHits); + double hitSuccessProbability = (double) calculateProbabilityNumeratorFrom(hitRollGoal) / 6; + double hitFailureProbability = (double) (6 - calculateProbabilityNumeratorFrom(hitRollGoal)) / 6; + int weaponAttacks = calculateWeaponAttacks(amountOfWeapons, unitAttackModifiers, target, weaponAttackResolution); // Base passing hits without rerolls - double passingHits = (weaponAttacks*amountOfWeapons) * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); - results.setWeaponStat(weapon, WeaponStat.HITS_COUNT_WITHOUT_REROLL, passingHits); + double passingHits = weaponAttacks * hitSuccessProbability; + weaponAttackResolution.setHitsWithoutRerolls(passingHits); // Add hits from rerolls - var hitRerolls = prepareHitRerolls(hitRollGoal); + var hitRerolls = prepareHitRerolls(hitRollGoal, unitAttackModifiers, target); if(hitRerolls.isPresent()) { var rerolls = hitRerolls.get(); - var successfulHitFromRerolls = rerolls.getProbabilityNumerator() * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); - results.setWeaponStat(weapon, WeaponStat.HITS_COUNT_FROM_REROLL, successfulHitFromRerolls); + var successfulHitFromRerolls = weaponAttacks * hitFailureProbability * rerolls.getSuccessProbability(); + weaponAttackResolution.setHitsFromRerolls(successfulHitFromRerolls); passingHits += successfulHitFromRerolls; } - results.setWeaponStat(weapon, WeaponStat.HITS_TOTAL_COUNT, passingHits); + + // calculate sustained hits + var sustainedHits = calculateSustainedHits(weaponAttacks, unitAttackModifiers, target); + weaponAttackResolution.setHitsFromSustainedHits(sustainedHits); + passingHits += sustainedHits; + + weaponAttackResolution.setHits(passingHits); return passingHits; } private double calculateSustainedHits(int weaponAttacks, UnitAttackModifiers unitAttackModifiers, - WeaponAttackResolution weaponAttackResolution) { + DefensiveProfile target) { var sustainedHitsAmounts = 0d; var hasSustainedHits = this.sustainedHits.isPresent() || @@ -124,7 +126,9 @@ public class Weapon { double nonSustainedProbability = (double) (6 - (7 - criticalHitValue)) / 6; double sustainedProbability = (double) (7 - criticalHitValue) / 6; - if(unitAttackModifiers.isFishForCriticalHits() && hasRerollAny(unitAttackModifiers.getHitRerollsModifier())) { + var hasHitRerollAny = unitAttackModifiers.hasRerollFor(RerollStageType.HIT, RerollType.ANY, + target.getUnitTypeKeywords(), this.rangeType); + if(unitAttackModifiers.isFishForCriticalHits() && hasHitRerollAny) { sustainedProbability += nonSustainedProbability*sustainedProbability; } sustainedHitsAmounts = weaponAttacks * sustainedProbability * sustainedHitsValue; @@ -150,7 +154,7 @@ public class Weapon { if(woundsRerolls.isPresent()) { var rerolls = woundsRerolls.get(); var failedWounds = normalHits * ((double) (1 - woundSuccessProbabilityNumerator) / PROBABILITY_DENOMINATOR_D6); - var extraWoundsFromRerolls = failedWounds * rerolls.getProbabilityNumerator() / PROBABILITY_DENOMINATOR_D6; + var extraWoundsFromRerolls = failedWounds * rerolls.getSuccessProbability() / PROBABILITY_DENOMINATOR_D6; results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_FROM_REROLL, extraWoundsFromRerolls); passingNormalWounds += extraWoundsFromRerolls; } @@ -192,12 +196,25 @@ public class Weapon { return lethalHitsCount; } - private Optional prepareHitRerolls(int hitRollGoal) { - if(hasRerollHitsOnes) { - return Optional.of(Reroll.rerollOnOnes()); + 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); + + // 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(hasRerollHitsFailures || hasRerollHitsAny) { - return Optional.of(Reroll.rerollFailures(hitRollGoal)); + if(hasOnesHitsReroll) { + return unitAttackModifiers.getRerollFor(RerollStageType.HIT, RerollType.ON_ONES, + target.getUnitTypeKeywords(), this.rangeType, hitRollGoal); } return Optional.empty(); @@ -215,30 +232,33 @@ public class Weapon { } private int calculateWeaponAttacks(int amountOfWeapons, UnitAttackModifiers unitAttackModifiers, - WeaponAttackResolution weaponAttackResolution) { - double weaponAttacks = calculateBasicWeaponAttacks(amountOfWeapons, unitAttackModifiers, weaponAttackResolution); - - - - return weaponAttacks; - } - - private static boolean hasRerollAny(List rerolls) { - return rerolls.stream().anyMatch(r -> r.getType() == RerollType.ANY); - } - - private double calculateBasicWeaponAttacks(int amountOfWeapons, UnitAttackModifiers unitAttackModifiers, - WeaponAttackResolution weaponAttackResolution) { - var weaponAttacks = this.attacks; + DefensiveProfile target, WeaponAttackResolution weaponAttackResolution) { + var weaponAttacks = calculateBasicWeaponAttacks(amountOfWeapons, unitAttackModifiers, weaponAttackResolution); // Rapid fire var hasRapidFire = this.getRapidFire().isPresent() || unitAttackModifiers.getRapidFireModifier()._1; if(hasRapidFire && unitAttackModifiers.isTargetInRapidFireRange()) { var higherRapidFireValue = Math.max(this.rapidFire.get(), unitAttackModifiers.getRapidFireModifier()._3); - weaponAttacks += higherRapidFireValue; - weaponAttackResolution.setAttacksFromRapidFire(higherRapidFireValue*amountOfWeapons); + weaponAttacks += higherRapidFireValue*amountOfWeapons; + weaponAttackResolution.setAttacksFromRapidFire(higherRapidFireValue); } + // Blast + if(this.hasBlast) { + var blastBonus = (int)Math.floor((double)target.getBodies() / 5); + weaponAttacks += blastBonus*amountOfWeapons; + weaponAttackResolution.setAttacksFromBlast(blastBonus); + } + + weaponAttackResolution.setTotalAttacks(weaponAttacks); + + return weaponAttacks; + } + + private int calculateBasicWeaponAttacks(int amountOfWeapons, UnitAttackModifiers unitAttackModifiers, + WeaponAttackResolution weaponAttackResolution) { + var weaponAttacks = this.attacks; + // Attacks modifiers var attackModifier = 0; var attackBonus = unitAttackModifiers.getAttacksBonusModifier(); @@ -253,7 +273,6 @@ public class Weapon { weaponAttacks += attackModifier; weaponAttacks *= amountOfWeapons; - weaponAttackResolution.setTotalAttacks(weaponAttacks); return weaponAttacks; } @@ -298,6 +317,7 @@ public class Weapon { // Because a roll of 1 always fails and a roll of 6 always succeeds, we modify the roll goal to be between // values 2-5 var modifiedRollGoal = Math.max(rollGoal, 2); + modifiedRollGoal = Math.min(modifiedRollGoal, 5); return 7 - modifiedRollGoal; diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java index b358843..402a57a 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponAttackResolution.java @@ -23,5 +23,6 @@ public class WeaponAttackResolution { private double totalAttacks; private int attacksFromRapidFire; private int attacksFromModifiers; + private int attacksFromBlast; private SaveResolution saveResolution; }