fix(rules): update initiative when a readied action fires

SRD Ready - Initiative Consequences of Readying: 'your initiative result becomes the count on which you took the readied action' and you 'act immediately ahead of the character whose action triggered your readied action'. _check_readied fired the payload off-turn but never touched the readier's initiative. It now calls _ready_shift_initiative, which rewrites the readier's _initiative_keys entry (trigger total + 0.5, keeping its mod and prior index) using the same tuple shape _execute_delay writes, so _build_round_order holds the new position for all following rounds. Deterministic: no RNG consumed, current round order untouched.

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-13 14:54:15 +02:00
parent 6702efc652
commit 7fb7ec55dd
3 changed files with 143 additions and 1 deletions
+30
View File
@@ -4242,6 +4242,7 @@ class CombatEngine:
f"round {self._current_round} {state.combatant.id}: "
f"ready fires ({trigger})"
)
self._ready_shift_initiative(state, event.actor)
target = next(
(s for s in self._states if s.combatant.id == payload.target_id),
None,
@@ -4252,6 +4253,35 @@ class CombatEngine:
return True
return False
def _ready_shift_initiative(
self, state: CombatantState, trigger_actor: CombatantState
) -> None:
"""Move the readier's initiative to the count it acted on (PF1e).
PF1e (SRD "Ready — Initiative Consequences of Readying"): "Your
initiative result becomes the count on which you took the readied
action" and you "act immediately ahead of the character whose action
triggered your readied action" — for the rest of the encounter. The
slot is written into ``_initiative_keys`` with the same tuple shape
``_execute_delay`` uses (a 0.5 step off the anchor total, keeping the
readier's initiative modifier and prior list index) so
``_build_round_order`` holds the new position for every following
round. The step is ADDED (not subtracted as for delay): a readied
action interrupts and acts immediately AHEAD of the trigger, whereas a
delay postpones to act after — the two intents are opposite.
Deterministic: no RNG is consumed and the current round's order
(already built) is untouched — only subsequent rounds reorder.
"""
key = self._initiative_keys.get(state.combatant.id)
prior_index = key[2] if key is not None else self._states.index(state)
trigger_key = self._initiative_keys.get(trigger_actor.combatant.id)
trigger_total = trigger_key[0] if trigger_key is not None else 0.0
self._initiative_keys[state.combatant.id] = (
trigger_total + _DELAY_INITIATIVE_STEP,
state.combatant.initiative_mod,
prior_index,
)
def _ready_trigger_matches(
self, state: CombatantState, event: ReactionEvent
) -> bool: