fix(rules): consume the current turn's swift for an in-turn immediate
PF1e (SRD Immediate Actions): "Using an immediate action on your turn is the same as using a swift action and counts as your swift action for that turn." The engine now tracks the acting combatant (_turn_actor); an immediate fired during its OWN turn sets swift_used_this_turn instead of immediate_used, so swift actions later in the turn's sequence are dropped (downgraded:immediate_swift) and no second immediate fires that round, while the next turn's swift stays free. Off-turn immediates keep consuming the next swift. The off-turn consumption test was retargeted to a true off-turn event and an in-turn test added; README updated (Actions immediates + budget classification). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -190,7 +190,7 @@ def test_immediate_attack_reaction_executes() -> None:
|
||||
|
||||
|
||||
def test_immediate_consumes_next_swift() -> None:
|
||||
"""An immediate action consumes the swift action of the next turn."""
|
||||
"""An OFF-TURN immediate action consumes the swift action of the next turn."""
|
||||
hero = make_combatant("hero", initiative_mod=5)
|
||||
goblin = make_combatant("goblin")
|
||||
hero_state = make_state(hero, "players", (1, 1))
|
||||
@@ -218,7 +218,12 @@ def test_immediate_consumes_next_swift() -> None:
|
||||
_engine: CombatEngine, state: CombatantState, event: ReactionEvent
|
||||
) -> Action | None:
|
||||
nonlocal reacted
|
||||
if state.combatant.id == "hero" and event.kind == "attack_resolved" and not reacted:
|
||||
if (
|
||||
state.combatant.id == "hero"
|
||||
and event.kind == "attack_resolved"
|
||||
and event.actor.combatant.id == "goblin"
|
||||
and not reacted
|
||||
):
|
||||
reacted = True
|
||||
return Action(kind="immediate", target_id="goblin")
|
||||
return None
|
||||
@@ -239,6 +244,59 @@ def test_immediate_consumes_next_swift() -> None:
|
||||
assert deed_rounds == [1, 3]
|
||||
|
||||
|
||||
def test_in_turn_immediate_consumes_current_swift() -> None:
|
||||
"""SRD Immediate Actions: "Using an immediate action on your turn is the
|
||||
same as using a swift action and counts as your swift action for that
|
||||
turn." The hero attacks; its own attack_resolved event triggers its
|
||||
in-turn reaction, spending the current swift — so the deed that follows
|
||||
in the same sequence is dropped, and the next turn's swift stays free."""
|
||||
hero = make_combatant("hero", initiative_mod=5)
|
||||
goblin = make_combatant("goblin")
|
||||
hero_state = make_state(hero, "players", (1, 1))
|
||||
hero_state.resources_left = {"grit": 2}
|
||||
states = [hero_state, make_state(goblin, "monsters", (1, 2))]
|
||||
reacted = False
|
||||
|
||||
def deed_policy(
|
||||
_engine: CombatEngine, state: CombatantState
|
||||
) -> tuple[Action, ...]:
|
||||
if state.combatant.id == "hero":
|
||||
return (
|
||||
Action(kind="attack", target_id="goblin"),
|
||||
Action(kind="deed", target_id="goblin", deed_name="up_close_and_deadly"),
|
||||
)
|
||||
return (Action(kind="wait"),)
|
||||
|
||||
def reaction_policy(
|
||||
_engine: CombatEngine, state: CombatantState, event: ReactionEvent
|
||||
) -> Action | None:
|
||||
nonlocal reacted
|
||||
if (
|
||||
state.combatant.id == "hero"
|
||||
and event.kind == "attack_resolved"
|
||||
and event.actor.combatant.id == "hero"
|
||||
and not reacted
|
||||
):
|
||||
reacted = True
|
||||
return Action(kind="immediate", target_id="goblin")
|
||||
return None
|
||||
|
||||
engine = CombatEngine(
|
||||
SeededRng(4),
|
||||
make_grid(),
|
||||
states,
|
||||
policy=deed_policy,
|
||||
reaction_policy=reaction_policy,
|
||||
)
|
||||
engine._roll_initiative()
|
||||
engine._take_turn(hero_state)
|
||||
# The in-turn immediate spent the current swift: the deed is dropped...
|
||||
reasons = [(r.kind, r.reason) for r in hero_state.last_action_results]
|
||||
assert ("deed", "downgraded:immediate_swift") in reasons
|
||||
# ...and the NEXT turn's swift is NOT consumed (unlike the off-turn rule).
|
||||
assert hero_state.immediate_used is False
|
||||
|
||||
|
||||
def test_immediate_in_turn_sequence_executes() -> None:
|
||||
"""An immediate action in the turn sequence executes like an attack."""
|
||||
hero = make_combatant("hero", initiative_mod=5)
|
||||
|
||||
Reference in New Issue
Block a user