fix(rules): tick bleed and auto-fail stabilization for a dying creature

PF1e (CRB, Stable Characters and Recovery): characters taking continuous damage such as bleed automatically fail all Constitution checks made to stabilize, losing 1 hp per round in addition to the continuous damage. _process_dying never applied bleed (only _turn_gate, reached by active creatures did), so a bleeding dying creature lost 0 hp per round. It now ticks bleed_damage at the start of the dying turn and auto-fails the check (no d20 consumed), then evaluates death.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-11 21:06:10 +02:00
parent 32c09df264
commit 1b5cac234e
3 changed files with 68 additions and 2 deletions
+6 -1
View File
@@ -319,7 +319,12 @@ Règles modélisées :
ramenant à `hp ≥ 0` réinitialise la stabilisation pour toute chute future.
La mort survient quand les pv négatifs atteignent le score de CON
(`hp ≤ -CON`) — conformance P2 : la règle du −10 de D&D 3.5 n'existe pas
en PF1e et n'est plus modélisée.
en PF1e et n'est plus modélisée. Une créature mourante qui subit des dégâts
continus (saignement) perd ses dégâts de saignement au début de son tour,
puis **échoue automatiquement** son jet de stabilisation (aucun d20 jeté) et
perd donc 1 pv de plus par tour en plus des dégâts continus (CRB « Stable
Characters and Recovery ») — elle meurt ainsi plus vite qu'une créature
mourante non saignante.
- Mort simplifiée des monstres (option `--simple-monster-death`, défaut
désactivé) : quand l'option est présente dans la commande, un **monstre**
meurt dès `hp ≤ 0` — ni bande d'invalidité à 0 pv, ni bande d'agonie entre
+20 -1
View File
@@ -1816,7 +1816,9 @@ class CombatEngine:
(d20 + Con modifier - negative hp total); a natural 20 auto-succeeds.
Failure bleeds 1 more hp; success stabilizes (no further loss while
stable). New damage below the stabilizing hp resumes dying; healing to
hp >= 0 resets stabilization for any future downing.
hp >= 0 resets stabilization for any future downing. A creature taking
continuous damage (bleed) instead ticks the bleed and AUTOMATICALLY
FAILS the check (CRB, Stable Characters and Recovery).
"""
if state.dead:
return
@@ -1827,6 +1829,23 @@ class CombatEngine:
if state.hp >= state.stabilized_hp:
return
state.stabilized_hp = None
# PF1e (CRB, Stable Characters and Recovery): "Characters taking
# continuous damage, such as from an acid arrow or a bleed effect,
# automatically fail all Constitution checks made to stabilize. Such
# characters lose 1 hit point per round in addition to the continuous
# damage." This is the inactive creature's once-per-round slot (the
# active path already ticked bleed in _turn_gate), so the bleed damage
# is applied here and the d20 check is skipped (auto-fail).
if state.bleed_damage > 0:
self._apply_bleed_damage(state)
hp_before = state.hp
state.hp -= 1
self._log(
f"round {self._current_round} {state.combatant.id}: dying — continuous "
f"damage auto-fails the Constitution check, loses 1 hp "
f"(hp {hp_before} -> {state.hp})"
)
return
con_mod = (state.combatant.abilities.con_score - 10) // 2
penalty = -state.hp
roll = self._rng.d20()
+42
View File
@@ -170,6 +170,48 @@ def test_dying_bleeds_to_death_threshold() -> None:
assert s.hp == -10
def test_dying_with_bleed_ticks_damage_and_auto_fails_stabilization() -> None:
# PF1e (CRB, Stable Characters and Recovery): a creature taking continuous
# damage (bleed) automatically fails the stabilization check and loses 1 hp
# per round IN ADDITION to the bleed damage. No d20 is consumed (an empty
# queue would raise), and it dies faster than the 1-hp-per-round baseline.
eng, s = _solo(con_score=8) # death threshold -8
s.hp = -3
s.bleed_damage = 2
eng._current_round = 2
eng._rng = ScriptedRng([]) # no roll: the check is auto-failed
eng._process_dying(s)
# bleed 2 (hp -3 -> -5), then auto-fail loses 1 more (hp -5 -> -6).
assert s.hp == -6
assert s.stabilized_hp is None
lines = "\n".join(eng._transcript)
assert "bleed 2 (hp -3->-5)" in lines
assert "continuous damage auto-fails the Constitution check" in lines
assert "loses 1 hp (hp -5 -> -6)" in lines
def test_dying_with_bleed_reaches_death_faster_than_without() -> None:
# Same start; the bleeding creature hits -CON in fewer rounds than the
# non-bleeding one (which only loses 1 hp per failed check).
eng_b, bleed = _solo(con_score=8) # death threshold -8
bleed.hp = -3
bleed.bleed_damage = 2
eng_b._rng = ScriptedRng([]) # auto-fail, no roll
eng_b._process_dying(bleed) # -3 -2 bleed -1 = -6
assert bleed.hp == -6
assert not bleed.dead
eng_b._process_dying(bleed) # -6 -2 bleed -1 = -9 <= -8 -> dead
assert bleed.dead
eng_n, plain = _solo(con_score=8)
plain.hp = -3
eng_n._rng = ScriptedRng([1, 1]) # two failed CON checks: -3 -> -4 -> -5
eng_n._process_dying(plain)
eng_n._process_dying(plain)
assert plain.hp == -5 # still alive after two rounds
assert not plain.dead
def test_dying_tick_runs_on_initiative_slot_during_round() -> None:
# m1 is downed before the battle starts; both sides still have active
# members so the battle continues and m1's slot processes the dying check.