Three table manners: Safari replays, docked creature cards, a quiet hand

Safari collapsed the replay board to nothing: an svg with only a
viewBox has no intrinsic size there, and inside the replay's
max-height flex column WebKit sized it 0x0 while Chrome inferred the
ratio and filled the frame. Explicit width/height attributes (doubled,
so the CSS max-* caps still govern the display size) give every engine
the same answer — verified 560x560 in both WebKit and Chromium, with
the main board-zone rendering unchanged.

Commanding your own creature no longer opens the centered card peek —
a modal squarely blocks the squares you are about to march it across.
The card now docks in the paper rail with its live stats, the one
place a card enlarges outside the center of the table. Enemy creatures
and idle peeks keep the modal; they ask nothing of the board.

And once an object is pocketed, the hand goes visibly quiet: cards dim
and refuse selection instead of walking you through target-picking
toward an engine refusal, with a rail slip saying what remains (draw
and end the turn). Counters, chaos shields, and discards still play —
an ambush sprung by the very pickup must be answerable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 18:59:16 -04:00
co-authored by Claude Fable 5
parent 7bd82d7679
commit 1275ac9287
2 changed files with 49 additions and 11 deletions
+44 -11
View File
@@ -108,6 +108,17 @@
); );
const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you); const youMustDiscard = $derived(view != null && view.pendingDiscard === view.you);
const youMustShield = $derived(view?.chaosPending?.queue[0] === view?.you && view != null); const youMustShield = $derived(view?.chaosPending?.queue[0] === view?.you && view != null);
/** Picking up an object ends the turn's actions: the hand goes quiet until
* the draw — but never while a counter, shield, or discard is owed. */
const actionsSpent = $derived(
view != null && isYourTurn && view.turn.actionsEnded &&
!youMustRespond && !youMustShield && !youMustDiscard,
);
const commandedCreature = $derived(
selectedCreature && view
? (view.creatures.find((c) => c.id === selectedCreature) ?? null)
: null,
);
const holdingWard = $derived(view?.yourHand.some((c) => c.cardId === "ward") ?? false); const holdingWard = $derived(view?.yourHand.some((c) => c.cardId === "ward") ?? false);
/** A live moment to interrupt: another wizard acts, no attack is pending. */ /** A live moment to interrupt: another wizard acts, no attack is pending. */
const canInterruptNow = $derived( const canInterruptNow = $derived(
@@ -228,6 +239,7 @@
discardSelection = next; discardSelection = next;
return; return;
} }
if (actionsSpent) return; // picking up an object ended the turn's actions
if (!isYourTurn) { if (!isYourTurn) {
// Live interruption: Interrupt / Opportunity Fire may be played during // Live interruption: Interrupt / Opportunity Fire may be played during
// another player's turn (when no attack is pending). // another player's turn (when no attack is pending).
@@ -571,15 +583,10 @@
} }
const mine = creature.controllerId === view.you || creature.kind === "democratic-monster"; const mine = creature.controllerId === view.you || creature.kind === "democratic-monster";
if (mine) { if (mine) {
const selecting = selectedCreature !== creatureId; // Commanding your own creature docks its card in the rail instead of
selectedCreature = selecting ? creatureId : null; // opening the centered peek — a modal would block the very board
if (selecting) { // squares you are about to march it across.
peekCard = { instanceId: `peek-${creature.kind}`, cardId: creature.kind }; selectedCreature = selectedCreature !== creatureId ? creatureId : null;
peekCreatureId = creature.id;
} else {
peekCard = null;
peekCreatureId = null;
}
} else { } else {
peekAt(creature.position); peekAt(creature.position);
} }
@@ -1153,6 +1160,18 @@
Tap its token to command it. Tap its token to command it.
</div> </div>
{/each} {/each}
{#if commandedCreature}
<div class="creature-dock">
<Card
card={{ instanceId: `dock-${commandedCreature.kind}`, cardId: commandedCreature.kind }}
onfaq={(id) => (faqCardId = id)}
/>
<div class="peek-note">{creatureStats(commandedCreature)}</div>
</div>
{/if}
{#if actionsSpent}
<div class="slip">Your spellwork is spent for this turn — all that remains is to draw and end it.</div>
{/if}
{#if view.yourAmbushes.length > 0} {#if view.yourAmbushes.length > 0}
{#each view.yourAmbushes as a (a.id)} {#each view.yourAmbushes as a (a.id)}
@@ -1258,7 +1277,7 @@
{#if sc} {#if sc}
<span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}. <span>Commanding <strong>{cardDef(sc.kind).name}</strong> — {creatureStats(sc)}.
Tap a square beside it to march, a target in its square to attack.</span> Tap a square beside it to march, a target in its square to attack.</span>
<button class="hint-cancel" onclick={() => { selectedCreature = null; peekCard = null; peekCreatureId = null; }}>done</button> <button class="hint-cancel" onclick={() => (selectedCreature = null)}>done</button>
{/if} {/if}
{/if} {/if}
{#if youMustRespond && view.stack} {#if youMustRespond && view.stack}
@@ -1427,7 +1446,7 @@
</div> </div>
{/if} {/if}
<div class="hand" aria-label="your hand"> <div class="hand" class:spent={actionsSpent && !discardMode && discardSelection.size === 0} aria-label="your hand">
{#each view.phase === "finished" ? [] : view.yourHand as card (card.instanceId)} {#each view.phase === "finished" ? [] : view.yourHand as card (card.instanceId)}
<Card <Card
{card} {card}
@@ -2018,6 +2037,20 @@
.big-peek :global(.card:hover) { transform: scale(2.1); } .big-peek :global(.card:hover) { transform: scale(2.1); }
.big-peek { display: flex; flex-direction: column; align-items: center; } .big-peek { display: flex; flex-direction: column; align-items: center; }
.big-peek-note { margin-top: 6.8rem; max-width: 15rem; font-size: 0.8rem; } .big-peek-note { margin-top: 6.8rem; max-width: 15rem; font-size: 0.8rem; }
.creature-dock {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
}
.creature-dock :global(.card) { cursor: default; }
.creature-dock :global(.card:hover) { transform: none; }
.hand.spent :global(.card) {
opacity: 0.45;
filter: grayscale(0.5);
cursor: default;
}
.hand.spent :global(.card:hover) { transform: none; }
.tier-pick { .tier-pick {
background: #f6f0df; background: #f6f0df;
border: 1px solid #b3a687; border: 1px solid #b3a687;
+5
View File
@@ -149,8 +149,13 @@
}); });
</script> </script>
<!-- Explicit width/height give the svg an intrinsic size: Safari cannot
derive one from the viewBox alone and collapses the board to 0x0 inside
a max-height flex column. Doubled so CSS max-* caps still govern. -->
<svg <svg
viewBox={`-8 -8 ${view.board.width * CELL + 16} ${view.board.height * CELL + 16}`} viewBox={`-8 -8 ${view.board.width * CELL + 16} ${view.board.height * CELL + 16}`}
width={(view.board.width * CELL + 16) * 2}
height={(view.board.height * CELL + 16) * 2}
class="board" class="board"
> >
<!-- floor --> <!-- floor -->