fix(rules): keep swift and free actions for staggered combatants

PF1e (CRB, Staggered): a staggered creature takes a single move OR standard action but can still take free, swift, and immediate actions. _staggered_filter returned only the first non-full-round action, so a disabled combatant with a queued swift bonus (deed/inspire_courage) lost its standard action. It now keeps every swift and free action plus the first move-or-standard, preserving the full_attack -> single attack downgrade.

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 20:59:19 +02:00
parent 529999da69
commit 32c09df264
3 changed files with 76 additions and 11 deletions
+29 -8
View File
@@ -6054,19 +6054,40 @@ def _staggered_filter(
*,
cast_time: CastTimeResolver = _default_cast_time,
) -> tuple[Action, ...]:
"""Staggered: one move OR one standard action per round, no full-round.
"""Staggered: one move OR one standard action, plus every swift and free.
A leading ``full_attack`` (already in range) becomes a single standard
``attack``; anything whose effective slot is full-round (charge,
withdraw, run, a full-round/one-round cast, spell_combat) is dropped.
PF1e (CRB, Conditions — Staggered): "A staggered creature may take a
single move action or standard action each round (but not both, nor can
he take full-round actions). A staggered creature can still take free,
swift, and immediate actions." So the filter walks the sequence in order
and keeps every swift and free action, the FIRST move-or-standard action,
and drops full-round actions plus any SECOND move-or-standard. A leading
``full_attack`` (already in range) is downgraded to a single standard
``attack`` that consumes the one move-or-standard slot; a further
full-round action (charge, withdraw, run, a full-round/one-round cast,
spell_combat) is dropped.
"""
kept: list[Action] = []
slot_used = False # the single move-OR-standard slot is spent
for action in actions:
if action.kind == "full_attack":
return (Action(kind="attack", target_id=action.target_id),)
if _effective_slot(action, cast_time) == "full_round":
# A full attack is a full-round action; staggered it collapses to
# a single standard attack (the kept move-or-standard action).
if not slot_used:
kept.append(Action(kind="attack", target_id=action.target_id))
slot_used = True
continue
return (action,)
return ()
slot = _effective_slot(action, cast_time)
if slot == "full_round":
continue
if slot in ("standard", "move"):
if slot_used:
continue
kept.append(action)
slot_used = True
else: # swift / free — always allowed while staggered (CRB)
kept.append(action)
return tuple(kept)
def _nauseated_filter(actions: tuple[Action, ...]) -> tuple[Action, ...]: