The interruption window opens for actual casting

OPPORTUNITY FIRE and INTERRUPT bought a moment the client would not
honor: every selection and targeting gate asked "is it your turn?",
and mid-interruption it is not — so the window's holder could click
attack cards into silence while the engine sat ready to accept the
cast. A window is now yourMoment: hand selection, player/creature/
cell/edge targeting, and the dimming aid all open exactly as on your
own turn, and the slip says what to do with it ("pick an attack card
and a target") instead of the cryptic "cast now".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-17 13:45:52 -04:00
co-authored by Claude Fable 5
parent 94ec714e35
commit 44c14f5ce1
+12 -7
View File
@@ -144,6 +144,11 @@
view !== null && view.phase === "playing" && view.activePlayerId === view.you && !view.stack,
);
const youMustRespond = $derived(view?.stack != null && view.stack.waitingOn === view.you);
/** An interruption window (INTERRUPT / OPPORTUNITY FIRE) is your moment:
* casting and targeting open up as if the turn were yours. */
const yourMoment = $derived(
isYourTurn || (view != null && view.outOfTurnWindow?.playerId === view.you),
);
const attackNoticeKey = $derived(
youMustRespond && view?.stack
? `${view.stack.attackerId}:${view.stack.attackCard?.instanceId ?? view.stack.creatureId ?? "punch"}:${view.stack.counters.length}`
@@ -379,7 +384,7 @@
return;
}
if (actionsSpent) return; // picking up an object ended the turn's actions
if (!isYourTurn) {
if (!isYourTurn && !yourMoment) {
// Live interruption: Interrupt / Opportunity Fire may be played during
// another player's turn (when no attack is pending).
if (!view.stack && (card.cardId === "interrupt" || card.cardId === "opportunity-fire")) {
@@ -497,7 +502,7 @@
clearSelection();
return;
}
if (!isYourTurn) { peekAt(cell); return; }
if (!yourMoment) { peekAt(cell); return; }
if (pendingCellFor && selectedCard) {
// Stage 2 of teleport-opponent: destination chosen.
dispatch({
@@ -727,7 +732,7 @@
if (!view) return;
const creature = view.creatures.find((c) => c.id === creatureId);
if (!creature) return;
if (!isYourTurn) { peekAt(creature.position); return; }
if (!yourMoment) { peekAt(creature.position); return; }
if (selectedCard && CELL_CARDS.has(selectedCard.cardId)) {
// Cell-target spells aimed at a creature (DISPEL CREATION on a wraith)
// land on its square — the engine finds the creature there.
@@ -762,7 +767,7 @@
}
function clickPlayer(playerId: string) {
if (!view || !isYourTurn) return;
if (!view || !yourMoment) return;
if (selectedCreature) {
dispatch({ type: "creatureAttack", creatureId: selectedCreature, targetId: playerId });
selectedCreature = null;
@@ -824,7 +829,7 @@
);
/** Squares a selected L.O.S./ADJACENT card can reach; null = no dimming. */
const litCells = $derived.by(() => {
if (!view || !selectedDef || !isYourTurn) return null;
if (!view || !selectedDef || !yourMoment) return null;
if (ambushVia || ambushSpell || ambushTrigger) return null; // ambushes aim at the future
if (!me) return null;
if (CELL_CARDS.has(selectedCard!.cardId)) {
@@ -1308,7 +1313,7 @@
<section class="board-zone">
<Board
{view}
edgeSelectMode={edgeSelectMode && isYourTurn}
edgeSelectMode={edgeSelectMode && yourMoment}
selectedCreatureId={selectedCreature}
onCellClick={clickCell}
onEdgeClick={clickEdge}
@@ -1347,7 +1352,7 @@
{:else if view.stack}
<div class="slip">Waiting on {view.stack.waitingOn}…</div>
{:else if view.outOfTurnWindow?.playerId === view.you}
<div class="slip urgent">Your interruption — cast now, or <button class="stamp tiny" onclick={pass}>waste it</button></div>
<div class="slip urgent">Your interruption — pick an attack card and a target, or <button class="stamp tiny" onclick={pass}>waste it</button></div>
{:else if isYourTurn}
<div class="slip yours">
Your turn — round {view.turn.round}.