From 59caf6790990920323f8da966b75aefbc99eaf5f Mon Sep 17 00:00:00 2001 From: ctan Date: Wed, 16 Sep 2026 11:45:23 +0200 Subject: [PATCH] fix(rules): undead and constructs are destroyed at hp <= 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SRD Undead/Construct traits: 'immediately destroyed when reduced to 0 hit points or fewer' — undead and constructs have no Constitution score, so the PF1e hp <= -CON death rule does not apply to them. - combat.py: CombatantState.death_threshold returns 0 for creature_type undead/construct (in addition to the opt-in dies_at_zero flag) - tests: TestUndeadDeathThreshold pins undead dead at 0 and below, living keeps the disabled band at 0 and the dying band down to -CON - golden unchanged: tammara-vs-lich has the lich winning in 1 round without taking damage --- src/pf1e_simulator/combat.py | 6 +++++- tests/test_lich.py | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index dcc7cf7..49501d6 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -769,9 +769,13 @@ class CombatantState: @property def death_threshold(self) -> int: """PF1e (CRB): death when negative hp reaches the CON score (no -10 rule). - The optional simple monster-death rule raises the threshold to 0.""" + Undead and constructs have no Constitution score and are "immediately + destroyed when reduced to 0 hit points or fewer" (SRD Undead/Construct + traits). The optional simple monster-death rule raises the threshold to 0.""" if self.dies_at_zero: return 0 + if self.combatant.creature_type in ("undead", "construct"): + return 0 return -self.combatant.abilities.con_score diff --git a/tests/test_lich.py b/tests/test_lich.py index 77528e1..465c23e 100644 --- a/tests/test_lich.py +++ b/tests/test_lich.py @@ -34,6 +34,7 @@ def _make_lich() -> Combatant: name="Lich", level=11, size="Medium", + creature_type="undead", abilities=AbilityScores( str_score=12, dex_score=14, con_score=14, int_score=20, wis_score=14, cha_score=16, @@ -173,6 +174,42 @@ class TestFearAura: assert all(ac.condition.name != "shaken" for ac in foe_state.active_conditions) +# ── Undead death threshold ──────────────────────────────────────────────────── + + +class TestUndeadDeathThreshold: + """Given: an undead creature (no CON score, SRD Undead traits) + When: its hp reaches 0 or below + Then: it is immediately destroyed (dead), unlike a living creature + which keeps the disabled band at 0 and the dying band down to -CON.""" + + def test_undead_dead_at_zero(self) -> None: + lich = _make_lich() + state = _make_state(lich, "monsters", (1, 1)) + state.hp = 0 + assert state.dead is True + assert state.active is False + + def test_undead_dead_below_zero(self) -> None: + lich = _make_lich() + state = _make_state(lich, "monsters", (1, 1)) + state.hp = -1 + assert state.dead is True + + def test_living_not_dead_at_zero(self) -> None: + foe = _make_foe() + state = _make_state(foe, "players", (1, 1)) + state.hp = 0 + assert state.dead is False + assert state.active is True + + def test_living_not_dead_in_dying_band(self) -> None: + foe = _make_foe() + state = _make_state(foe, "players", (1, 1)) + state.hp = -1 # -1 > -CON (12) + assert state.dead is False + + # ── JSON integrity ────────────────────────────────────────────────────────────