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:
@@ -319,7 +319,12 @@ Règles modélisées :
|
|||||||
ramenant à `hp ≥ 0` réinitialise la stabilisation pour toute chute future.
|
ramenant à `hp ≥ 0` réinitialise la stabilisation pour toute chute future.
|
||||||
La mort survient quand les pv négatifs atteignent le score de CON
|
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
|
(`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
|
- Mort simplifiée des monstres (option `--simple-monster-death`, défaut
|
||||||
désactivé) : quand l'option est présente dans la commande, un **monstre**
|
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
|
meurt dès `hp ≤ 0` — ni bande d'invalidité à 0 pv, ni bande d'agonie entre
|
||||||
|
|||||||
@@ -1816,7 +1816,9 @@ class CombatEngine:
|
|||||||
(d20 + Con modifier - negative hp total); a natural 20 auto-succeeds.
|
(d20 + Con modifier - negative hp total); a natural 20 auto-succeeds.
|
||||||
Failure bleeds 1 more hp; success stabilizes (no further loss while
|
Failure bleeds 1 more hp; success stabilizes (no further loss while
|
||||||
stable). New damage below the stabilizing hp resumes dying; healing to
|
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:
|
if state.dead:
|
||||||
return
|
return
|
||||||
@@ -1827,6 +1829,23 @@ class CombatEngine:
|
|||||||
if state.hp >= state.stabilized_hp:
|
if state.hp >= state.stabilized_hp:
|
||||||
return
|
return
|
||||||
state.stabilized_hp = None
|
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
|
con_mod = (state.combatant.abilities.con_score - 10) // 2
|
||||||
penalty = -state.hp
|
penalty = -state.hp
|
||||||
roll = self._rng.d20()
|
roll = self._rng.d20()
|
||||||
|
|||||||
@@ -170,6 +170,48 @@ def test_dying_bleeds_to_death_threshold() -> None:
|
|||||||
assert s.hp == -10
|
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:
|
def test_dying_tick_runs_on_initiative_slot_during_round() -> None:
|
||||||
# m1 is downed before the battle starts; both sides still have active
|
# 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.
|
# members so the battle continues and m1's slot processes the dying check.
|
||||||
|
|||||||
Reference in New Issue
Block a user