diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 4f8a513..46317ee 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -2173,6 +2173,7 @@ class CombatEngine: return old_hp = state.hp state.hp -= state.bleed_damage + self._stats[state.combatant.id].damage_taken += state.bleed_damage self._log( f"round {self._current_round} {state.combatant.id}: " f"bleed {state.bleed_damage} (hp {old_hp}->{state.hp})" @@ -2186,11 +2187,13 @@ class CombatEngine: if not state.recurring_damage: return remaining: list[RecurringDamage] = [] + live = self._stats[state.combatant.id] for entry in state.recurring_damage: raw = entry.formula.roll(self._rng) dmg = self._apply_energy_resistance(raw, entry.types, state) old_hp = state.hp state.hp -= dmg + live.damage_taken += dmg self._log( f"round {self._current_round} {state.combatant.id}: " f"{entry.source} {dmg} (hp {old_hp}->{state.hp})" @@ -6766,7 +6769,7 @@ class CombatEngine: return self._apply_heal_effect( effect, target, caster_level=caster.combatant.level, empower=empower, maximize=maximize, saved=saved, - healer=caster, + healer=caster, caster_live=caster_live, target_live=target_live, ) if isinstance(effect, PenaltyEffect): return self._apply_penalty_effect( @@ -7032,6 +7035,8 @@ class CombatEngine: maximize: bool, saved: bool, healer: CombatantState | None = None, + caster_live: _LiveStats | None = None, + target_live: _LiveStats | None = None, ) -> str: formula = effect.effective_formula(caster_level) heal = formula.roll(self._rng) @@ -7050,6 +7055,10 @@ class CombatEngine: # healing, not this damage. dmg = heal // 2 if saved else heal target.hp -= dmg + if caster_live is not None: + caster_live.damage_dealt += dmg + if target_live is not None: + target_live.damage_taken += dmg return f"{dmg} damage (undead)" if healer is not None: heal = self._bitter_reduced(healer, target, heal) diff --git a/tests/test_bleeding_confused.py b/tests/test_bleeding_confused.py index 3f2558c..8eafbe3 100644 --- a/tests/test_bleeding_confused.py +++ b/tests/test_bleeding_confused.py @@ -89,6 +89,24 @@ def test_bleed_to_death_ends_battle() -> None: assert any("a: bleed 5 (hp -8->-13)" in line for line in eng._transcript) +def test_bleed_tick_records_damage_taken() -> None: + """Attrition (README "dégâts subis"): bleed is damage - the tick lands in + damage_taken with no attributed dealer (no attack source).""" + eng = _engine( + [_combatant("a"), _combatant("b")], + [(0, 0), (0, 1)], + ["players", "monsters"], + [], + ) + a, _b = eng._states + a.bleed_damage = 2 + eng._apply_bleed_damage(a) + assert a.hp == 98 + assert eng._stats["a"].damage_taken == 2 + assert eng._stats["a"].damage_dealt == 0 + assert eng._stats["b"].damage_taken == 0 + + def test_heal_spell_clears_bleed() -> None: eng = _engine( [_combatant("a"), _combatant("b")], diff --git a/tests/test_cure_undead.py b/tests/test_cure_undead.py index 5fa2f70..28f3086 100644 --- a/tests/test_cure_undead.py +++ b/tests/test_cure_undead.py @@ -78,3 +78,27 @@ class TestCureDamagesUndead: engine._cast_spell(c, t, "Cure Light Wounds") assert t.hp == 18 assert any("+13 HP" in line for line in engine._transcript) + + +class TestCureUndeadAttrition: + """Given: a cure spell delivered to an undead + When: the negative-energy damage lands + Then: it counts as damage in attrition (dealer + taker), while healing a + living target records no damage either way.""" + + def test_undead_damage_recorded_for_both_sides(self) -> None: + engine, c, t = _make_engine( + _caster(), _undead(), [1, 8], target_pos=(1, 0), target_side="players" + ) + engine._cast_spell(c, t, "Cure Light Wounds") + assert engine._stats["skeleton"].damage_taken == 13 + assert engine._stats["caster"].damage_dealt == 13 + + def test_living_heal_records_no_damage(self) -> None: + engine, c, t = _make_engine( + _caster(), _make_target(), [1, 8], target_pos=(1, 0), target_side="players" + ) + t.hp = 5 + engine._cast_spell(c, t, "Cure Light Wounds") + assert engine._stats["caster"].damage_dealt == 0 + assert engine._stats["target"].damage_taken == 0 diff --git a/tests/test_recurring_damage.py b/tests/test_recurring_damage.py index 31592ab..f389999 100644 --- a/tests/test_recurring_damage.py +++ b/tests/test_recurring_damage.py @@ -244,3 +244,23 @@ class TestRecurringSnapshot: assert entry["recurring_damage"] == [ {"formula": "2d4", "types": ("acid",), "rounds_left": 2} ] + + +class TestRecurringTicksRecordDamageTaken: + """Given: a target burning from Acid Arrow + When: each recurring tick lands + Then: attrition's damage_taken grows by the tick (README "dégâts subis"); + the initial spell hit was already counted at cast time.""" + + def test_each_tick_counts_as_damage_taken(self) -> None: + caster = _caster(level=3) + victim = _victim(hp_max=200) + # attack (15) + initial 2d4 (3+3) + tick1 2d4 (2+2) + tick2 2d4 (4+4) + engine, c, v = _engine(caster, victim, [15, 3, 3, 2, 2, 4, 4]) + engine._cast_spell(c, v, "Acid Arrow") + assert engine._stats["victim"].damage_taken == 6 + assert engine._stats["caster"].damage_dealt == 6 + engine._apply_recurring_damage(v) + assert engine._stats["victim"].damage_taken == 10 + engine._apply_recurring_damage(v) + assert engine._stats["victim"].damage_taken == 18