diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java index ed21ecc..9b36272 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java @@ -97,18 +97,42 @@ public class AttackScenario { int amountOfWeapons = weaponData._2; // Attack probabilities - int finalHitRollGoal = calculateHitRollGoal(weapon); - int finalWoundRollGoal = calculateWoundRollGoal(weapon.getStrength(), defensiveProfile.getToughness(), weapon); - var passingHits = calculatePassingHits(weapon, amountOfWeapons, finalHitRollGoal); + var passingHits = calculatePassingHits(weapon, amountOfWeapons); + var passingWounds = calculatePassingWounds(weapon, amountOfWeapons, passingHits); - - int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(finalWoundRollGoal); } } - private double calculatePassingHits(Weapon weapon, int amountOfWeapons, int hitRollGoal) { + private Tuple2 calculatePassingWounds(Weapon weapon, int amountOfWeapons, double passingHits) { + double lethalHitsCount = calculateLethalHits(weapon, amountOfWeapons); + var normalHits = passingHits - lethalHitsCount; + results.setWeaponStat(weapon, WeaponStat.NON_LETHAL_HITS_COUNT, normalHits); + + int woundRollGoal = calculateWoundRollGoal(weapon); + int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(woundRollGoal); + + var passingWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6) + + } + + private double calculateLethalHits(Weapon weapon, int amountOfWeapons) { + var lethalHitsCount = 0d; + + 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); + } + + return lethalHitsCount; + } + + + private double calculatePassingHits(Weapon weapon, int amountOfWeapons) { + int hitRollGoal = calculateHitRollGoal(weapon); int hitSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(hitRollGoal); - int hitFailureProbabilityNumerator = 6 - hitSuccessProbabilityNumerator; double weaponAttacks = calculateWeaponAttacks(weapon, amountOfWeapons); // Base passing hits without rerolls @@ -138,11 +162,7 @@ public class AttackScenario { } private double calculateWeaponAttacks(Weapon weapon, int amountOfWeapons) { - double weaponAttacks = weapon.getAttacks(); - if(weapon.getRapidFire().isPresent() && attackerInRapidFireRange) { - weaponAttacks += weapon.getRapidFire().get(); - } - weaponAttacks *= amountOfWeapons; + double weaponAttacks = calculateBasicWeaponAttacks(weapon, amountOfWeapons); results.setWeaponStat(weapon, WeaponStat.POTENTIAL_HITS_COUNT, weaponAttacks); // Add sustained hits @@ -162,6 +182,16 @@ public class AttackScenario { return weaponAttacks; } + private double calculateBasicWeaponAttacks(Weapon weapon, int amountOfWeapons) { + double weaponAttacks = weapon.getAttacks(); + if(weapon.getRapidFire().isPresent() && attackerInRapidFireRange) { + weaponAttacks += weapon.getRapidFire().get(); + } + weaponAttacks *= amountOfWeapons; + + return weaponAttacks; + } + private int calculateHitRollGoal(Weapon weapon) { var finalHitRollGoal = weapon.getHitValue(); if(hasHitRollBonus) { @@ -191,9 +221,11 @@ public class AttackScenario { } - private int calculateWoundRollGoal(int strength, int toughness, Weapon weapon) { + private int calculateWoundRollGoal(Weapon weapon) { var woundRollGoal = 4; var finalWoundPenalty = hasWoundRollPenalty ? 1 : 0; + var toughness = defensiveProfile.getToughness(); + var strength = weapon.getStrength(); if(strength > toughness) { finalWoundPenalty += woundRollPenaltyIfStrengthIsHigher ? 1 : 0; @@ -213,6 +245,12 @@ public class AttackScenario { // Having a -1 to wound rolls implies that the roll goal moves by 1 woundRollGoal -= finalWoundRollModifier; + // Now apply any anti-X if it is better than the current wound roll + var antiWoundRoll = weapon.getBestAntiFor(defensiveProfile); + if(antiWoundRoll.isPresent()) { + woundRollGoal = Math.min(woundRollGoal, antiWoundRoll.get()); + } + return woundRollGoal; } } diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java index 4853c90..7b4a258 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/DefensiveProfile.java @@ -2,7 +2,9 @@ package ninja.thefirearchmage.games.fourtykcalculator; import lombok.Getter; +import java.util.HashSet; import java.util.Optional; +import java.util.Set; @Getter public class DefensiveProfile { @@ -12,6 +14,7 @@ public class DefensiveProfile { private final int bodies; private final Optional invulnerableSave; private final Optional feelNoPain; + private final Set unitTypeKeywords; public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave) { this(toughness, wounds, bodies, normalSave, Optional.empty(), Optional.empty()); @@ -24,5 +27,11 @@ public class DefensiveProfile { this.invulnerableSave = invulnerableSave; this.feelNoPain = feelNoPain; this.bodies = bodies; + this.unitTypeKeywords = new HashSet<>(); + } + + public DefensiveProfile withUnitTypes(Set types) { + this.unitTypeKeywords.addAll(types); + return this; } } diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitTypeKeyword.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitTypeKeyword.java new file mode 100644 index 0000000..2e735f8 --- /dev/null +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/UnitTypeKeyword.java @@ -0,0 +1,8 @@ +package ninja.thefirearchmage.games.fourtykcalculator; + +public enum UnitTypeKeyword { + INFANTRY, + VEHICLE, + PSYCHIC, + MONSTER; +} diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java index 1ed71cc..9e4a4c0 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/Weapon.java @@ -2,7 +2,10 @@ package ninja.thefirearchmage.games.fourtykcalculator; import lombok.Getter; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; +import java.util.Set; public class Weapon { @Getter @@ -14,7 +17,7 @@ public class Weapon { @Getter private int attacks; @Getter - private WeaponDamage averageDamage; + private WeaponDamage damage; @Getter private WeaponRangeType rangeType; @Getter @@ -22,16 +25,9 @@ public class Weapon { private boolean hasLethalHits; @Getter private Optional sustainedHits; - private boolean hasMortalWounds; + private boolean hasDevastatingWounds; private boolean hasHazardous; - @Getter - private Optional antiInfantery; - @Getter - private Optional antiVehicle; - @Getter - private Optional antiMonster; - @Getter - private Optional antiPsychic; + private Map antiStats; private boolean hasHeavy; private boolean hasBlast; private boolean hasLance; @@ -42,12 +38,19 @@ public class Weapon { @Getter private Optional rapidFire; + public Weapon() { + this.antiStats = new HashMap<>(); + this.melta = Optional.empty(); + this.rapidFire = Optional.empty(); + this.sustainedHits = Optional.empty(); + } + public boolean hasLethalHits() { return hasLethalHits; } - public boolean hasMortalWounds() { - return hasMortalWounds; + public boolean hasDevastatingWounds() { + return hasDevastatingWounds; } public boolean hasHazardous() { @@ -72,4 +75,32 @@ public class Weapon { public boolean hasIndirectFire() { return hasIndirectFire; } + + /** + * Returns whether this weapon has Anti-X where X is the given unit type. + */ + public Optional getAnti(UnitTypeKeyword unitType) { + return Optional.ofNullable(antiStats.get(unitType)); + } + + /** + * 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/WeaponStat.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java index 79109f5..124def3 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java @@ -11,6 +11,7 @@ public enum WeaponStat { MORTAL_WOUNDS_COUNT(Double.class), TOTAL_DAMAGE(Double.class), LETHAL_HITS_COUNT(Double.class), + NON_LETHAL_HITS_COUNT(Double.class), SUSTAINED_HITS_COUNT(Double.class); private final Class statClass;