From 2e2873f33115a361e74e08efec94986c2b01b363 Mon Sep 17 00:00:00 2001 From: ctan Date: Sat, 19 Sep 2026 16:52:04 +0200 Subject: [PATCH] =?UTF-8?q?fix(stats):=20channel-energy=20heal=20is=20no?= =?UTF-8?q?=20longer=20counted=20as=20damage=5Fdealt=20(attrition=20tracks?= =?UTF-8?q?=20damage=20only,=20README=20'd=C3=A9g=C3=A2ts=20inflig=C3=A9s/?= =?UTF-8?q?subis';=20consistent=20with=20spell=20HealEffect=20which=20is?= =?UTF-8?q?=20counted=20nowhere)=20-=20harm=20mode=20still=20records=20dam?= =?UTF-8?q?age=5Fdealt/damage=5Ftaken,=20pinned=20by=20new=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pf1e_simulator/combat.py | 4 +++- tests/test_class_abilities.py | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index bc19c69..4f8a513 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -5652,7 +5652,9 @@ class CombatEngine: if affected_state.hp < 0: affected_state.stabilized_hp = affected_state.hp actual = affected_state.hp - old_hp - self._stats[state.combatant.id].damage_dealt += actual + # Healing is NOT damage dealt: the attrition counters track damage only + # (README "degats infligés/subis"), consistent with spell healing + # (HealEffect), which is counted nowhere. self._stop_bleeding(affected_state) self._log( f"round {self._current_round} {state.combatant.id}: " diff --git a/tests/test_class_abilities.py b/tests/test_class_abilities.py index c92966e..2174984 100644 --- a/tests/test_class_abilities.py +++ b/tests/test_class_abilities.py @@ -202,6 +202,43 @@ def test_oni_channel_heal_heals_living_enemies_in_range() -> None: assert enemy[0].hp == min(2 + (1 + 5), 6) # healed, capped at hp_max +def test_oni_channel_heal_is_not_counted_as_damage_dealt() -> None: + """Attrition counts damage only (README "dégâts infligés/subis"): channel + healing restores hp but must leave damage_dealt at 0, while the harm mode + of the same feature still records its damage.""" + oni = _combatant("oni", resources={"channel_energy": 6}, hp_max=30) + goblin = _combatant("goblin", hp_max=6) + engine, me, enemy = _engine(oni, [goblin], rng_queue=[5]) + enemy[0].hp = 2 + + engine._execute( + me, + Action(kind="channel_energy", target_id=me.combatant.id, channel_mode="heal"), + ) + + assert enemy[0].hp == 6 # the heal itself still lands + assert engine._stats["oni"].damage_dealt == 0 + assert engine._stats["oni"].damage_taken == 0 + assert engine._stats["goblin"].damage_taken == 0 + + +def test_oni_channel_harm_is_counted_as_damage_dealt() -> None: + """The damage mode of the same feature still records damage_dealt and + damage_taken (a failed Will save takes full 2d6).""" + oni = _combatant("oni", resources={"channel_energy": 6}, hp_max=30) + lich = _combatant("lich", hp_max=100, creature_type="undead") + engine, me, enemy = _engine(oni, [lich], rng_queue=[11, 5]) + + engine._execute( + me, Action(kind="channel_energy", target_id=lich.id, channel_mode="harm") + ) + + dealt = 1 + 11 # 2d6 scripted, save failed + assert enemy[0].hp == 100 - dealt + assert engine._stats["oni"].damage_dealt == dealt + assert engine._stats["lich"].damage_taken == dealt + + def test_oni_emits_channel_harm_only_for_grouped_undead() -> None: """Channel harm is emitted only when at least 2 undead enemies are grouped at 30ft — living enemies are never harmed by positive energy."""