The casting panel: the rail becomes the spell while you build it
While a card is selected (or an attack demands an answer) on a wide screen, the chronicle and chat yield their column to a casting panel: the card at reading size, attached numbers and modifiers as cards with their total, and the whole guidance strip — targeting hints, pickers, discard, cancel — moved in beside them. Counteractions get the same panel with the incoming card, its power, and every counter played so far. The under-board strip remains for phones, where a rail below the fold is no place to aim from. The guidance markup extracts into one snippet rendered wherever the mode calls it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
beaae4a0a4
commit
6189b4935c
+283
-195
@@ -437,6 +437,16 @@
|
||||
const TWO_CELL_CARDS = new Set(["trader", "dimensional-warp", "redirection"]);
|
||||
const CREATURE_TARGET_CARDS = new Set(["mega-monster"]);
|
||||
const MODIFIER_CARDS = new Set(["amplify", "add", "extend", "around-the-corner", "power-attack"]);
|
||||
/** Casting panel: on wide screens the paper-rail's chronicle yields to
|
||||
* the spell being built (or answered), controls and all — table talk
|
||||
* returns the moment the cast resolves. Phones keep the under-board
|
||||
* strips; a rail below the fold is no place to aim from. */
|
||||
const wideQuery = typeof matchMedia === "undefined" ? null : matchMedia("(min-width: 901px)");
|
||||
let wideScreen = $state(wideQuery?.matches ?? true);
|
||||
wideQuery?.addEventListener("change", (e) => (wideScreen = e.matches));
|
||||
const castPanelOn = $derived(wideScreen && !local.active &&
|
||||
(selectedCard != null || (youMustRespond && !!view?.stack)));
|
||||
|
||||
/** The hand in the player's own order: drag a card onto another to take
|
||||
* its place. The order lives only in this browser — the server's hand
|
||||
* array is authority for contents, this for arrangement. */
|
||||
@@ -2095,6 +2105,201 @@
|
||||
</section>
|
||||
{:else if view}
|
||||
<div class="game" class:hand-left={prefs.handLeft}>
|
||||
{#snippet tableGuidance()}
|
||||
{#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||
<div class="hint-strip">
|
||||
{#if selectedCreature && !selectedDef}
|
||||
{@const sc = view.creatures.find((c) => c.id === selectedCreature)}
|
||||
{#if 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>
|
||||
{#if dmWarnCell}
|
||||
<span class="hint-alert">⚠ it will claw YOU the moment it enters your square — tap again if you mean it</span>
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={() => (selectedCreature = null)}>done</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if youMustRespond && view.stack}
|
||||
<span class="hint-alert">
|
||||
{#if view.stack.defenderId === view.you && selectedCard?.cardId === "teleport"}
|
||||
{pendingTeleport
|
||||
? "Escaping by teleport — tap the marked square again to jump."
|
||||
: "Escaping by teleport — tap a square within four spaces to mark it."}
|
||||
{:else if view.stack.defenderId === view.you}
|
||||
{view.stack.attackerId} attacks with
|
||||
{attackingCreature ? `the ${cardDef(attackingCreature.kind).name}`
|
||||
: view.stack.attackCard?.cardId === "illusionary-attack" && view.stack.params?.cardId
|
||||
? `an illusionary ${cardDef(view.stack.params.cardId).name}`
|
||||
: view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name
|
||||
: view.stack.tearTreasure ? "a grab at your treasure" : "a punch"} —
|
||||
tap a counteraction card, or
|
||||
{:else}
|
||||
{view.stack.waitingOn === view.you ? "They counter your spell — counter back, or" : ""}
|
||||
{/if}
|
||||
</span>
|
||||
<button class="stamp tiny" onclick={pass}>let it resolve</button>
|
||||
<button class="stamp tiny" onclick={askCouncil}>🎓 hints</button>
|
||||
{/if}
|
||||
{#if youMustDiscard}
|
||||
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
|
||||
{:else if discardMode && discardSelection.size === 0}
|
||||
<span>Mark the cards to throw away, then confirm.</span>
|
||||
{/if}
|
||||
{#if discardSelection.size > 0}
|
||||
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
|
||||
{/if}
|
||||
{#if discardMode || (discardSelection.size > 0 && !youMustDiscard)}
|
||||
<button class="hint-cancel" onclick={cancelDiscardMode}>never mind</button>
|
||||
{/if}
|
||||
{#if selectedDef}
|
||||
<strong class="hint-name">{selectedDef.name}</strong>
|
||||
{#if pendingCellFor}
|
||||
<span>— now click where {pendingCellFor} goes{selectedCard?.cardId === "mental-force"
|
||||
? " (within three walked spaces of them — no sight needed)" : ""}</span>
|
||||
{/if}
|
||||
{#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)}
|
||||
<span>— {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"}</span>
|
||||
{/if}
|
||||
{#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber}
|
||||
<span>— first use sets the charges: tap a NUMBER card first, or cast bare for a single charge</span>
|
||||
{:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}
|
||||
<span>— click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}</span>
|
||||
{/if}
|
||||
{#if attachedMods.length > 0}
|
||||
<span class="hint-mods">[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]</span>
|
||||
{/if}
|
||||
{#if attachedMods.some((m) => m.cardId === "power-attack")}
|
||||
<label class="inline">burn
|
||||
<select class="tier-pick" bind:value={paPoints} aria-label="life points to burn">
|
||||
{#each [1, 2, 3, 4, 5] as n (n)}
|
||||
{#if me && n < me.life}<option value={n}>{n}</option>{/if}
|
||||
{/each}
|
||||
</select>
|
||||
life for +{paPoints} damage</label>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "boobytrap"}
|
||||
<span>— place 4 tokens ({trapCells.length}/4; the first is real)</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "redirection"}
|
||||
<span>{tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"}</span>
|
||||
{:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
|
||||
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "teleport" && !youMustRespond}
|
||||
<span>{pendingTeleport
|
||||
? "— tap the marked square again to jump, or pick another"
|
||||
: "— tap a destination (up to four squares, walls ignored)"}</span>
|
||||
{#if pendingTeleport}
|
||||
<button class="stamp tiny" onclick={confirmTeleport}>teleport!</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "mega-monster"}
|
||||
<span>— double its</span>
|
||||
<button class="stamp tiny" class:lit={megaBoost === "life"}
|
||||
onclick={() => (megaBoost = "life")}>life-points</button>
|
||||
<button class="stamp tiny" class:lit={megaBoost === "movement"}
|
||||
onclick={() => (megaBoost = "movement")}>movement</button>
|
||||
<span>— then tap the monster</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"}
|
||||
<label class="inline"><input type="checkbox" bind:checked={holdDoor} /> hold the door open</label>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "master-key" && isYourTurn &&
|
||||
!me?.displayed.some((c) => c.instanceId === selectedCard!.instanceId)}
|
||||
<button class="stamp tiny" onclick={castSelfWithNumber}>Display the key</button>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "rotate-sector"}
|
||||
<label class="inline"><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
|
||||
<span>— click the sector</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "relocate-sector"}
|
||||
<span>{pendingSectorFrom
|
||||
? (relocateGhosts?.length
|
||||
? "— now tap a dashed landing beside the maze"
|
||||
: "— that sector has nowhere legal to go; pick another")
|
||||
: "— click the sector to move"}</span>
|
||||
{/if}
|
||||
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
|
||||
<label class="inline">name <input class="text-input" list="card-name-options" bind:value={nameInput} placeholder="e.g. Fireball" /></label>
|
||||
<datalist id="card-name-options">
|
||||
{#each nameSuggestions as n (n)}<option value={n}></option>{/each}
|
||||
</datalist>
|
||||
{#if selectedCard.cardId === "deja-vu"}
|
||||
<button class="stamp tiny" onclick={() => {
|
||||
const id = nameToCardId(nameInput);
|
||||
if (id && selectedCard) {
|
||||
dispatch({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
|
||||
clearSelection();
|
||||
}
|
||||
}}>Retrieve</button>
|
||||
{:else}
|
||||
<span>— then click the target</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "swap-meet"}
|
||||
{#if !swapMeetTarget}
|
||||
<span>— click the other trader</span>
|
||||
{:else if myTradables.length === 0}
|
||||
<span>— you carry no items to trade</span>
|
||||
{:else if !swapMine}
|
||||
<span>— trade away your:</span>
|
||||
{#each myTradables as c (c.key)}
|
||||
<button class="stamp tiny" onclick={() => (swapMine = c.key)}>{c.label}</button>
|
||||
{/each}
|
||||
{:else if theirTradables.length === 0}
|
||||
<span>— they show no item you could claim</span>
|
||||
{:else}
|
||||
<span>— and take their:</span>
|
||||
{#each theirTradables as c (c.key)}
|
||||
<button class="stamp tiny" onclick={() => castSwapMeet(c.key)}>{c.label}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "waterbolt"}
|
||||
<label class="inline">damage <input class="num-input" type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
|
||||
<span>(knockback {numberTotal - wbDamage})</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "power-run"}
|
||||
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
|
||||
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
|
||||
{/if}
|
||||
{#if selectedCard && confirmCastable(selectedCard.cardId)}
|
||||
<button class="stamp tiny" onclick={castSelfWithNumber}>
|
||||
Cast{attachedNumber ? ` with the ${numberTotal}` : ""}
|
||||
</button>
|
||||
{/if}
|
||||
{#if selectedCard && isMovableObject(selectedCard.cardId) && isYourTurn}
|
||||
<button class="stamp tiny" onclick={() => {
|
||||
if (selectedCard) dispatch({ type: "dropObject", instanceId: selectedCard.instanceId });
|
||||
clearSelection();
|
||||
}}>Drop it here</button>
|
||||
{/if}
|
||||
{#if selectedCard && isYourTurn && !youMustRespond}
|
||||
<button class="stamp tiny discard-one" onclick={() => {
|
||||
if (selectedCard) dispatch({ type: "discard", instanceIds: [selectedCard.instanceId] });
|
||||
clearSelection();
|
||||
}}>Discard it</button>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "add"}
|
||||
<span>— ADD rides a second NUMBER: play one number for movement,
|
||||
then play another, and the Add spends itself</span>
|
||||
{/if}
|
||||
{#if selectedCard && isNumberCard(selectedCard.cardId)}
|
||||
{@const needsAdd = view.turn.numberPlayedForMovement}
|
||||
{@const haveAdd = view.yourHand.some((c) => c.cardId === "add")}
|
||||
{#if needsAdd && !haveAdd}
|
||||
<span>— a second movement number needs an ADD card</span>
|
||||
{:else}
|
||||
<button class="stamp tiny" onclick={playNumberForMovement}>
|
||||
Play for +{cardDef(selectedCard.cardId).value} movement{needsAdd ? " (spends your Add)" : ""}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
<section class="board-zone">
|
||||
<div class="table-stack" class:fpv-primary={prefs.liveFp && !!view.you && !net.spectating}>
|
||||
{#if prefs.liveFp && view.you && !net.spectating}
|
||||
@@ -2331,7 +2536,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chronicle" aria-label="game log" bind:this={chronicleEl}
|
||||
{#if castPanelOn && view}
|
||||
<div class="cast-panel" class:respond={youMustRespond} aria-label={youMustRespond ? "counteraction" : "casting"}>
|
||||
{#if youMustRespond && view.stack}
|
||||
<div class="cast-panel-head">⚔ incoming</div>
|
||||
{#if view.stack.attackCard}
|
||||
<div class="cast-panel-card"><Card card={view.stack.attackCard} onfaq={(id) => (faqCardId = id)} /></div>
|
||||
{:else}
|
||||
<div class="cast-panel-note">a physical blow — no card behind it</div>
|
||||
{/if}
|
||||
{#if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
|
||||
<div class="cast-panel-note">
|
||||
{view.stack.numberValue != null ? `powered by a ${view.stack.numberValue}` : ""}
|
||||
{view.stack.amplifyFactor > 1 ? ` amplified ×${view.stack.amplifyFactor}` : ""}
|
||||
</div>
|
||||
{/if}
|
||||
{#if view.stack.counters.length > 0}
|
||||
<div class="cast-panel-sub">counters so far</div>
|
||||
<div class="cast-panel-minis">
|
||||
{#each view.stack.counters as c (c.card.instanceId)}
|
||||
<span class="cast-mini" class:nullified={c.nullified}>
|
||||
<Card card={c.card} onclick={() => {}} /></span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else if selectedCard}
|
||||
<div class="cast-panel-card"><Card card={selectedCard} onfaq={(id) => (faqCardId = id)} /></div>
|
||||
{#if attachedNumber || attachedMods.length > 0}
|
||||
<div class="cast-panel-sub">riding along</div>
|
||||
<div class="cast-panel-minis">
|
||||
{#if attachedNumber}
|
||||
<span class="cast-mini"><Card card={attachedNumber} onclick={() => {}} /></span>
|
||||
{/if}
|
||||
{#each attachedMods as m (m.instanceId)}
|
||||
<span class="cast-mini"><Card card={m} onclick={() => {}} /></span>
|
||||
{/each}
|
||||
</div>
|
||||
{#if attachedNumber}
|
||||
<div class="cast-panel-note">number total {numberTotal}</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
{@render tableGuidance()}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="chronicle" class:tucked={castPanelOn} aria-label="game log" bind:this={chronicleEl}
|
||||
onscroll={onChronicleScroll}>
|
||||
{#if local.active}
|
||||
{#each local.log as line, i (i)}
|
||||
@@ -2355,7 +2604,7 @@
|
||||
onclick={() => local.rollTableDie()}>🎲 roll the die</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !local.active && net.roomId && !net.spectating}
|
||||
{#if !local.active && net.roomId && !net.spectating && !castPanelOn}
|
||||
<form class="say-box" onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const t = chatDraft.trim();
|
||||
@@ -2391,198 +2640,8 @@
|
||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
|
||||
<div class="hint-strip">
|
||||
{#if selectedCreature && !selectedDef}
|
||||
{@const sc = view.creatures.find((c) => c.id === selectedCreature)}
|
||||
{#if 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>
|
||||
{#if dmWarnCell}
|
||||
<span class="hint-alert">⚠ it will claw YOU the moment it enters your square — tap again if you mean it</span>
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={() => (selectedCreature = null)}>done</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if youMustRespond && view.stack}
|
||||
<span class="hint-alert">
|
||||
{#if view.stack.defenderId === view.you && selectedCard?.cardId === "teleport"}
|
||||
{pendingTeleport
|
||||
? "Escaping by teleport — tap the marked square again to jump."
|
||||
: "Escaping by teleport — tap a square within four spaces to mark it."}
|
||||
{:else if view.stack.defenderId === view.you}
|
||||
{view.stack.attackerId} attacks with
|
||||
{attackingCreature ? `the ${cardDef(attackingCreature.kind).name}`
|
||||
: view.stack.attackCard?.cardId === "illusionary-attack" && view.stack.params?.cardId
|
||||
? `an illusionary ${cardDef(view.stack.params.cardId).name}`
|
||||
: view.stack.attackCard ? cardDef(view.stack.attackCard.cardId).name
|
||||
: view.stack.tearTreasure ? "a grab at your treasure" : "a punch"} —
|
||||
tap a counteraction card, or
|
||||
{:else}
|
||||
{view.stack.waitingOn === view.you ? "They counter your spell — counter back, or" : ""}
|
||||
{/if}
|
||||
</span>
|
||||
<button class="stamp tiny" onclick={pass}>let it resolve</button>
|
||||
<button class="stamp tiny" onclick={askCouncil}>🎓 hints</button>
|
||||
{/if}
|
||||
{#if youMustDiscard}
|
||||
<span class="hint-alert">Hand over the limit — mark cards and discard.</span>
|
||||
{:else if discardMode && discardSelection.size === 0}
|
||||
<span>Mark the cards to throw away, then confirm.</span>
|
||||
{/if}
|
||||
{#if discardSelection.size > 0}
|
||||
<button class="stamp tiny" onclick={doDiscard}>Discard {discardSelection.size} marked</button>
|
||||
{/if}
|
||||
{#if discardMode || (discardSelection.size > 0 && !youMustDiscard)}
|
||||
<button class="hint-cancel" onclick={cancelDiscardMode}>never mind</button>
|
||||
{/if}
|
||||
{#if selectedDef}
|
||||
<strong class="hint-name">{selectedDef.name}</strong>
|
||||
{#if pendingCellFor}
|
||||
<span>— now click where {pendingCellFor} goes{selectedCard?.cardId === "mental-force"
|
||||
? " (within three walked spaces of them — no sight needed)" : ""}</span>
|
||||
{/if}
|
||||
{#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)}
|
||||
<span>— {EDGE_HINTS[selectedCard.cardId] ?? "click a wall line"}</span>
|
||||
{/if}
|
||||
{#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber}
|
||||
<span>— first use sets the charges: tap a NUMBER card first, or cast bare for a single charge</span>
|
||||
{:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")}
|
||||
<span>— click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}</span>
|
||||
{/if}
|
||||
{#if attachedMods.length > 0}
|
||||
<span class="hint-mods">[+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}]</span>
|
||||
{/if}
|
||||
{#if attachedMods.some((m) => m.cardId === "power-attack")}
|
||||
<label class="inline">burn
|
||||
<select class="tier-pick" bind:value={paPoints} aria-label="life points to burn">
|
||||
{#each [1, 2, 3, 4, 5] as n (n)}
|
||||
{#if me && n < me.life}<option value={n}>{n}</option>{/if}
|
||||
{/each}
|
||||
</select>
|
||||
life for +{paPoints} damage</label>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "boobytrap"}
|
||||
<span>— place 4 tokens ({trapCells.length}/4; the first is real)</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "redirection"}
|
||||
<span>{tradeFrom ? "— now the exit it should connect TO" : "— click the first exit"}</span>
|
||||
{:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)}
|
||||
<span>{tradeFrom ? "— now the second square" : "— click the first square"}</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "teleport" && !youMustRespond}
|
||||
<span>{pendingTeleport
|
||||
? "— tap the marked square again to jump, or pick another"
|
||||
: "— tap a destination (up to four squares, walls ignored)"}</span>
|
||||
{#if pendingTeleport}
|
||||
<button class="stamp tiny" onclick={confirmTeleport}>teleport!</button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "mega-monster"}
|
||||
<span>— double its</span>
|
||||
<button class="stamp tiny" class:lit={megaBoost === "life"}
|
||||
onclick={() => (megaBoost = "life")}>life-points</button>
|
||||
<button class="stamp tiny" class:lit={megaBoost === "movement"}
|
||||
onclick={() => (megaBoost = "movement")}>movement</button>
|
||||
<span>— then tap the monster</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"}
|
||||
<label class="inline"><input type="checkbox" bind:checked={holdDoor} /> hold the door open</label>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "master-key" && isYourTurn &&
|
||||
!me?.displayed.some((c) => c.instanceId === selectedCard!.instanceId)}
|
||||
<button class="stamp tiny" onclick={castSelfWithNumber}>Display the key</button>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "rotate-sector"}
|
||||
<label class="inline"><input type="checkbox" bind:checked={rotateCW} /> clockwise</label>
|
||||
<span>— click the sector</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "relocate-sector"}
|
||||
<span>{pendingSectorFrom
|
||||
? (relocateGhosts?.length
|
||||
? "— now tap a dashed landing beside the maze"
|
||||
: "— that sector has nowhere legal to go; pick another")
|
||||
: "— click the sector to move"}</span>
|
||||
{/if}
|
||||
{#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)}
|
||||
<label class="inline">name <input class="text-input" list="card-name-options" bind:value={nameInput} placeholder="e.g. Fireball" /></label>
|
||||
<datalist id="card-name-options">
|
||||
{#each nameSuggestions as n (n)}<option value={n}></option>{/each}
|
||||
</datalist>
|
||||
{#if selectedCard.cardId === "deja-vu"}
|
||||
<button class="stamp tiny" onclick={() => {
|
||||
const id = nameToCardId(nameInput);
|
||||
if (id && selectedCard) {
|
||||
dispatch({ type: "cast", instanceId: selectedCard.instanceId, params: { cardId: id } });
|
||||
clearSelection();
|
||||
}
|
||||
}}>Retrieve</button>
|
||||
{:else}
|
||||
<span>— then click the target</span>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "swap-meet"}
|
||||
{#if !swapMeetTarget}
|
||||
<span>— click the other trader</span>
|
||||
{:else if myTradables.length === 0}
|
||||
<span>— you carry no items to trade</span>
|
||||
{:else if !swapMine}
|
||||
<span>— trade away your:</span>
|
||||
{#each myTradables as c (c.key)}
|
||||
<button class="stamp tiny" onclick={() => (swapMine = c.key)}>{c.label}</button>
|
||||
{/each}
|
||||
{:else if theirTradables.length === 0}
|
||||
<span>— they show no item you could claim</span>
|
||||
{:else}
|
||||
<span>— and take their:</span>
|
||||
{#each theirTradables as c (c.key)}
|
||||
<button class="stamp tiny" onclick={() => castSwapMeet(c.key)}>{c.label}</button>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "waterbolt"}
|
||||
<label class="inline">damage <input class="num-input" type="number" min="0" max={numberTotal} bind:value={wbDamage} /></label>
|
||||
<span>(knockback {numberTotal - wbDamage})</span>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "power-run"}
|
||||
<label class="inline">life to trade <input class="num-input" type="number" min="1" max="10" bind:value={runPoints} /></label>
|
||||
<button class="stamp tiny" onclick={castPowerRun}>Run!</button>
|
||||
{/if}
|
||||
{#if selectedCard && confirmCastable(selectedCard.cardId)}
|
||||
<button class="stamp tiny" onclick={castSelfWithNumber}>
|
||||
Cast{attachedNumber ? ` with the ${numberTotal}` : ""}
|
||||
</button>
|
||||
{/if}
|
||||
{#if selectedCard && isMovableObject(selectedCard.cardId) && isYourTurn}
|
||||
<button class="stamp tiny" onclick={() => {
|
||||
if (selectedCard) dispatch({ type: "dropObject", instanceId: selectedCard.instanceId });
|
||||
clearSelection();
|
||||
}}>Drop it here</button>
|
||||
{/if}
|
||||
{#if selectedCard && isYourTurn && !youMustRespond}
|
||||
<button class="stamp tiny discard-one" onclick={() => {
|
||||
if (selectedCard) dispatch({ type: "discard", instanceIds: [selectedCard.instanceId] });
|
||||
clearSelection();
|
||||
}}>Discard it</button>
|
||||
{/if}
|
||||
{#if selectedCard?.cardId === "add"}
|
||||
<span>— ADD rides a second NUMBER: play one number for movement,
|
||||
then play another, and the Add spends itself</span>
|
||||
{/if}
|
||||
{#if selectedCard && isNumberCard(selectedCard.cardId)}
|
||||
{@const needsAdd = view.turn.numberPlayedForMovement}
|
||||
{@const haveAdd = view.yourHand.some((c) => c.cardId === "add")}
|
||||
{#if needsAdd && !haveAdd}
|
||||
<span>— a second movement number needs an ADD card</span>
|
||||
{:else}
|
||||
<button class="stamp tiny" onclick={playNumberForMovement}>
|
||||
Play for +{cardDef(selectedCard.cardId).value} movement{needsAdd ? " (spends your Add)" : ""}
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
<button class="hint-cancel" onclick={clearSelection}>cancel</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !castPanelOn}
|
||||
{@render tableGuidance()}
|
||||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
@@ -2629,7 +2688,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if selectedCard && !inspectorHidden}
|
||||
{#if selectedCard && !inspectorHidden && !castPanelOn}
|
||||
<div class="inspector">
|
||||
<button class="inspector-close" onclick={() => (inspectorHidden = true)} aria-label="hide enlarged card">×</button>
|
||||
<button class="inspector-card" title="read it full size"
|
||||
@@ -2783,6 +2842,35 @@
|
||||
.fb-note { font-size: 0.8rem; color: #6b5a41; font-style: italic; }
|
||||
.fb-thanks { color: #43331f; padding: 0.6rem 0; }
|
||||
.respond-card { display: inline-block; vertical-align: middle; margin-left: 0.4rem; zoom: 0.72; }
|
||||
.chronicle.tucked { display: none; }
|
||||
/* The casting panel wears the table's dark felt so the guidance strips
|
||||
* keep their own colors; the cards bring their own paper. */
|
||||
.cast-panel {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.45rem;
|
||||
padding: 0.6rem;
|
||||
border: 1px dashed rgba(233, 225, 203, 0.35);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.cast-panel.respond { border-color: #e8a03c; }
|
||||
.cast-panel-head {
|
||||
font-family: "Oswald", sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #e8a03c;
|
||||
text-align: center;
|
||||
}
|
||||
.cast-panel-card { zoom: 1.5; align-self: center; }
|
||||
.cast-panel-sub { font-size: 0.78rem; color: #a49c86; text-align: center; }
|
||||
.cast-panel-note { font-size: 0.85rem; color: #d8d2c0; font-style: italic; text-align: center; }
|
||||
.cast-panel-minis { display: flex; flex-wrap: wrap; gap: 0.3rem; justify-content: center; }
|
||||
.cast-mini { zoom: 0.55; }
|
||||
.cast-mini.nullified { opacity: 0.45; filter: grayscale(0.8); }
|
||||
.hand-slot { display: flex; cursor: grab; }
|
||||
.hand-slot:active { cursor: grabbing; }
|
||||
.mast-audience { font-size: 0.85rem; color: #a49c86; white-space: nowrap; }
|
||||
|
||||
Reference in New Issue
Block a user