fix(rules): allow a staggered creature to withdraw as a standard action
SRD 'Full-Round Actions - Restricted Withdraw': 'If you are limited to taking only a standard action each round you can withdraw as a standard action. In this case, you may move up to your speed.' _staggered_filter now keeps withdraw as the one move-or-standard action (spending the slot) instead of dropping it, and _flee_path halves the budget to single speed for a staggered/disabled creature (new _is_staggered_limited helper). Filter-level and engine-level tests added. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -7,7 +7,7 @@ Part (b): Condition behavioral effects enforced via action economy / AC modifier
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pf1e_simulator.combat import CombatantState, CombatEngine
|
||||
from pf1e_simulator.combat import Action, CombatantState, CombatEngine
|
||||
from pf1e_simulator.conditions import NAUSEATED, PARALYZED, PRONE, STAGGERED
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.effects import ActiveCondition, StatModifier
|
||||
@@ -188,6 +188,42 @@ class TestStaggeredActionEconomy:
|
||||
result = engine._take_turn(h)
|
||||
assert result is False
|
||||
|
||||
def test_staggered_restricted_withdraw_moves_single_speed(self) -> None:
|
||||
# SRD "Full-Round Actions — Restricted Withdraw": "If you are limited
|
||||
# to taking only a standard action each round you can withdraw as a
|
||||
# standard action. In this case, you may move up to your speed" — 30
|
||||
# ft = 6 cells, not the 2x full-round budget.
|
||||
legend = {".": TerrainType(type="floor", move_cost=1)}
|
||||
grid = Grid.from_spec(
|
||||
MapSpec(name="big", terrain=tuple(["." * 20] * 20), legend=legend)
|
||||
)
|
||||
hero = _make_hero()
|
||||
foe = _make_foe()
|
||||
h = CombatantState(combatant=hero, side="players", pos=(10, 10), hp=hero.hp_max)
|
||||
f = CombatantState(combatant=foe, side="monsters", pos=(10, 9), hp=foe.hp_max)
|
||||
h.active_conditions.append(
|
||||
ActiveCondition(condition=STAGGERED, rounds_left=5, source="test")
|
||||
)
|
||||
engine = CombatEngine(
|
||||
ScriptedRng([10, 9]),
|
||||
grid,
|
||||
[h, f],
|
||||
round_cap=1,
|
||||
policy=lambda _e, s: (
|
||||
(Action(kind="withdraw", target_id="foe"),)
|
||||
if s.combatant.id == hero.id
|
||||
else (Action(kind="wait"),)
|
||||
),
|
||||
)
|
||||
engine.run()
|
||||
withdraw_lines = [
|
||||
line for line in engine._transcript if "withdraw" in line
|
||||
]
|
||||
assert len(withdraw_lines) == 1
|
||||
# 6 steps from (10,10) — exactly the single-speed budget.
|
||||
assert withdraw_lines[0].count("->") == 6
|
||||
assert h.pos != (10, 10)
|
||||
|
||||
|
||||
# ── Test 4: nauseated → single move action only ───────────────────────────────
|
||||
|
||||
|
||||
@@ -350,6 +350,18 @@ def test_staggered_filter_drops_full_round_and_downgrades_full_attack() -> None:
|
||||
assert _staggered_filter((_foe("move"), _foe("full_attack"))) == (_foe("move"),)
|
||||
|
||||
|
||||
def test_staggered_filter_keeps_withdraw_as_restricted_standard() -> None:
|
||||
# SRD "Full-Round Actions — Restricted Withdraw": a creature limited to a
|
||||
# standard action each round may withdraw as a standard action (moving up
|
||||
# to its speed) — the staggered filter keeps it and spends the slot.
|
||||
assert _staggered_filter((_foe("withdraw"),)) == (_foe("withdraw"),)
|
||||
# After the one move-or-standard slot is spent, withdraw is dropped.
|
||||
assert _staggered_filter((_foe("attack"), _foe("withdraw"))) == (_foe("attack"),)
|
||||
assert _staggered_filter((_foe("move"), _foe("withdraw"))) == (_foe("move"),)
|
||||
# A leading withdraw spends the slot; the following standard is dropped.
|
||||
assert _staggered_filter((_foe("withdraw"), _foe("attack"))) == (_foe("withdraw"),)
|
||||
|
||||
|
||||
def test_nauseated_after_budget_keeps_single_move() -> None:
|
||||
# (move, attack, move): the budget drops the second move; the nauseated
|
||||
# filter then keeps only the first move and drops the standard attack.
|
||||
|
||||
Reference in New Issue
Block a user