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() {
|
||||
var resolution = new AttackScenarioResolution();
|
||||
// Filter out weapons that cannot participate in this attack
|
||||
var validWeapons = weapons.stream().filter( w -> hasLineOfSight || w._1.hasIndirectFire())
|
||||
.toList();
|
||||
|
||||
// Attack step
|
||||
var totalDamage = 0d;
|
||||
var totalPreventedDamage = 0d;
|
||||
for(var weaponData : validWeapons) {
|
||||
Weapon weapon = weaponData._1;
|
||||
int amountOfWeapons = weaponData._2;
|
||||
|
@ -101,17 +102,18 @@ public class AttackScenario {
|
|||
var mortalWounds = passingWounds._2;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
private void executeDefenceFunnel(Weapon weapon, double normalWounds, double mortalWounds) {
|
||||
// Save normal wounds
|
||||
|
||||
//
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package ninja.thefirearchmage.games.fourtykcalculator;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple;
|
||||
import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2;
|
||||
|
||||
|
@ -14,9 +15,15 @@ import static java.lang.String.format;
|
|||
@Getter
|
||||
public class AttackScenarioResolution {
|
||||
private Map<String, Map<WeaponStat, Object>> weaponStats;
|
||||
@Setter
|
||||
private double totalInflictedDamage;
|
||||
@Setter
|
||||
private double totalPreventedDamage;
|
||||
|
||||
public AttackScenarioResolution() {
|
||||
this.weaponStats = new HashMap<>();
|
||||
totalInflictedDamage = 0;
|
||||
totalPreventedDamage = 0;
|
||||
}
|
||||
|
||||
public void setWeaponStat(Weapon weapon, WeaponStat statName, Object statValue) {
|
||||
|
|
|
@ -12,26 +12,163 @@ public class DefensiveProfile {
|
|||
private final int normalSave;
|
||||
private final int wounds;
|
||||
private final int bodies;
|
||||
private final Optional<InvulnerableSave> invulnerableSave;
|
||||
private final Optional<FeelNoPainEffect> feelNoPain;
|
||||
private final Set<UnitTypeKeyword> unitTypeKeywords;
|
||||
private Optional<InvulnerableSave> invulnerableSave;
|
||||
private Optional<FeelNoPainEffect> feelNoPain;
|
||||
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) {
|
||||
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.wounds = wounds;
|
||||
this.normalSave = normalSave;
|
||||
this.wounds = wounds;
|
||||
this.bodies = bodies;
|
||||
this.invulnerableSave = invulnerableSave;
|
||||
this.feelNoPain = feelNoPain;
|
||||
this.bodies = bodies;
|
||||
this.unitTypeKeywords = new HashSet<>();
|
||||
this.unitTypeKeywords = unitTypeKeywords;
|
||||
this.isInCover = isInCover;
|
||||
this.hasRerollOneNormalSaves = hasRerollOneNormalSaves;
|
||||
this.hasRerollFailedNormalSaves = hasRerollFailedNormalSaves;
|
||||
this.hasRerollAnyNormalSaves = hasRerollAnyNormalSaves;
|
||||
this.isVisible = isVisible;
|
||||
}
|
||||
|
||||
public DefensiveProfile withUnitTypes(Set<UnitTypeKeyword> types) {
|
||||
this.unitTypeKeywords.addAll(types);
|
||||
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;
|
||||
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class FeelNoPainEffect {
|
||||
private int rollValue;
|
||||
private FeelNoPainType type;
|
||||
|
|
|
@ -15,4 +15,14 @@ public class InvulnerableSave {
|
|||
this.rollValue = rollValue;
|
||||
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_FROM_REROLL(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),
|
||||
NON_LETHAL_HITS_COUNT(Double.class),
|
||||
SUSTAINED_HITS_COUNT(Double.class);
|
||||
|
|
|
@ -6,7 +6,7 @@ eradicator_melta_rifle:
|
|||
dice_type: 6
|
||||
number_of_dices: 1
|
||||
melta: 2
|
||||
ap: -4
|
||||
ap: 4
|
||||
toHit: 3
|
||||
strength: 9
|
||||
isHeavy: true
|
||||
|
@ -18,7 +18,7 @@ eradicator_multi_melta:
|
|||
attacks: 2
|
||||
range_type: RANGED
|
||||
melta: 2
|
||||
ap: -4
|
||||
ap: 4
|
||||
toHit: 4
|
||||
strength: 9
|
||||
isHeavy: true
|
||||
|
@ -26,7 +26,7 @@ intercessor_bolt_rifle:
|
|||
name: Bolt Rifle (Intercessor)
|
||||
attacks: 2
|
||||
damage: 1
|
||||
ap: -1
|
||||
ap: 1
|
||||
toHit: 3
|
||||
strength: 4
|
||||
isHeavy: true
|
Loading…
Add table
Reference in a new issue