fix(rules): stabilize is a StabilizeEffect that auto-stabilizes a dying creature

This commit is contained in:
2026-09-20 10:48:08 +02:00
parent a8a65b18cd
commit f7886135bd
5 changed files with 338 additions and 16 deletions
+49 -14
View File
@@ -150,6 +150,7 @@ from pf1e_simulator.spells import (
RecurringDamageSpec,
SaveType,
SpellEffect,
StabilizeEffect,
SummonEffect,
TempHpEffect,
close_range_ft,
@@ -469,10 +470,22 @@ def _damage_effect_summary(effect: DamageEffect | PenaltyEffect, tags: list[str]
return " ".join(parts)
def _simple_effect_summary(
effect: OneShotEffect | TempHpEffect | StabilizeEffect,
) -> str:
if isinstance(effect, OneShotEffect):
return (
f"one-shot +{effect.value} {effect.bonus_type} on {effect.track}"
)
if isinstance(effect, TempHpEffect):
return f"temp hp +{effect.amount} ({effect.duration_rounds}r)"
return "stabilize"
def _non_damage_effect_summary(
effect: (
ConditionEffect | HealEffect | BuffEffect | AreaEffect | SummonEffect
| OneShotEffect | TempHpEffect
| OneShotEffect | TempHpEffect | StabilizeEffect
)
) -> str:
if isinstance(effect, ConditionEffect):
@@ -483,12 +496,8 @@ def _non_damage_effect_summary(
return f"area {effect.radius_ft}ft"
if isinstance(effect, SummonEffect):
return f"summon {effect.creature}"
if isinstance(effect, (OneShotEffect, TempHpEffect)):
return (
f"one-shot +{effect.value} {effect.bonus_type} on {effect.track}"
if isinstance(effect, OneShotEffect)
else f"temp hp +{effect.amount} ({effect.duration_rounds}r)"
)
if isinstance(effect, (OneShotEffect, TempHpEffect, StabilizeEffect)):
return _simple_effect_summary(effect)
return f"buff {effect.duration_rounds}r"
@@ -6904,7 +6913,12 @@ class CombatEngine:
caster, spell, spell_name, center_pos, has_area=has_area
)
return not point_ok
if not target.active:
if not target.active and not any(
isinstance(e, StabilizeEffect) for e in spell.effects
):
# Stabilize (SRD spells/stabilize.md) targets a living creature
# at -1 or fewer hit points — an inactive state every other spell
# cannot reach, so it alone bypasses the active-target gate.
return True
dist_ft = self.creature_distance(caster, target) * _SQUARE_FT
if dist_ft > spell_range_ft(spell, caster.combatant.level):
@@ -7155,7 +7169,7 @@ class CombatEngine:
self,
effect: (
DamageEffect | ConditionEffect | HealEffect | BuffEffect
| PenaltyEffect | OneShotEffect | TempHpEffect
| PenaltyEffect | OneShotEffect | TempHpEffect | StabilizeEffect
),
target: CombatantState,
caster_live: _LiveStats,
@@ -7193,8 +7207,8 @@ class CombatEngine:
effect, target, caster=caster, saved=saved,
empower=empower, maximize=maximize, spell_name=spell_name,
)
if isinstance(effect, (OneShotEffect, TempHpEffect)):
return self._apply_grant_effect(
if isinstance(effect, (OneShotEffect, TempHpEffect, StabilizeEffect)):
return self._apply_simple_effect(
effect, target, saved=saved, spell_name=spell_name,
)
return self._apply_buff_effect(
@@ -7202,9 +7216,9 @@ class CombatEngine:
caster_level=caster.combatant.level,
)
def _apply_grant_effect(
def _apply_simple_effect(
self,
effect: OneShotEffect | TempHpEffect,
effect: OneShotEffect | TempHpEffect | StabilizeEffect,
target: CombatantState,
*,
saved: bool,
@@ -7214,7 +7228,28 @@ class CombatEngine:
return self._apply_one_shot_effect(
effect, target, saved=saved, spell_name=spell_name,
)
return self._apply_temp_hp_effect(effect, target, saved=saved)
if isinstance(effect, TempHpEffect):
return self._apply_temp_hp_effect(effect, target, saved=saved)
return self._apply_stabilize_effect(target, saved=saved)
def _apply_stabilize_effect(
self, target: CombatantState, *, saved: bool
) -> str:
"""Stabilize a dying creature (SRD spells/stabilize.md).
The spell targets a living creature at -1 or fewer hit points and
automatically stabilizes it: stabilized_hp is set to the target's
current hp, the same marker a successful Constitution check writes in
_process_dying, so the per-round hp loss stops. New damage below that
hp clears the marker and the creature resumes dying normally; a
creature at hp >= 0 or dead is unaffected.
"""
if saved:
return "stabilize negated"
if target.dead or target.hp >= 0:
return "stabilize no effect"
target.stabilized_hp = target.hp
return f"stabilized (hp {target.hp})"
def _apply_one_shot_effect(
self, effect: OneShotEffect, target: CombatantState, *, saved: bool, spell_name: str