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
+41
View File
@@ -33,6 +33,7 @@ from pf1e_simulator.combat import (
_cast_time_slot,
_dropped_kinds,
_dropped_kinds_multiset,
_staggered_filter,
default_policy,
)
from pf1e_simulator.conditions import CONDITIONS_BY_NAME
@@ -309,6 +310,46 @@ def test_staggered_after_budget_keeps_single_legal_action() -> None:
assert ("charge", "downgraded:staggered") in reasons
# ── (d2) the staggered filter keeps swift + free, one move-OR-standard ─────────
def test_staggered_filter_keeps_swift_and_free_actions() -> None:
# PF1e (CRB, Staggered): a single move OR standard, but free/swift/immediate
# actions are still allowed. The policy's swift deed must not be dropped.
assert _staggered_filter((_foe("deed"), _foe("attack"))) == (
_foe("deed"),
_foe("attack"),
)
# A swift taken AFTER the standard is likewise kept.
assert _staggered_filter((_foe("attack"), _foe("deed"))) == (
_foe("attack"),
_foe("deed"),
)
# A free 5-foot step survives alongside the single standard action.
assert _staggered_filter((_foe("5foot_step"), _foe("attack"))) == (
_foe("5foot_step"),
_foe("attack"),
)
def test_staggered_filter_keeps_only_one_move_or_standard() -> None:
# move then standard → the move wins (first move-or-standard).
assert _staggered_filter((_foe("move"), _foe("attack"))) == (_foe("move"),)
# standard then move → the standard wins.
assert _staggered_filter((_foe("attack"), _foe("move"))) == (_foe("attack"),)
# two standards → only the first survives.
assert _staggered_filter((_foe("attack"), _foe("attack"))) == (_foe("attack"),)
def test_staggered_filter_drops_full_round_and_downgrades_full_attack() -> None:
# A leading full_attack collapses to a single standard attack (kept).
assert _staggered_filter((_foe("full_attack"),)) == (_foe("attack"),)
# A full-round charge is dropped; a following standard fills the slot.
assert _staggered_filter((_foe("charge"), _foe("attack"))) == (_foe("attack"),)
# full_attack after a spent slot is dropped (the slot is already used).
assert _staggered_filter((_foe("move"), _foe("full_attack"))) == (_foe("move"),)
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.