From 45803a26647e121e89302b17f2e90825c6f64d04 Mon Sep 17 00:00:00 2001 From: Eric Wagoner Date: Sun, 16 Aug 2026 13:07:22 -0400 Subject: [PATCH] Live interrupts announce themselves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interrupt and Opportunity Fire were fully playable out of turn — tap the card while another wizard acts — but nothing said so, and cards are otherwise inert out of turn, so playtesters never tried. Now, whenever the moment is live (someone else's turn, no attack or chaos pending, past round one, not hotseat), those cards wear a pulsing golden glow in your hand and the rail says to tap one. The glow respects prefers-reduced-motion. Co-Authored-By: Claude Fable 5 --- packages/web/src/App.svelte | 16 ++++++++++++++++ packages/web/src/Card.svelte | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 62a019d..975798f 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -89,6 +89,16 @@ const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you); const youMustShield = $derived(view?.chaosPending?.queue[0] === view?.you && view != null); const holdingWard = $derived(view?.yourHand.some((c) => c.cardId === "ward") ?? false); + /** A live moment to interrupt: another wizard acts, no attack is pending. */ + const canInterruptNow = $derived( + view != null && !isYourTurn && !view.stack && !view.chaosPending && + !view.outOfTurnWindow && view.phase === "playing" && view.turn.round > 1 && !local.active, + ); + const interruptCards = $derived( + canInterruptNow + ? view!.yourHand.filter((c) => c.cardId === "interrupt" || c.cardId === "opportunity-fire") + : [], + ); const selectedDef = $derived(selectedCard ? cardDef(selectedCard.cardId) : null); const EDGE_CARDS = new Set([ @@ -1004,6 +1014,11 @@ {:else}
{view.activePlayerId} is taking their turn…
{/if} + {#if interruptCards.length > 0} +
+ ⚡ Your {cardDef(interruptCards[0]!.cardId).name} can seize this moment — tap the glowing card. +
+ {/if} {#if view.yourWardArmed}
🗡 Your ward is set — a thief who grabs your treasure bleeds for 3.
@@ -1272,6 +1287,7 @@ attachedMods.some((m) => m.instanceId === card.instanceId)} marked={discardSelection.has(card.instanceId)} displayed={view.players.find((p) => p.id === view.you)?.displayed.some((c) => c.instanceId === card.instanceId) ?? false} + glowing={interruptCards.some((c) => c.instanceId === card.instanceId)} charges={view.wandCharges[card.instanceId] ?? null} onclick={() => selectCard(card)} /> diff --git a/packages/web/src/Card.svelte b/packages/web/src/Card.svelte index 2f24db1..6fe0ee9 100644 --- a/packages/web/src/Card.svelte +++ b/packages/web/src/Card.svelte @@ -8,6 +8,7 @@ attached = false, marked = false, displayed = false, + glowing = false, charges = null, onclick, }: { @@ -16,6 +17,8 @@ attached?: boolean; marked?: boolean; displayed?: boolean; + /** Playable RIGHT NOW out of turn — a golden pulse invites the tap. */ + glowing?: boolean; charges?: number | null; onclick?: () => void; } = $props(); @@ -40,6 +43,7 @@ class="card" class:selected class:attached + class:glowing class:marked class:number={isNumber} {onclick} @@ -192,7 +196,16 @@ white-space: nowrap; } + .card.glowing { + box-shadow: 0 0 0 2px #c9a72a, 0 0 14px 3px rgba(201, 167, 42, 0.55); + animation: card-glow 1.4s ease-in-out infinite; + } + @keyframes card-glow { + 0%, 100% { box-shadow: 0 0 0 2px #c9a72a, 0 0 14px 3px rgba(201, 167, 42, 0.55); } + 50% { box-shadow: 0 0 0 2px #c9a72a, 0 0 6px 1px rgba(201, 167, 42, 0.3); } + } @media (prefers-reduced-motion: reduce) { + .card.glowing { animation: none; } .card { transition: none; } .card:hover, .card.selected, .card.attached { transform: none; } }