Skip to content

Reactions

Reactions work — opportunity attacks, Shield, Counterspell — but they work differently from how a table plays them, and the difference shapes any integration. Read this before wiring a host.

The constraint: reactions are pre-armed

The engine never pauses mid-resolution to ask whether a creature wants to react. There is no callback, no "do you want to cast Shield?" prompt, and no mid-turn round-trip to the host.

This is deliberate. submit_player_intent and advance_monster_turn are the only two ways into the engine, and each resolves to completion before it returns. A reaction is therefore armed in advance and fires automatically when its trigger occurs.

# On the wizard's own turn, arm Shield against being hit.
await submit_player_intent(
    handle,
    actor_id="char:wizard",
    intent=PlayerIntent(
        intent_type="ready",
        spell_id="shield",
        reaction_trigger="hit_by_attack",
    ),
)

# Later, on the goblin's turn, the armed Shield fires by itself if the
# goblin attacks the wizard — no host involvement.
await advance_monster_turn(handle)

If you want a player to choose in the moment, your host has to ask them before their turn ends and arm accordingly. You cannot ask them mid-resolution.

Recognized triggers

PlayerIntent.reaction_trigger accepts a closed set:

Trigger Fires when
"hit_by_attack" The reactor is hit by an attack (Shield)
"cast_spell" Another creature casts a spell (Counterspell)
"targeted_by_magic_missile" The reactor is targeted by Magic Missile

Opportunity attacks are separate: they need no arming and fire automatically in both directions when a creature leaves an enemy's reach, unless the mover took the Disengage action.

This set is not data-driven

A new reaction spell cannot be added as data today — the trigger vocabulary is a Literal in the orchestrator, and Shield / Counterspell / Magic Missile carry named special cases. This is a known architectural gap, tracked in BACKLOG.md.

Reaction economy

Each creature has one reaction per round, refreshed at the start of its turn — the same budget an opportunity attack spends. Arming a reaction does not spend it; firing does.

Known limits

Two gaps are worth knowing about before you rely on these (both in BACKLOG.md):

  • Counterspell ignores its 60 ft range. An armed reactor counters a cast at any distance.
  • A readied spell fires without a slot. The reaction spends a spell slot when one is available but fires regardless, so a reactor with an empty pool gets a free cast.

Neither is reachable by accident — the host chooses to arm the reaction — but neither is gated by the engine.

For the full design, including how the queue drains relative to attack resolution and why Shield's AC bonus lands before the hit/miss comparison, see the reaction queue design note.