fix(rules): exempt retrieve and repair weapon from the disabled hit-point cost

PF1e (CRB, Disabled): a disabled creature can take move actions without further injuring itself. retrieve_weapon and repair_weapon are budget-classified move actions (_ACTION_ECONOMY_MOVE_KINDS) but were absent from _DISABLED_MOVE_KINDS, so a creature at 0 hp paid 1 hp to draw or mend a weapon. Added both kinds to the exempt set.

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:22:03 +02:00
parent fa01ed74c5
commit 4f6b076a49
3 changed files with 68 additions and 3 deletions
+6 -2
View File
@@ -347,8 +347,12 @@ Règles modélisées :
d'action à round complet, mais les actions swift, libre et immédiate restent d'action à round complet, mais les actions swift, libre et immédiate restent
permises — CRB « Staggered »), se déplace à **moitié vitesse**, et achever une permises — CRB « Staggered »), se déplace à **moitié vitesse**, et achever une
action non-mouvement (standard ou pénible) lui coûte **1 pv** à la fin de action non-mouvement (standard ou pénible) lui coûte **1 pv** à la fin de
cette action (pv → −1, agonie). Simplification documentée : si l'action cette action (pv → −1, agonie). Les actions de mouvement — dont
elle-même soigne la créature au-dessus de 0 pv, ce coût n'est pas appliqué. `retrieve_weapon` (sortir/récupérer une arme) et `repair_weapon` (raccommoder
une arme brisée), classées mouvement — ne risquent pas de blessure
supplémentaire et coûtent 0 pv (CRB « Disabled »). Simplification documentée :
si l'action elle-même soigne la créature au-dessus de 0 pv, ce coût n'est pas
appliqué.
- `hp < 0` = inactive (mourante ou morte) ; `hp = 0` = active mais invalide. - `hp < 0` = inactive (mourante ou morte) ; `hp = 0` = active mais invalide.
- Initiative : triée sur (total, modificateur, ordre de liste), sans re-jet. - Initiative : triée sur (total, modificateur, ordre de liste), sans re-jet.
- Économie d'action : un tour normal donne une action standard + une action de - Économie d'action : un tour normal donne une action standard + une action de
+11 -1
View File
@@ -195,8 +195,18 @@ _HELPLESS_CONDITION_NAMES: frozenset[str] = frozenset(
# PF1e (CRB, Disabled): at exactly 0 hp a creature takes only a single move # PF1e (CRB, Disabled): at exactly 0 hp a creature takes only a single move
# or standard action (staggered, half speed); move-equivalent and free # or standard action (staggered, half speed); move-equivalent and free
# activities are not "strenuous" and cost no hp — everything else does. # activities are not "strenuous" and cost no hp — everything else does.
# retrieve_weapon / repair_weapon are move actions (CRB: drawing or mending a
# weapon is a move action), so they belong here too.
_DISABLED_MOVE_KINDS: frozenset[str] = frozenset( _DISABLED_MOVE_KINDS: frozenset[str] = frozenset(
{"move", "5foot_step", "stand_up", "drop_prone", "wait"} {
"move",
"5foot_step",
"stand_up",
"drop_prone",
"wait",
"retrieve_weapon",
"repair_weapon",
}
) )
# PF1e (CRB, Combat Maneuver): attempting a combat maneuver provokes an # PF1e (CRB, Combat Maneuver): attempting a combat maneuver provokes an
# attack of opportunity from the target unless the attacker holds the # attack of opportunity from the target unless the attacker holds the
+51
View File
@@ -338,6 +338,57 @@ def test_disabled_move_action_costs_no_hp() -> None:
assert not any("costs 1 hp" in ln for ln in eng._transcript) assert not any("costs 1 hp" in ln for ln in eng._transcript)
def _disabled_one_action_policy(action: Action) -> PolicyFn:
def policy(_engine: CombatEngine, state: CombatantState) -> tuple[Action, ...]:
if state.combatant.id == "hero":
return (action,)
return (Action(kind="wait"),)
return policy
def test_disabled_retrieve_weapon_costs_no_hp() -> None:
# PF1e (CRB, Disabled): "Taking move actions doesn't risk further injury."
# Drawing/retrieving a weapon is a move action, so it must not cost 1 hp.
hero = _combatant("hero")
foe = _combatant("foe")
eng = _engine(
[hero, foe], [(0, 0), (0, 7)], ["players", "monsters"], [],
policy=_disabled_one_action_policy(
Action(kind="retrieve_weapon", weapon_id="hero-sword")
),
)
s = eng._states[0]
s.hp = 0
s.weapon_stolen.add("hero-sword") # make the retrieve meaningful
eng._current_round = 1
eng._take_turn(s)
assert s.hp == 0
assert "hero-sword" not in s.weapon_stolen # the retrieve ran
assert not any("costs 1 hp" in ln for ln in eng._transcript)
def test_disabled_repair_weapon_costs_no_hp() -> None:
# Mending a broken weapon is likewise a move action: no hp cost while 0 hp.
hero = _combatant("hero")
foe = _combatant("foe")
eng = _engine(
[hero, foe], [(0, 0), (0, 7)], ["players", "monsters"], [],
policy=_disabled_one_action_policy(
Action(kind="repair_weapon", weapon_id="hero-sword")
),
)
s = eng._states[0]
s.hp = 0
s.weapon_damage["hero-sword"] = 10 # a broken weapon, repairable
s.weapon_broken.add("hero-sword")
eng._current_round = 1
eng._take_turn(s)
assert s.hp == 0
assert "hero-sword" not in s.weapon_broken # the repair ran
assert not any("costs 1 hp" in ln for ln in eng._transcript)
# ── B12: death threshold is -CON, no -10 floor ─────────────────────────────── # ── B12: death threshold is -CON, no -10 floor ───────────────────────────────