Start refactoring of wounds calculation
This commit is contained in:
parent
96a5e91e0b
commit
2f2c5dfabd
4 changed files with 68 additions and 86 deletions
|
@ -3,10 +3,7 @@ package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple3;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple3;
|
||||||
|
|
||||||
import java.util.HashSet;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class DefensiveProfile {
|
public class DefensiveProfile {
|
||||||
|
@ -25,31 +22,33 @@ public class DefensiveProfile {
|
||||||
private boolean fishFor6NormalSaves;
|
private boolean fishFor6NormalSaves;
|
||||||
private boolean hasStealth;
|
private boolean hasStealth;
|
||||||
private boolean isInStealthRange;
|
private boolean isInStealthRange;
|
||||||
|
private boolean hasWoundMalusIfStrengthHigherThanToughness;
|
||||||
private Tuple3<Boolean, DamageReductionType, Integer> damageReductionModifier;
|
private Tuple3<Boolean, DamageReductionType, Integer> damageReductionModifier;
|
||||||
private Tuple3<Boolean, WeaponRangeType, Integer> apReduction;
|
private Tuple3<Boolean, WeaponRangeType, Integer> apReduction;
|
||||||
|
|
||||||
public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave) {
|
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,
|
new HashSet<>(), false, false, false,
|
||||||
false, true);
|
false, true, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave, Optional<InvulnerableSave> invulnerableSave,
|
public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave, Optional<InvulnerableSave> invulnerableSave,
|
||||||
Optional<FeelNoPainEffect> feelNoPain, Set<UnitTypeKeyword> unitTypeKeywords,
|
List<FeelNoPainEffect> feelNoPain, Set<UnitTypeKeyword> unitTypeKeywords,
|
||||||
boolean isInCover, boolean hasRerollOneNormalSaves, boolean hasRerollFailedNormalSaves,
|
boolean isInCover, boolean hasRerollOneNormalSaves, boolean hasRerollFailedNormalSaves,
|
||||||
boolean hasRerollAnyNormalSaves, boolean isVisible) {
|
boolean hasRerollAnyNormalSaves, boolean isVisible, boolean hasWoundMalusIfStrengthHigherThanToughness) {
|
||||||
this.toughness = toughness;
|
this.toughness = toughness;
|
||||||
this.normalSave = normalSave;
|
this.normalSave = normalSave;
|
||||||
this.wounds = wounds;
|
this.wounds = wounds;
|
||||||
this.bodies = bodies;
|
this.bodies = bodies;
|
||||||
this.invulnerableSave = invulnerableSave;
|
this.invulnerableSave = invulnerableSave;
|
||||||
this.feelNoPain = feelNoPain;
|
this.feelNoPainEffects = feelNoPain;
|
||||||
this.unitTypeKeywords = unitTypeKeywords;
|
this.unitTypeKeywords = unitTypeKeywords;
|
||||||
this.isInCover = isInCover;
|
this.isInCover = isInCover;
|
||||||
this.hasRerollOneNormalSaves = hasRerollOneNormalSaves;
|
this.hasRerollOneNormalSaves = hasRerollOneNormalSaves;
|
||||||
this.hasRerollFailedNormalSaves = hasRerollFailedNormalSaves;
|
this.hasRerollFailedNormalSaves = hasRerollFailedNormalSaves;
|
||||||
this.hasRerollAnyNormalSaves = hasRerollAnyNormalSaves;
|
this.hasRerollAnyNormalSaves = hasRerollAnyNormalSaves;
|
||||||
this.isVisible = isVisible;
|
this.isVisible = isVisible;
|
||||||
|
this.hasWoundMalusIfStrengthHigherThanToughness = hasWoundMalusIfStrengthHigherThanToughness;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefensiveProfile withUnitTypes(Set<UnitTypeKeyword> types) {
|
public DefensiveProfile withUnitTypes(Set<UnitTypeKeyword> types) {
|
||||||
|
@ -68,7 +67,7 @@ public class DefensiveProfile {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
public DefensiveProfile withFeelNoPainConfig(FeelNoPainEffect feelNoPainConfig) {
|
public DefensiveProfile withFeelNoPainConfig(FeelNoPainEffect feelNoPainConfig) {
|
||||||
this.feelNoPain = Optional.of(feelNoPainConfig);
|
this.feelNoPainEffects.add(feelNoPainConfig);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
public DefensiveProfile setInCover(boolean inCover) {
|
public DefensiveProfile setInCover(boolean inCover) {
|
||||||
|
|
|
@ -71,4 +71,8 @@ public class UnitAttackModifiers {
|
||||||
private static <T> boolean setContainsAnyOf(Set<T> oneSet, Set<T> otherSet, T otherValue) {
|
private static <T> boolean setContainsAnyOf(Set<T> oneSet, Set<T> otherSet, T otherValue) {
|
||||||
return oneSet.stream().anyMatch(i -> i.equals(otherValue) || otherSet.contains(i));
|
return oneSet.stream().anyMatch(i -> i.equals(otherValue) || otherSet.contains(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean lethalHitsAppliesTo(Weapon weapon) {
|
||||||
|
return lethalHitsModifier._1 && lethalHitsModifier._2.appliesTo(weapon);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple;
|
||||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class Weapon {
|
public class Weapon {
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -73,7 +74,7 @@ public class Weapon {
|
||||||
var weaponAttackResolution = new WeaponAttackResolution();
|
var weaponAttackResolution = new WeaponAttackResolution();
|
||||||
|
|
||||||
var passingHits = calculatePassingHits(target, unitModifiers, weaponAmount, 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 normalWounds = passingWounds._1;
|
||||||
var mortalWounds = passingWounds._2;
|
var mortalWounds = passingWounds._2;
|
||||||
|
|
||||||
|
@ -93,7 +94,7 @@ public class Weapon {
|
||||||
weaponAttackResolution.setHitsWithoutRerolls(passingHits);
|
weaponAttackResolution.setHitsWithoutRerolls(passingHits);
|
||||||
|
|
||||||
// Add hits from rerolls
|
// Add hits from rerolls
|
||||||
var hitRerolls = prepareHitRerolls(hitRollGoal, unitAttackModifiers, target);
|
var hitRerolls = prepareRerolls(RerollStageType.HIT, hitRollGoal, unitAttackModifiers, target);
|
||||||
if(hitRerolls.isPresent()) {
|
if(hitRerolls.isPresent()) {
|
||||||
var rerolls = hitRerolls.get();
|
var rerolls = hitRerolls.get();
|
||||||
var successfulHitFromRerolls = weaponAttacks * hitFailureProbability * rerolls.getSuccessProbability();
|
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.
|
* Returns a tuple of normal passing wounds and mortal wounds so that the caller can handle them differently.
|
||||||
*/
|
*/
|
||||||
private Tuple2<Double, Double> calculatePassingWounds(Weapon weapon, int amountOfWeapons, double passingHits) {
|
private Tuple2<Double, Double> calculatePassingWounds(DefensiveProfile target, UnitAttackModifiers unitAttackModifiers,
|
||||||
double lethalHitsCount = calculateLethalHits(weapon, amountOfWeapons);
|
int amountOfWeapons, double passingHits,
|
||||||
|
WeaponAttackResolution weaponAttackResolution) {
|
||||||
|
double lethalHitsCount = calculateLethalHits(amountOfWeapons, unitAttackModifiers, target, weaponAttackResolution);
|
||||||
var normalHits = passingHits - lethalHitsCount;
|
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);
|
int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(woundRollGoal);
|
||||||
|
|
||||||
var passingNormalWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6);
|
var passingNormalWounds = normalHits * woundSuccessProbabilityNumerator / 6;
|
||||||
results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_WITHOUT_REROLL, passingNormalWounds+lethalHitsCount);
|
weaponAttackResolution.setNormalWoundsWithoutRerolls(passingNormalWounds + lethalHitsCount);
|
||||||
var woundsRerolls = prepareWoundRerolls(woundRollGoal);
|
|
||||||
|
var woundsRerolls = prepareRerolls(RerollStageType.WOUND, woundRollGoal, unitAttackModifiers, target);
|
||||||
if(woundsRerolls.isPresent()) {
|
if(woundsRerolls.isPresent()) {
|
||||||
var rerolls = woundsRerolls.get();
|
var rerolls = woundsRerolls.get();
|
||||||
var failedWounds = normalHits * ((double) (1 - woundSuccessProbabilityNumerator) / PROBABILITY_DENOMINATOR_D6);
|
var failedWounds = normalHits * (6 - woundSuccessProbabilityNumerator) / 6;
|
||||||
var extraWoundsFromRerolls = failedWounds * rerolls.getSuccessProbability() / PROBABILITY_DENOMINATOR_D6;
|
var extraWoundsFromRerolls = failedWounds * rerolls.getSuccessProbability() / 6;
|
||||||
results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_FROM_REROLL, extraWoundsFromRerolls);
|
weaponAttackResolution.setNormalWoundsFromRerolls(extraWoundsFromRerolls);
|
||||||
passingNormalWounds += extraWoundsFromRerolls;
|
passingNormalWounds += extraWoundsFromRerolls;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,50 +185,42 @@ public class Weapon {
|
||||||
return Tuple.of(passingNormalWounds, passingMortalWounds);
|
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 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()) {
|
lethalHitsCount = weaponAttacks * lethalHitsProbability;
|
||||||
double weaponAttacks = calculateBasicWeaponAttacks(weapon, amountOfWeapons);
|
// If we're fishing for hit criticals, the lethal hits will be higher
|
||||||
var lethalHitsProbabilityNumerator = calculateProbabilityNumeratorFrom(criticalHitValue);
|
if(unitAttackModifiers.isFishForCriticalHits()) {
|
||||||
|
var lethalHitsFailureProbability = (6 - (lethalHitsProbability * 6)) / 6;
|
||||||
lethalHitsCount = weaponAttacks * ((double) lethalHitsProbabilityNumerator / PROBABILITY_DENOMINATOR_D6);
|
var extraLethalHitsFromFishingCriticals = weaponAttacks * lethalHitsFailureProbability * lethalHitsProbability;
|
||||||
results.setWeaponStat(weapon, WeaponStat.LETHAL_HITS_COUNT, lethalHitsCount);
|
weaponAttackResolution.setLethalHitsFromFishingCriticals(extraLethalHitsFromFishingCriticals);
|
||||||
|
lethalHitsCount += extraLethalHitsFromFishingCriticals;
|
||||||
|
}
|
||||||
|
weaponAttackResolution.setNormalWoundsFromLethalHits(lethalHitsCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
return lethalHitsCount;
|
return lethalHitsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Reroll> prepareHitRerolls(int hitRollGoal, UnitAttackModifiers unitAttackModifiers,
|
private Optional<Reroll> prepareRerolls(RerollStageType stageType, int rollGoal,
|
||||||
DefensiveProfile target) {
|
UnitAttackModifiers unitAttackModifiers, DefensiveProfile target) {
|
||||||
var hasAnyHitReroll = unitAttackModifiers.hasRerollFor(RerollStageType.HIT, RerollType.ANY,
|
var hasAnyReroll = unitAttackModifiers.hasRerollFor(stageType, RerollType.ANY, target.getUnitTypeKeywords(), this.rangeType);
|
||||||
target.getUnitTypeKeywords(), this.rangeType);
|
var hasOnesReroll = unitAttackModifiers.hasRerollFor(stageType, RerollType.ON_ONES, target.getUnitTypeKeywords(), this.rangeType);
|
||||||
var hasOnesHitsReroll = unitAttackModifiers.hasRerollFor(RerollStageType.HIT, RerollType.ON_ONES,
|
var hasFailedReroll = unitAttackModifiers.hasRerollFor(stageType, RerollType.ON_FAILURE, target.getUnitTypeKeywords(), this.rangeType);
|
||||||
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 (hasAnyReroll || hasFailedReroll) {
|
||||||
if(hasAnyHitReroll || hasFailedHitsReroll) {
|
return hasAnyReroll ?
|
||||||
return hasAnyHitReroll ? unitAttackModifiers.getRerollFor(RerollStageType.HIT, RerollType.ANY,
|
unitAttackModifiers.getRerollFor(stageType, RerollType.ANY, target.getUnitTypeKeywords(), this.rangeType, rollGoal)
|
||||||
target.getUnitTypeKeywords(), this.rangeType, hitRollGoal) :
|
: unitAttackModifiers.getRerollFor(stageType, RerollType.ON_FAILURE, target.getUnitTypeKeywords(), this.rangeType, rollGoal);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.empty();
|
if (hasOnesReroll) {
|
||||||
}
|
return unitAttackModifiers.getRerollFor(stageType, RerollType.ON_ONES, target.getUnitTypeKeywords(), this.rangeType, rollGoal);
|
||||||
|
|
||||||
private Optional<Reroll> prepareWoundRerolls(int woundRollGoal) {
|
|
||||||
if(hasRerollWoundsOnes) {
|
|
||||||
return Optional.of(Reroll.rerollOnOnes());
|
|
||||||
}
|
|
||||||
if(hasRerollWoundsFailures || hasRerollWoundsAny) {
|
|
||||||
return Optional.of(Reroll.rerollFailures(woundRollGoal));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.empty();
|
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 woundRollGoal = 4;
|
||||||
var finalWoundPenalty = hasWoundRollPenalty ? 1 : 0;
|
var finalWoundPenalty = unitAttackModifiers.getWoundMalusModifier()._1 ? 1 : 0;
|
||||||
var toughness = defensiveProfile.getToughness();
|
var toughness = target.getToughness();
|
||||||
var strength = weapon.getStrength();
|
|
||||||
|
|
||||||
if(strength > toughness) {
|
if(this.strength > toughness) {
|
||||||
finalWoundPenalty += woundRollPenaltyIfStrengthIsHigher ? 1 : 0;
|
finalWoundPenalty += target.isHasWoundMalusIfStrengthHigherThanToughness() ? 1 : 0;
|
||||||
woundRollGoal = strength >= toughness*2 ? 2 : 3;
|
woundRollGoal = strength >= toughness*2 ? 2 : 3;
|
||||||
} else if(strength < toughness) {
|
} else if(strength < toughness) {
|
||||||
woundRollGoal = toughness*2 >= strength ? 6 : 5;
|
woundRollGoal = toughness*2 >= strength ? 6 : 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
var finalWoundBonus = hasWoundRollBonus ? 1 : 0;
|
var finalWoundBonus = unitAttackModifiers.getWoundBonusModifier()._1 ? 1 : 0;
|
||||||
if(attackerCharged && weapon.hasLance() && weapon.getRangeType() == WeaponRangeType.MELEE) {
|
var hasLance = (this.hasLance || unitAttackModifiers.isLanceModifier()) && rangeType == WeaponRangeType.MELEE;
|
||||||
|
if(unitAttackModifiers.isUnitCharged() && hasLance) {
|
||||||
finalWoundBonus++;
|
finalWoundBonus++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,32 +343,15 @@ public class Weapon {
|
||||||
woundRollGoal -= finalWoundRollModifier;
|
woundRollGoal -= finalWoundRollModifier;
|
||||||
|
|
||||||
// Now apply any anti-X if it is better than the current wound roll
|
// 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()) {
|
if(antiWoundRoll.isPresent()) {
|
||||||
woundRollGoal = Math.min(woundRollGoal, antiWoundRoll.get());
|
woundRollGoal = Math.min(woundRollGoal, antiWoundRoll.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
return woundRollGoal;
|
return woundRollGoal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,5 +24,6 @@ public class WeaponAttackResolution {
|
||||||
private int attacksFromRapidFire;
|
private int attacksFromRapidFire;
|
||||||
private int attacksFromModifiers;
|
private int attacksFromModifiers;
|
||||||
private int attacksFromBlast;
|
private int attacksFromBlast;
|
||||||
|
private double lethalHitsFromFishingCriticals;
|
||||||
private SaveResolution saveResolution;
|
private SaveResolution saveResolution;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue