Implemented both attack and defence, now comes refactoring
This commit is contained in:
parent
fa2f37b813
commit
5a19f29e3b
8 changed files with 204 additions and 22 deletions
|
@ -84,12 +84,13 @@ public class AttackScenario {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttackScenarioResolution resolveAttack() {
|
public AttackScenarioResolution resolveAttack() {
|
||||||
var resolution = new AttackScenarioResolution();
|
|
||||||
// Filter out weapons that cannot participate in this attack
|
// Filter out weapons that cannot participate in this attack
|
||||||
var validWeapons = weapons.stream().filter( w -> hasLineOfSight || w._1.hasIndirectFire())
|
var validWeapons = weapons.stream().filter( w -> hasLineOfSight || w._1.hasIndirectFire())
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Attack step
|
// Attack step
|
||||||
|
var totalDamage = 0d;
|
||||||
|
var totalPreventedDamage = 0d;
|
||||||
for(var weaponData : validWeapons) {
|
for(var weaponData : validWeapons) {
|
||||||
Weapon weapon = weaponData._1;
|
Weapon weapon = weaponData._1;
|
||||||
int amountOfWeapons = weaponData._2;
|
int amountOfWeapons = weaponData._2;
|
||||||
|
@ -101,17 +102,18 @@ public class AttackScenario {
|
||||||
var mortalWounds = passingWounds._2;
|
var mortalWounds = passingWounds._2;
|
||||||
|
|
||||||
// Defence funnel
|
// Defence funnel
|
||||||
executeDefenceFunnel(weapon, normalWounds, mortalWounds);
|
var savesResolution = defensiveProfile.saveWounds(weapon, normalWounds, mortalWounds);
|
||||||
|
results.setWeaponStat(weapon, WeaponStat.DAMAGE_NORMAL, normalWounds - savesResolution.getPreventedNormalWounds());
|
||||||
|
results.setWeaponStat(weapon, WeaponStat.DAMAGE_MORTAL, mortalWounds - savesResolution.getPreventedMortalWoundsFromFNP());
|
||||||
|
results.setWeaponStat(weapon, WeaponStat.DAMAGE_TOTAL, normalWounds + mortalWounds - savesResolution.getPreventedWounds());
|
||||||
|
|
||||||
|
totalDamage += normalWounds + mortalWounds - savesResolution.getPreventedWounds();
|
||||||
|
totalPreventedDamage += savesResolution.getPreventedWounds();
|
||||||
}
|
}
|
||||||
|
results.setTotalPreventedDamage(totalDamage);
|
||||||
|
results.setTotalPreventedDamage(totalPreventedDamage);
|
||||||
|
|
||||||
return resolution;
|
return results;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void executeDefenceFunnel(Weapon weapon, double normalWounds, double mortalWounds) {
|
|
||||||
// Save normal wounds
|
|
||||||
|
|
||||||
//
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package ninja.thefirearchmage.games.fourtykcalculator;
|
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple;
|
||||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
||||||
|
|
||||||
|
@ -14,9 +15,15 @@ import static java.lang.String.format;
|
||||||
@Getter
|
@Getter
|
||||||
public class AttackScenarioResolution {
|
public class AttackScenarioResolution {
|
||||||
private Map<String, Map<WeaponStat, Object>> weaponStats;
|
private Map<String, Map<WeaponStat, Object>> weaponStats;
|
||||||
|
@Setter
|
||||||
|
private double totalInflictedDamage;
|
||||||
|
@Setter
|
||||||
|
private double totalPreventedDamage;
|
||||||
|
|
||||||
public AttackScenarioResolution() {
|
public AttackScenarioResolution() {
|
||||||
this.weaponStats = new HashMap<>();
|
this.weaponStats = new HashMap<>();
|
||||||
|
totalInflictedDamage = 0;
|
||||||
|
totalPreventedDamage = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setWeaponStat(Weapon weapon, WeaponStat statName, Object statValue) {
|
public void setWeaponStat(Weapon weapon, WeaponStat statName, Object statValue) {
|
||||||
|
|
|
@ -12,26 +12,163 @@ public class DefensiveProfile {
|
||||||
private final int normalSave;
|
private final int normalSave;
|
||||||
private final int wounds;
|
private final int wounds;
|
||||||
private final int bodies;
|
private final int bodies;
|
||||||
private final Optional<InvulnerableSave> invulnerableSave;
|
private Optional<InvulnerableSave> invulnerableSave;
|
||||||
private final Optional<FeelNoPainEffect> feelNoPain;
|
private Optional<FeelNoPainEffect> feelNoPain;
|
||||||
private final Set<UnitTypeKeyword> unitTypeKeywords;
|
private Set<UnitTypeKeyword> unitTypeKeywords;
|
||||||
|
private boolean isInCover;
|
||||||
|
private boolean isVisible;
|
||||||
|
private boolean hasRerollOneNormalSaves;
|
||||||
|
private boolean hasRerollFailedNormalSaves;
|
||||||
|
private boolean hasRerollAnyNormalSaves;
|
||||||
|
private boolean fishFor6NormalSaves;
|
||||||
|
|
||||||
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(), Optional.empty(),
|
||||||
|
new HashSet<>(), false, false, false,
|
||||||
|
false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave, Optional<InvulnerableSave> invulnerableSave, Optional<FeelNoPainEffect> feelNoPain) {
|
public DefensiveProfile(int toughness, int wounds, int bodies, int normalSave, Optional<InvulnerableSave> invulnerableSave,
|
||||||
|
Optional<FeelNoPainEffect> feelNoPain, Set<UnitTypeKeyword> unitTypeKeywords,
|
||||||
|
boolean isInCover, boolean hasRerollOneNormalSaves, boolean hasRerollFailedNormalSaves,
|
||||||
|
boolean hasRerollAnyNormalSaves, boolean isVisible) {
|
||||||
this.toughness = toughness;
|
this.toughness = toughness;
|
||||||
this.wounds = wounds;
|
|
||||||
this.normalSave = normalSave;
|
this.normalSave = normalSave;
|
||||||
|
this.wounds = wounds;
|
||||||
|
this.bodies = bodies;
|
||||||
this.invulnerableSave = invulnerableSave;
|
this.invulnerableSave = invulnerableSave;
|
||||||
this.feelNoPain = feelNoPain;
|
this.feelNoPain = feelNoPain;
|
||||||
this.bodies = bodies;
|
this.unitTypeKeywords = unitTypeKeywords;
|
||||||
this.unitTypeKeywords = new HashSet<>();
|
this.isInCover = isInCover;
|
||||||
|
this.hasRerollOneNormalSaves = hasRerollOneNormalSaves;
|
||||||
|
this.hasRerollFailedNormalSaves = hasRerollFailedNormalSaves;
|
||||||
|
this.hasRerollAnyNormalSaves = hasRerollAnyNormalSaves;
|
||||||
|
this.isVisible = isVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefensiveProfile withUnitTypes(Set<UnitTypeKeyword> types) {
|
public DefensiveProfile withUnitTypes(Set<UnitTypeKeyword> types) {
|
||||||
this.unitTypeKeywords.addAll(types);
|
this.unitTypeKeywords.addAll(types);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
public DefensiveProfile withRerollsConfig(boolean rerollOneNormalSaves, boolean rerollFailedNormalSaves,
|
||||||
|
boolean rerollAnyNormalSaves) {
|
||||||
|
this.hasRerollOneNormalSaves = rerollOneNormalSaves;
|
||||||
|
this.hasRerollFailedNormalSaves = rerollFailedNormalSaves;
|
||||||
|
this.hasRerollAnyNormalSaves = rerollAnyNormalSaves;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public DefensiveProfile withInvulnerabilitySave(InvulnerableSave invulnerabilitySave) {
|
||||||
|
this.invulnerableSave = Optional.of(invulnerabilitySave);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public DefensiveProfile withFeelNoPainConfig(FeelNoPainEffect feelNoPainConfig) {
|
||||||
|
this.feelNoPain = Optional.of(feelNoPainConfig);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public DefensiveProfile setInCover(boolean inCover) {
|
||||||
|
this.isInCover = inCover;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public DefensiveProfile setIsVisible(boolean isVisible) {
|
||||||
|
this.isVisible = isVisible;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a detailed description of how the wounds applied to this defensive profile were prevented
|
||||||
|
*/
|
||||||
|
public SaveResolution saveWounds(Weapon weapon, double normalWounds, double mortalWounds) {
|
||||||
|
var resolution = new SaveResolution();
|
||||||
|
|
||||||
|
// Get the best save for this weapon
|
||||||
|
var saveRollGoal = calculateBestSaveFor(weapon);
|
||||||
|
var saveProbabilityNumerator = 7 - saveRollGoal;
|
||||||
|
|
||||||
|
// Normal wounds
|
||||||
|
var savedNormalWounds = normalWounds * saveProbabilityNumerator/6;
|
||||||
|
resolution.setPreventedNormalWoundsFromSavesWithoutRerolls(savedNormalWounds);
|
||||||
|
var saveRerolls = calculateBestReroll(saveRollGoal);
|
||||||
|
if(saveRerolls.isPresent()) {
|
||||||
|
var rerollProbability = saveRerolls.get().getProbabilityNumerator();
|
||||||
|
var savedNormalWoundsFromRerolls = normalWounds*(1 - saveProbabilityNumerator)/6 * rerollProbability/6;
|
||||||
|
resolution.setPreventedNormalWoundsFromSavesRerolls(savedNormalWoundsFromRerolls);
|
||||||
|
savedNormalWounds += savedNormalWoundsFromRerolls;
|
||||||
|
}
|
||||||
|
resolution.setPreventedNormalWoundsFromSaves(savedNormalWounds);
|
||||||
|
|
||||||
|
// Feel no pain saves
|
||||||
|
var preventedWoundsFromFNP = 0d;
|
||||||
|
if(feelNoPain.isPresent()) {
|
||||||
|
var fnp = feelNoPain.get();
|
||||||
|
var fnpRollGoalProbability = fnp.getRollValue() / 6;
|
||||||
|
// Normal
|
||||||
|
var remainingNormalWounds = normalWounds - savedNormalWounds;
|
||||||
|
var fnpedNormalWounds = 0d;
|
||||||
|
if(fnp.getType() == FeelNoPainType.ALL) {
|
||||||
|
fnpedNormalWounds += remainingNormalWounds * fnpRollGoalProbability;
|
||||||
|
resolution.setPreventedNormalWoundsFromFNP(fnpedNormalWounds);
|
||||||
|
savedNormalWounds -= fnpedNormalWounds;
|
||||||
|
preventedWoundsFromFNP += fnpedNormalWounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mortal wounds
|
||||||
|
var fnpedMortalWounds = 0d;
|
||||||
|
if(fnp.getType() == FeelNoPainType.ALL || fnp.getType() == FeelNoPainType.MORTAL_WOUNDS) {
|
||||||
|
fnpedMortalWounds += mortalWounds * fnpRollGoalProbability;
|
||||||
|
resolution.setPreventedMortalWoundsFromFNP(fnpedMortalWounds);
|
||||||
|
mortalWounds -= fnpedMortalWounds;
|
||||||
|
preventedWoundsFromFNP += fnpedMortalWounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolution.setPreventedWoundsFromFNP(fnpedNormalWounds + fnpedMortalWounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolution.setPreventedNormalWounds(savedNormalWounds);
|
||||||
|
resolution.setPreventedWounds(savedNormalWounds + preventedWoundsFromFNP);
|
||||||
|
|
||||||
|
return resolution;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<Reroll> calculateBestReroll(int saveTarget) {
|
||||||
|
if(hasRerollOneNormalSaves) {
|
||||||
|
return Optional.of(Reroll.rerollOnOnes());
|
||||||
|
} else if (hasRerollFailedNormalSaves || hasRerollAnyNormalSaves) {
|
||||||
|
return Optional.of(fishFor6NormalSaves && hasRerollAnyNormalSaves ? Reroll.rerollOnNot(Set.of(6)) :
|
||||||
|
Reroll.rerollFailures(saveTarget));
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateBestSaveFor(Weapon weapon) {
|
||||||
|
var ap = weapon.getAp();
|
||||||
|
// Augment basic save with potential modifiers
|
||||||
|
var finalSave = normalSave;
|
||||||
|
var finalIsInCover = isInCover;
|
||||||
|
// Indirect fire save bonus
|
||||||
|
if(weapon.hasIndirectFire() && !isVisible) {
|
||||||
|
finalIsInCover = true;
|
||||||
|
}
|
||||||
|
// Cover save bonus
|
||||||
|
if(finalIsInCover && !weapon.ignoresCover()) {
|
||||||
|
// Saves better than +3 get no cover benefits from ap 0 weapons
|
||||||
|
if(normalSave > 3 || ap > 0) {
|
||||||
|
finalSave++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Subtract AP (a higher save is worse)
|
||||||
|
finalSave += weapon.getAp();
|
||||||
|
finalSave = Math.min(finalSave, 6);
|
||||||
|
|
||||||
|
if(invulnerableSave.isPresent()) {
|
||||||
|
if(invulnerableSave.get().appliesTo(weapon)) {
|
||||||
|
var invulnerabilitySave = invulnerableSave.get().getRollValue();
|
||||||
|
if(invulnerabilitySave < finalSave) {
|
||||||
|
finalSave = invulnerabilitySave;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalSave;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
package ninja.thefirearchmage.games.fourtykcalculator;
|
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
public class FeelNoPainEffect {
|
public class FeelNoPainEffect {
|
||||||
private int rollValue;
|
private int rollValue;
|
||||||
private FeelNoPainType type;
|
private FeelNoPainType type;
|
||||||
|
|
|
@ -15,4 +15,14 @@ public class InvulnerableSave {
|
||||||
this.rollValue = rollValue;
|
this.rollValue = rollValue;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether this invulnerable save applies to the given weapon attack
|
||||||
|
*/
|
||||||
|
public boolean appliesTo(Weapon weapon) {
|
||||||
|
return switch(weapon.getRangeType()) {
|
||||||
|
case MELEE -> type == InvulnerableType.MELEE || type == InvulnerableType.ALL;
|
||||||
|
case RANGED -> type == InvulnerableType.RANGED || type == InvulnerableType.ALL;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
@AllArgsConstructor @NoArgsConstructor
|
||||||
|
public class SaveResolution {
|
||||||
|
private double preventedWounds;
|
||||||
|
private double preventedNormalWounds;
|
||||||
|
private double preventedNormalWoundsFromSaves;
|
||||||
|
private double preventedNormalWoundsFromSavesRerolls;
|
||||||
|
private double preventedNormalWoundsFromSavesWithoutRerolls;
|
||||||
|
private double preventedNormalWoundsFromFNP;
|
||||||
|
private double preventedMortalWoundsFromFNP;
|
||||||
|
private double preventedWoundsFromFNP;
|
||||||
|
}
|
|
@ -12,7 +12,9 @@ public enum WeaponStat {
|
||||||
WOUNDS_COUNT_WITHOUT_REROLL(Double.class),
|
WOUNDS_COUNT_WITHOUT_REROLL(Double.class),
|
||||||
WOUNDS_COUNT_FROM_REROLL(Double.class),
|
WOUNDS_COUNT_FROM_REROLL(Double.class),
|
||||||
MORTAL_WOUNDS_COUNT(Double.class),
|
MORTAL_WOUNDS_COUNT(Double.class),
|
||||||
TOTAL_DAMAGE(Double.class),
|
DAMAGE_NORMAL(Double.class),
|
||||||
|
DAMAGE_MORTAL(Double.class),
|
||||||
|
DAMAGE_TOTAL(Double.class),
|
||||||
LETHAL_HITS_COUNT(Double.class),
|
LETHAL_HITS_COUNT(Double.class),
|
||||||
NON_LETHAL_HITS_COUNT(Double.class),
|
NON_LETHAL_HITS_COUNT(Double.class),
|
||||||
SUSTAINED_HITS_COUNT(Double.class);
|
SUSTAINED_HITS_COUNT(Double.class);
|
||||||
|
|
|
@ -6,7 +6,7 @@ eradicator_melta_rifle:
|
||||||
dice_type: 6
|
dice_type: 6
|
||||||
number_of_dices: 1
|
number_of_dices: 1
|
||||||
melta: 2
|
melta: 2
|
||||||
ap: -4
|
ap: 4
|
||||||
toHit: 3
|
toHit: 3
|
||||||
strength: 9
|
strength: 9
|
||||||
isHeavy: true
|
isHeavy: true
|
||||||
|
@ -18,7 +18,7 @@ eradicator_multi_melta:
|
||||||
attacks: 2
|
attacks: 2
|
||||||
range_type: RANGED
|
range_type: RANGED
|
||||||
melta: 2
|
melta: 2
|
||||||
ap: -4
|
ap: 4
|
||||||
toHit: 4
|
toHit: 4
|
||||||
strength: 9
|
strength: 9
|
||||||
isHeavy: true
|
isHeavy: true
|
||||||
|
@ -26,7 +26,7 @@ intercessor_bolt_rifle:
|
||||||
name: Bolt Rifle (Intercessor)
|
name: Bolt Rifle (Intercessor)
|
||||||
attacks: 2
|
attacks: 2
|
||||||
damage: 1
|
damage: 1
|
||||||
ap: -1
|
ap: 1
|
||||||
toHit: 3
|
toHit: 3
|
||||||
strength: 4
|
strength: 4
|
||||||
isHeavy: true
|
isHeavy: true
|
Loading…
Add table
Reference in a new issue