Hits implemented, now moving on to calculating wounds

This commit is contained in:
Loic Prieto 2025-06-16 00:38:55 +02:00
parent c2a19499c9
commit 434b534012
5 changed files with 112 additions and 25 deletions

View file

@ -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<Double, Double> 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;
}
}

View file

@ -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> invulnerableSave;
private final Optional<FeelNoPainEffect> feelNoPain;
private final Set<UnitTypeKeyword> 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<UnitTypeKeyword> types) {
this.unitTypeKeywords.addAll(types);
return this;
}
}

View file

@ -0,0 +1,8 @@
package ninja.thefirearchmage.games.fourtykcalculator;
public enum UnitTypeKeyword {
INFANTRY,
VEHICLE,
PSYCHIC,
MONSTER;
}

View file

@ -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<Integer> sustainedHits;
private boolean hasMortalWounds;
private boolean hasDevastatingWounds;
private boolean hasHazardous;
@Getter
private Optional<Integer> antiInfantery;
@Getter
private Optional<Integer> antiVehicle;
@Getter
private Optional<Integer> antiMonster;
@Getter
private Optional<Integer> antiPsychic;
private Map<UnitTypeKeyword, Integer> antiStats;
private boolean hasHeavy;
private boolean hasBlast;
private boolean hasLance;
@ -42,12 +38,19 @@ public class Weapon {
@Getter
private Optional<Integer> 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<Integer> 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<UnitTypeKeyword> unitTypes) {
var antiUnitTypes = antiStats.keySet();
return unitTypes.stream().anyMatch(antiUnitTypes::contains);
}
public Optional<Integer> getBestAntiFor(DefensiveProfile target) {
var bestAnti = Optional.<Integer>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;
}
}

View file

@ -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;