From 440c9dd9d5398b55993c691213035439d6e87c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Prieto?= Date: Mon, 16 Jun 2025 12:35:02 +0200 Subject: [PATCH] Finished attack funnel. Now implementing defence funnel --- .../fourtykcalculator/AttackScenario.java | 64 +++++++++++++++++-- .../games/fourtykcalculator/WeaponStat.java | 5 +- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java index 9b36272..d7fa5d8 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/AttackScenario.java @@ -6,8 +6,6 @@ import ninja.thefirearchmage.games.fourtykcalculator.utils.datastructures.Tuple2 import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.IntStream; public class AttackScenario { private static final int PROBABILITY_DENOMINATOR_D6 = 6; @@ -96,13 +94,26 @@ public class AttackScenario { Weapon weapon = weaponData._1; int amountOfWeapons = weaponData._2; - // Attack probabilities + // Attack funnel var passingHits = calculatePassingHits(weapon, amountOfWeapons); var passingWounds = calculatePassingWounds(weapon, amountOfWeapons, passingHits); + var normalWounds = passingWounds._1; + var mortalWounds = passingWounds._2; + + // Defence funnel + executeDefenceFunnel(weapon, normalWounds, mortalWounds); } } + + private void executeDefenceFunnel(Weapon weapon, double normalWounds, double mortalWounds) { + + } + + /** + * Returns a tuple of normal passing wounds and mortal wounds so that the caller can handle them differently. + */ private Tuple2 calculatePassingWounds(Weapon weapon, int amountOfWeapons, double passingHits) { double lethalHitsCount = calculateLethalHits(weapon, amountOfWeapons); var normalHits = passingHits - lethalHitsCount; @@ -111,8 +122,38 @@ public class AttackScenario { int woundRollGoal = calculateWoundRollGoal(weapon); int woundSuccessProbabilityNumerator = calculateProbabilityNumeratorFrom(woundRollGoal); - var passingWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6) + var passingNormalWounds = normalHits * ( (double) woundSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); + results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_WITHOUT_REROLL, passingNormalWounds+lethalHitsCount); + var woundsRerolls = prepareWoundRerolls(woundRollGoal); + if(woundsRerolls.isPresent()) { + var rerolls = woundsRerolls.get(); + var failedWounds = normalHits * ((double) (1 - woundSuccessProbabilityNumerator) / PROBABILITY_DENOMINATOR_D6); + var extraWoundsFromRerolls = failedWounds * rerolls.getProbabilityNumerator() / PROBABILITY_DENOMINATOR_D6; + results.setWeaponStat(weapon, WeaponStat.WOUNDS_COUNT_FROM_REROLL, extraWoundsFromRerolls); + passingNormalWounds += extraWoundsFromRerolls; + } + // Mortal wounds + var passingMortalWounds = 0d; + if(weapon.hasDevastatingWounds()) { + var criticalWoundRollGoal = weapon.getBestAntiFor(defensiveProfile) + .orElse(criticalWoundValue); + var criticalProbability = calculateProbabilityNumeratorFrom(criticalWoundRollGoal); + passingMortalWounds += (normalHits*criticalProbability/PROBABILITY_DENOMINATOR_D6); + if(fishCriticalWounds && hasRerollWoundsAny) { + passingMortalWounds += (normalHits * (1-criticalProbability)/PROBABILITY_DENOMINATOR_D6)*criticalProbability/PROBABILITY_DENOMINATOR_D6; + } + + results.setWeaponStat(weapon, WeaponStat.MORTAL_WOUNDS_COUNT, passingMortalWounds); + // passing wounds was calculated from the whole, but we must substract the critical wounds from the whole + // to be able to handle them separately, as mortal wounds replace normal wounds. + passingNormalWounds -= passingMortalWounds; + } + + passingNormalWounds += lethalHitsCount; + results.setWeaponStat(weapon, WeaponStat.WOUNDS_TOTAL_COUNT, passingNormalWounds+passingMortalWounds); + + return Tuple.of(passingNormalWounds, passingMortalWounds); } private double calculateLethalHits(Weapon weapon, int amountOfWeapons) { @@ -143,7 +184,9 @@ public class AttackScenario { var hitRerolls = prepareHitRerolls(hitRollGoal); if(hitRerolls.isPresent()) { var rerolls = hitRerolls.get(); - passingHits += rerolls.getProbabilityNumerator() * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); + var successfulHitFromRerolls = rerolls.getProbabilityNumerator() * ((double) hitSuccessProbabilityNumerator / PROBABILITY_DENOMINATOR_D6); + results.setWeaponStat(weapon, WeaponStat.HITS_COUNT_FROM_REROLL, successfulHitFromRerolls); + passingHits += successfulHitFromRerolls; } results.setWeaponStat(weapon, WeaponStat.HITS_TOTAL_COUNT, passingHits); @@ -161,6 +204,17 @@ public class AttackScenario { return Optional.empty(); } + private Optional prepareWoundRerolls(int woundRollGoal) { + if(hasRerollWoundsOnes) { + return Optional.of(Reroll.rerollOnOnes()); + } + if(hasRerollWoundsFailures || hasRerollWoundsAny) { + return Optional.of(Reroll.rerollFailures(woundRollGoal)); + } + + return Optional.empty(); + } + private double calculateWeaponAttacks(Weapon weapon, int amountOfWeapons) { double weaponAttacks = calculateBasicWeaponAttacks(weapon, amountOfWeapons); results.setWeaponStat(weapon, WeaponStat.POTENTIAL_HITS_COUNT, weaponAttacks); diff --git a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java index 124def3..b80f92c 100644 --- a/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java +++ b/src/main/java/ninja/thefirearchmage/games/fourtykcalculator/WeaponStat.java @@ -5,9 +5,12 @@ import lombok.Getter; @Getter public enum WeaponStat { HITS_COUNT_WITHOUT_REROLL(Double.class), + HITS_COUNT_FROM_REROLL(Double.class), HITS_TOTAL_COUNT(Double.class), POTENTIAL_HITS_COUNT(Double.class), - WOUNDS_COUNT(Double.class), + WOUNDS_TOTAL_COUNT(Double.class), + WOUNDS_COUNT_WITHOUT_REROLL(Double.class), + WOUNDS_COUNT_FROM_REROLL(Double.class), MORTAL_WOUNDS_COUNT(Double.class), TOTAL_DAMAGE(Double.class), LETHAL_HITS_COUNT(Double.class),