{#snippet sayBox(extra: string, withDie: boolean)}
{ e.preventDefault(); const t = chatDraft.trim(); if (!t) return; if (net.sendChat(t)) chatDraft = ""; else net.flash("Not sent. The table is out of reach, and your words wait in the box."); }}> {#if net.chatPending}sending…{/if} {#if withDie} {/if}
{/snippet} {#snippet gallerySayBox(extra: string)} {/snippet} {#if fpvWorkshop || tokenWorkshop || fxWorkshop}
Wiz-War {fpvWorkshop ? "the eyes workshop" : tokenWorkshop ? "the token workshop" : "the flourish workshop"} ← back to the table the clips tokens flourishes eyes
{#if fpvWorkshop} {:else if tokenWorkshop} {:else} {/if} {:else}
Wiz-War sixth edition {__BUILD_STAMP__} {#if local.active} hotseat {:else if net.roomId} {#if net.you === net.hostId && net.roomId && (net.audience > 0 || net.galleryTalk)} {:else if net.audience > 0} 👁 {net.audience}{net.galleryTalk ? " 💬" : ""} {/if} {#if !net.spectating} {/if} {#if !net.spectating && net.you === net.hostId && peopleSeats.length > 0} {/if} {/if} {net.status === "connected" ? "" : "reconnecting…"}
{#if offline} {/if} {#if showDiscards && view}
(showDiscards = false)} onkeydown={() => {}}>
{/if} {#if peekCard || discardPeek || peekTreasure || peekEdge || peekEffect}
{ peekCard = null; peekCreatureId = null; peekToken = null; discardPeek = null; peekTreasure = null; peekEdge = null; peekEffect = null; }} onkeydown={() => {}}>
{#if peekEffect} {#if peekCard}
(faqCardId = id)} />
{/if}
{spellName(peekEffect.cardId)}. {effectNote(peekEffect)}
{:else if peekEdge}
{peekEdge}
{:else if peekTreasure}
{peekTreasure.note}
{:else} {#if peekCard && peekToken} {/if}
(faqCardId = id)} />
{#if peekNote}
{peekNote}
{/if} {/if}
{/if} {#if illusionPrompt}
(illusionPrompt = null)} onkeydown={() => {}}>
{/if} {#if view?.wardPending} {#if view.wardPending.ownerId === view.you}
{view.wardPending.takerId} grabs your treasure!
"When a player picks up one of your treasures, you may play at that time (out of turn) this card on him." Three points, and the Ward is spent.
{/if} {/if} {#if youMustShield && view?.chaosPending} {@const shield = view.yourHand.find((c) => c.cardId === "full-shield")}
{view.chaosPending.casterId} casts Chaos!
Every hand goes into one pile and is dealt back at random, each wizard getting as many cards as they held. {shield ? "Your Full Shield keeps your hand out of it." : "You hold no Full Shield to sit it out."}
{#if shield} {/if}
{/if} {#if megaPrompt && view} {@const beast = view.creatures.find((c) => c.id === megaPrompt!.creatureId)}
Mega-Monster: the {beast ? cardDef(beast.kind).name : "monster"} grows
"Doubles the existing life-points OR movement rate of any one monster." Which? {#if beast} Now {Number.isFinite(beast.maxDamage) ? `${beast.maxDamage - beast.damage} of ${beast.maxDamage} hits` : "unkillable"} and {beast.movesPerTurn} move{beast.movesPerTurn === 1 ? "" : "s"} a turn. {/if}
{/if} {#if view?.pushPending && view.pushPending.pusheeId === view.you}
{view.pushPending.giantId} bears down on you!
The giant fills the corridor and shoves you ahead of him. The way forks here. Which way do you go?
{#each view.pushPending.exits as d (d)} {/each}
{/if} {#if view?.slowDeathPending && view.slowDeathPending.playerId === view.you} {@const sdPoints = view.slowDeathPending.points} {@const sdAbsorb = view.yourHand.find((c) => c.cardId === "absorb")} {@const sdBlunt = view.yourHand.find((c) => c.cardId === "blunt")}
Slow Death bites for {sdPoints}!
The draw's bites land as one blow, so counter the total. Absorb soaks up to three points, and Blunt halves it (fractions round up).
{#if sdAbsorb} {/if} {#if sdBlunt} {/if}
{/if} {#if feedbackOpen && net.roomId}
(feedbackOpen = false)} onkeydown={() => {}}>
{/if} {#if prefsOpen}
(prefsOpen = false)} onkeydown={() => {}}>
{/if} {#if faqCardId} (faqCardId = null)} /> {/if} {#if showHelp} net.requestStats()} onnavigate={helpNavigate} onclose={helpClose} /> {/if} {#if view?.phase === "finished" && view.winner && victoryCurtain && !victorySeen}
🏆
{view.winner}
{#if view.winReason === "treasures"} carried two stolen treasures home, and the maze is theirs {:else if view.winReason === "lastStanding"} the last wizard standing {:else} wins the game {/if}
{/if} {#if trapNotice}
A cruel draw!
{trapNotice === "gift-from-below" ? "It bites for 3 points, then the deck deals you another." : "The trap snaps shut, and you lose a turn."}
{/if} {#if caution}
Hold on
{#each caution.reasons as r (r)}
{r}
{/each}
{/if} {#if cardReveal}
{cardReveal.title}
{#each cardReveal.cards as card (card.instanceId)} (faqCardId = id)} /> {/each} {#if cardReveal.cards.length === 0}
Nothing. Their thoughts are empty-handed.
{/if}
{cardReveal.cards.length > 0 ? `${cardReveal.cards.length} card${cardReveal.cards.length === 1 ? "" : "s"}. ${cardReveal.note}` : ""}
{/if} {#if wardBite}
The treasure bites back!
{wardBite.pending ? `${wardBite.owner}'s treasure was warded, and it bites for 3 unless you answer it. Your counteractions come next.` : wardBite.amount > 0 ? `${wardBite.owner}'s treasure was warded, and it bites you for ${wardBite.amount} point${wardBite.amount === 1 ? "" : "s"}.` : `${wardBite.owner}'s treasure was warded, but its bite finds no purchase.`}
{/if} {#if view && view.outOfTurnWindow?.playerId === view.you && !momentSeen}
The maze holds its breath
Time parts for your {cardDef(view.outOfTurnWindow.kind).name}. Pick an attack card and a target, or let the moment pass.
{/if} {#if openingRolls}
The dice decide who casts first
{#each openingRolls.players as p (p)}
{p === openingRolls.first ? "🏆 " : ""}{p} {#each openingRolls.rolls[p] ?? [] as r, i (i)}🎲{r}{/each}
{/each}
{#if Object.values(openingRolls.rolls).some((r) => r.length > 1)}
Tied wizards rolled again.
{/if}
{/if} {#if view?.stack && attackNoticeKey && attackNoticeSeen !== attackNoticeKey && !noFanfare}
{#if view.stack.defenderId === view.you && attackingCreature} {view.stack.attackerId}'s {cardDef(attackingCreature.kind).name} attacks you! {:else if view.stack.defenderId === view.you && view.stack.reflectedBase} Your own spell comes screaming back! {:else if view.stack.defenderId === view.you} {view.stack.attackerId} attacks you! {:else} {view.stack.counters[view.stack.counters.length - 1]?.player} counters your {yourAttackName}! {/if}
{#if view.stack.defenderId === view.you && attackingCreature}
{:else if view.stack.defenderId === view.you && view.stack.attackCard} {@const illusionOf = view.stack.attackCard.cardId === "illusionary-attack" ? (view.stack.params?.cardId ?? null) : null} {#if illusionOf}
(faqCardId = id)} /> (faqCardId = id)} />
an ILLUSIONARY {cardDef(illusionOf).name}, which bites only if believed
{:else}
{/if} {#if view.stack.reflectedBase}
REFLECTED, with {view.stack.reflectedBase.damage} damage inbound unless you answer it
{:else if view.stack.numberValue != null || view.stack.amplifyFactor > 1}
{view.stack.numberValue != null ? `powered by a ${view.stack.numberValue}` : ""} {view.stack.amplifyFactor > 1 ? `, AMPLIFIED ×${view.stack.amplifyFactor}` : ""}
{/if} {:else if view.stack.defenderId === view.you && view.stack.sourceName} {#if blowCard}
(faqCardId = id)} />
{/if}
{view.stack.sourceName.toUpperCase()}, with {view.stack.params?.damage ?? 0} damage inbound unless you answer it
{:else if view.stack.defenderId === view.you}
a bare-knuckled punch
{:else if view.stack.counters.length > 0}
{/if} {#if view.stack.defenderId === view.you && view.stack.counters.length > 0} {@const standing = view.stack.counters.filter((c) => c.player === view.you && !c.nullified)} {@const broken = view.stack.counters.filter((c) => c.player === view.you && c.nullified)} {#if broken.length > 0 && standing.length === 0}
Your {broken.map((c) => cardDef(c.card.cardId).name).join(" and ")} {broken.length === 1 ? "was" : "were"} nullified, and the attack comes on unchecked.
{:else if standing.length > 0}
Your {standing.map((c) => cardDef(c.card.cardId).name).join(" and ")} stand{standing.length === 1 ? "s" : ""}, so only what slips past {standing.length === 1 ? "it" : "them"} will land. Counter again, or let it resolve.
{/if} {/if}
{/if} {#if net.moment && net.moment.steps.length > 0} net.requestShare()} onclose={() => net.closeMoment()} /> {:else if net.catchUp && net.catchUp.length > 0} net.requestShare(-1) : null} onclose={() => net.closeCatchUp()} /> {:else if local.replaySteps && local.replaySteps.length > 0} (local.replaySteps = null)} /> {/if} {#if net.error} {/if} {#if net.transferCode}
On your other device, claim this phrase within ten minutes. It works once. {net.transferCode.code}
{/if} {#if seatsOpen && net.you === net.hostId && !net.spectating}
Seats at your table
{#each peopleSeats as p (p)} {@const standing = net.standIns.includes(p)}
{p}{#if standing} ⚙ the clockwork plays for them{/if} {#if !standing && net.started && view?.phase === "playing" && view.players.find((w) => w.id === p)?.alive} {#if standInAsked === p} {:else} {/if} {/if}
{/each}
{/if} {#if net.seatPhrase}
Give {net.seatPhrase.name} this phrase to claim within ten minutes, on the front page or while watching this table. It works once, and their old link stops opening the seat. {net.seatPhrase.code}
{/if} {#if local.setup}
wizard {local.setup.names.length + 1} of {local.setup.count}
What is your name?
Your color
{#each [0, 1, 2, 3, 4, 5] as c (c)} {@const taken = local.setup.colors.includes(c)} {/each}
{ e.preventDefault(); const err = local.submitName(setupName, setupColor); if (err) net.error = err; else setupName = ""; }}>
{#if local.setup.names.length > 0}
so far: {local.setup.names.join(", ")}
{/if}
{:else if local.handoffTo && !openingRolls}
local.takeSeat()} onkeydown={(e) => e.key === "Enter" && local.takeSeat()}>
pass the device to
{local.handoffTo}
tap when only they can see the screen
{/if} {#if !net.roomId && !local.active}
Wiz-War
Steal treasure. Blast through walls. Outwit your friends.
A turn-based wizard board game for two to six, free in your browser, with friends or clockwork opponents. Carry home two treasures that aren't yours, or be the last wizard standing.
or join one
Through a wizard's eyes: an ambush watches its corridor through a wormhole watch clips from the labyrinth {#if family.length}

Also in {#if hall}Kestrel's Hall{:else}Kestrel's Hall{/if}: {#each family as g, i (g.slug)}{i > 0 ? ", " : " "}{g.name}{/each}

{/if}
More ways to play, passing one device around or claiming a transferred seat
Play here on one device, passed from wizard to wizard
{#each [2, 3, 4, 5, 6] as n (n)} {/each}
{#if local.hasSave()} {/if}

Lost your games? If this browser forgot them, .

{#if seatsFound || seatsElsewhere}

{seatsFound}{#if seatsElsewhere} {seatsElsewhere === 1 ? "One more table" : `${seatsElsewhere} more tables`} in play {seatsElsewhere === 1 ? "has" : "have"} a seat under your name, last used from another address, or given to the clockwork by the host. The host is whoever opened the table: ask them for a transfer phrase, or write to the keeper, who can seat you again.{/if}

{/if}
{#if net.seats.length > 0}
your games {#if !net.notificationsEnabled} {/if} {#if noFanfare} {/if}
{#each net.seats as seat (seat.roomId + seat.name)} {@const g = net.games.find((x) => x.roomId === seat.roomId && x.name === seat.name)}
{#if g?.rematch && g.rematch.by !== seat.name} {/if} {#if forgetting === seat.roomId} This browser may be all that holds your seat. {:else} {/if}
{/each} {#if net.feedbackReports.length > 0}
your reports to the keeper
{#each net.feedbackReports as r (r.id)} {@const thread = r.thread ?? []}
{r.roomId}
“{r.happened}”{#if r.image} 📷{/if}
{#each thread as line, j (j)} {#if line.from === "player"}
you: {line.text}
{:else}
{line.status} {line.text}
{/if} {/each} {#if !thread.length}
the keeper is studying the moment
{:else if thread[thread.length - 1]!.from === "player"}
your word is with the keeper
{/if} {#if thread.some((l) => l.from === "desk")}
{ e.preventDefault(); const t = (reportAnswers[r.id] ?? "").trim(); if (t && net.answerReport(r, t)) reportAnswers = { ...reportAnswers, [r.id]: "" }; }}>
{/if}
{/each} {/if}
{/if}
{:else if !local.active && !net.started}
Gather your wizards
Room {net.roomId} · {net.players.length} of 6 seats filled
{#if net.spectating} {#if net.players.length < 6}
{net.hostId ? `Join ${net.hostId}'s game` : "Join the game"}
Choose your wizard's name and take a seat. The boards flip when the host begins.

👁 Just watching? You're in the Peanut Gallery until you sit.

{:else}

👁 The table is full, so you watch from the Peanut Gallery. The boards flip when the host begins.

{/if} {/if}
    {#each net.players as p (p)} {@const chosen = net.roomColors[p]} {@const isYou = p === net.you}
  • {#if chosen !== undefined} {:else} {/if} {p}{isYou ? " (you)" : ""} {#if net.roomBots[p]}⚙ {net.roomBots[p]}{:else if chosen === undefined}{p === net.hostId ? "host · " : ""}choosing a wizard…{:else if p === net.hostId}host{:else}wizard{/if} {#if isYou && !net.spectating} {/if} {#if net.you === net.hostId && p !== net.hostId} {/if}
  • {#if isYou && !net.spectating && colorPickerOpen}
  • {#each [0, 1, 2, 3, 4, 5] as c (c)} {@const takenBy = Object.entries(net.roomColors).find(([, v]) => v === c)?.[0]} {/each}
  • {/if} {/each} {#each net.expected.filter((n) => !net.players.includes(n)) as n (n)}
  • {n} {net.challenge && n === net.keeper ? `called to the table by ${net.challenge.by}, not yet answered` : "from the last table, not yet seated"}
  • {/each} {#if net.players.length < 6}
  • An empty seat {net.players.length < 2 ? "one more wizard and the boards can flip" : "room for another"}
  • {/if}
{#if !net.spectating}
{#if net.you === net.hostId && net.players.length < 6} {/if}
the link itself, for pasting by hand {inviteLink}
{#if net.keeper && !net.challenge && !net.players.includes(net.keeper) && net.players.length + net.expected.length < 6}
{net.keeper} keeps this table and answers every raven, sometimes within the minute, sometimes after a night's sleep. {keeperHour ? `It is ${keeperHour} where ${net.keeper} lives.` : ""} The seat is held either way.
{:else if net.challenge}
The raven has flown to {net.keeper}, called by {net.challenge.by}. {keeperHour ? `It is ${keeperHour} there.` : ""} A seat is held for them. If they can come now, they will take it. If they are at supper, or asleep, or off in the world, they will leave a word below when they can. In the meantime you can seat a clockwork wizard, invite a friend, or play another game in a new tab. This room keeps its place on your ledger until you come back, as long as nobody abandons it.
{/if}
{#each lobbyTalk as line, i (i)}
{line.text}
{/each} {@render sayBox("lobby-say", false)}
{#if net.you === net.hostId && addingBot && net.players.length < 6}
Difficulty {#each ["apprentice", "adept", "archmage"] as t (t)} {/each}
Personality {#each ["hunter", "berserker", "worrier", "mystery"] as st (st)} {/each}
{BOT_BLURBS[botStyle]}
{/if} {:else}
{#each lobbyTalk as line, i (i)}
{line.text}
{/each} {#if net.galleryTalk} {@render gallerySayBox("lobby-say")} {:else}

👁 The gallery listens. The host has not opened its mouth.

{/if}
{/if} {#if net.you === net.hostId}
{#if net.players.length < 2}
Waiting for one more wizard
{/if}
{:else}

Waiting for {net.hostId} to flip the boards…

{/if}
{:else if view}
{#snippet tableGuidance()} {#if youMustRespond || selectedDef || selectedCreature || youMustDiscard || discardMode || discardSelection.size > 0}
{#if selectedCreature && !selectedDef} {@const sc = view.creatures.find((c) => c.id === selectedCreature)} {#if sc} {#if sc.kind === "alter-ego"} Your double stands ready. Select a card and it casts from the double's square, by the double's sight. {:else} Commanding {cardDef(sc.kind).name}, {creatureStats(sc)}. Tap a square beside it to march, a target in its square to attack{sc.kind === "troll" ? ", or a wall line beside it to punch" : ""}. {/if} {#if dmWarnCell} ⚠ it will claw YOU the moment it enters your square, so tap again only if you mean it {/if} {/if} {/if} {#if youMustRespond && view.stack} {#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" : view.stack.sourceName ? `a ${view.stack.sourceName}` : "a punch"}. Tap a counteraction card, or {:else} {view.stack.waitingOn === view.you ? `They counter your ${yourAttackName}. Counter back, or` : ""} {/if} {/if} {#if youMustDiscard} Your hand is over the limit. Mark cards and discard. {:else if discardMode && discardSelection.size === 0} Mark the cards to throw away, then confirm. {/if} {#if discardSelection.size > 0} {/if} {#if discardMode || (discardSelection.size > 0 && !youMustDiscard)} {/if} {#if selectedDef} {selectedDef.name} {#if pendingCellFor} Now click where {pendingCellFor} goes{selectedCard?.cardId === "mental-force" ? " (within three walked spaces of them, no sight needed)" : ""}. {/if} {#if selectedCard && EDGE_CARDS.has(selectedCard.cardId)} {EDGE_HINTS[selectedCard.cardId] ?? "Click a wall line."} {/if} {#if selectedCard && WAND_IDS.has(selectedCard.cardId) && view.wandCharges[selectedCard.instanceId] == null && !attachedNumber} The first use sets the charges. Tap a NUMBER card first, or cast bare for a single charge. {:else if selectedDef.cardType === "attack" && !cellSelectMode && !EDGE_CARDS.has(selectedCard?.cardId ?? "")} Click a target, or a wall line to batter it{attachedNumber ? ` (powered by a ${numberTotal})` : ""}. {/if} {#if attachedMods.length > 0} [+ {attachedMods.map((m) => cardDef(m.cardId).name).join(", ")}] {/if} {#if attachedMods.some((m) => m.cardId === "power-attack")} {/if} {#if selectedCard?.cardId === "boobytrap"} Place 4 tokens ({trapCells.length}/4). The first is real. {/if} {#if selectedCard?.cardId === "redirection"} {tradeFrom ? "Now the exit it should connect TO." : "Click the first exit."} {:else if selectedCard && TWO_CELL_CARDS.has(selectedCard.cardId)} {tradeFrom ? "Now the second square." : "Click the first square."} {/if} {#if selectedCard?.cardId === "teleport" && !youMustRespond} {pendingTeleport ? "Tap the marked square again to jump, or pick another." : "Tap a destination up to four squares away, walls ignored."} {#if pendingTeleport} {/if} {/if} {#if selectedCard?.cardId === "mega-monster"} Tap the monster, then choose whether to double its life-points or its movement. {/if} {#if selectedCard?.cardId === "pick-lock" || selectedCard?.cardId === "master-key"} {/if} {#if selectedCard?.cardId === "master-key" && isYourTurn && !me?.displayed.some((c) => c.instanceId === selectedCard!.instanceId)} {/if} {#if selectedCard?.cardId === "rotate-sector"} {#if pendingRotate} The marked sector turns a quarter. {:else} Tap a square in the sector to turn. {/if} {/if} {#if selectedCard?.cardId === "relocate-sector"} {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."} {/if} {#if selectedCard && NAMED_CARDS.has(selectedCard.cardId)} {#each nameSuggestions as n (n)}{/each} {#if selectedCard.cardId === "deja-vu"} {:else} Then click the target. {/if} {/if} {#if selectedCard?.cardId === "swap-meet"} {#if !swapMeetTarget} Click the other trader. {:else if myTradables.length === 0} You carry no items to trade. {:else if !swapMine} Trade away your {#each myTradables as c (c.key)} {/each} {:else if theirTradables.length === 0} They show no item you could claim. {:else} and take their {#each theirTradables as c (c.key)} {/each} {/if} {/if} {#if selectedCard?.cardId === "waterbolt"} (knockback {numberTotal - wbDamage}) {/if} {#if selectedCard?.cardId === "power-run"} {/if} {#if selectedCard && confirmCastable(selectedCard.cardId)} {/if} {#if selectedCard && isMovableObject(selectedCard.cardId) && isYourTurn} {/if} {#if selectedCard && isYourTurn && !youMustRespond} {/if} {#if selectedCard?.cardId === "add"} ADD rides a second NUMBER. Play one number for movement, then play another, and the Add spends itself. {/if} {#if selectedCard && isNumberCard(selectedCard.cardId)} {@const needsAdd = view.turn.numberPlayedForMovement} {@const haveAdd = view.yourHand.some((c) => c.cardId === "add")} {#if needsAdd && !haveAdd} A second movement number needs an ADD card. {:else} {/if} {/if} {/if}
{/if} {/snippet}
{#if view.you && !net.spectating} {/if}
{Math.round(boardZoom * 100)}%
{#if stepCaption && me}
This is you. Tap a lit square beside your wizard to walk.
{:else if theirTurnCaption}
{theirTurnCaption}
{/if}
{#if prefs.liveFp && view.you && !net.spectating} setPref("liveFp", false)} onstride={(side) => tryMove(side)} canStride={yourMoment} ontarget={fpvTarget} onfacing={(s) => (fpvFacing = s)} {litCells} aimBeings={selectedCard != null && !CELL_CARDS.has(selectedCard.cardId) && !EDGE_CARDS.has(selectedCard.cardId)} onpickup={(w) => dispatch(w.kind === "treasure" ? { type: "pickUpTreasure", treasureId: w.id } : { type: "pickUpObject", instanceId: w.id })} onCreatureMove={(creatureId, direction) => dispatch({ type: "moveCreature", creatureId, direction })} onCreatureAttack={(creatureId, targetId) => dispatch({ type: "creatureAttack", creatureId, targetId })} onbody={(id) => (fpvBody = id)} onWarpStep={(creatureId) => dispatch(creatureId ? { type: "creatureWarpStep", creatureId } : { type: "warpStep" })} edgeSelect={edgeSelectMode && yourMoment} /> {/if}
c.id === selectedCreature)?.position ?? null) : punchWallMode ? (me?.position ?? null) : null} selectedCreatureId={selectedCreature} onCellClick={clickCell} onEdgeClick={clickEdge} onPlayerClick={clickPlayer} onCreatureClick={clickCreature} onWarpClick={clickWarp} markedCell={tradeFrom ?? pendingTeleport} markedCells={trapCells.length > 0 ? trapCells : null} effects={boardFx} markedSector={pendingSectorOrigin} ghostSlots={relocateGhosts} onGhostClick={clickGhostSlot} onCellPeek={(c) => peekAt(c)} onIllusionClick={(cell, side) => (illusionPrompt = { cell, side })} onEdgePeek={(tip) => (peekEdge = tip)} {litCells} spotCells={stepCells} {sightTrace} povFacing={prefs.liveFp && view.you && !net.spectating ? fpvFacing : null} povCreatureId={prefs.liveFp && !net.spectating ? fpvBody : null} />
{#if ambushVia}
{cardDef(ambushVia.cardId).name} Set an ambush. {#if !ambushTrigger} It springs when an opponent… {:else if !ambushSpell} Now tap the attack card to commit. {:else if ambushNeedsCell && !ambushCell} {cardDef(ambushSpell.cardId).name}. Click where the trap will send them. {:else} {cardDef(ambushSpell.cardId).name}{ambushCell ? ` → (${ambushCell.x}, ${ambushCell.y})` : ""}{attachedNumber ? ` with a ${numberTotal}` : " (tap a number to power it)"} {/if}
{/if} {#if !castPanelOn} {@render tableGuidance()} {/if}
{#if dockLead && isYourTurn}
{dockLead} {#if selectedCard?.cardId === "teleport" && pendingTeleport} {/if} {#if selectedCard?.cardId === "rotate-sector" && pendingRotate} {/if} {#if selectedCard && isNumberCard(selectedCard.cardId)} {@const needsAdd = view.turn.numberPlayedForMovement} {@const haveAdd = view.yourHand.some((c) => c.cardId === "add")} {#if !(needsAdd && !haveAdd)} {/if} {/if}
{/if} {#if isYourTurn}
{#if onWarpToken} {/if} {#if pickChoice} whose? {#each treasuresHere as t (t.id)} {/each} {/if} {#each objectsHere as obj (obj.instanceId)} {/each} {#each tearTargets as t (t.id)} {/each}
{#if net.pending} sending {net.pending.label}… {:else if net.confirmed} ✓ {net.confirmed} confirmed {/if}
{/if}
{#if selectedCard && !inspectorHidden && !castPanelOn}
{/if} {#if view.phase === "finished" && view.revealedHands}
The hands, face-up
{#each revealFolded ? [] : view.players as p (p.id)}
{p.id === view.winner ? "🏆 " : ""}{p.id} {#if !p.alive}as they fell{/if}
{#each view.revealedHands[p.id] ?? [] as card (card.instanceId)} (peekCard = card)} onfaq={(id) => (faqCardId = id)} /> {/each} {#if (view.revealedHands[p.id] ?? []).length === 0} empty-handed {/if}
{/each}
{/if}
{#if net.spectating} 👁 You watch from the Peanut Gallery, and every hand stays secret, even from you. {#if view.phase !== "finished"} Had a seat at this table? Give your name, and if the seat was last used from here it is yours again. Otherwise choose transfer seat on the device that still holds it, or ask {net.hostId ?? "the host"} for a phrase, and claim it here. Failing that, write to the keeper for a link that brings it back. {#if seatsFound || seatsElsewhere}{seatsFound}{#if seatsElsewhere} That seat was last used from another address, or the host gave it to the clockwork: ask {net.hostId ?? "the host"} for a transfer phrase, or write to the keeper.{/if}{/if} {/if} {/if} {#each view.phase === "finished" ? [] : orderedHand as card (card.instanceId)}
{ draggingId = card.instanceId; e.dataTransfer?.setData("text/plain", card.instanceId); }} ondragover={(e) => e.preventDefault()} ondrop={(e) => { e.preventDefault(); dropCardOn(card.instanceId); }} ondragend={() => (draggingId = null)}> 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)} onfaq={(id) => (faqCardId = id)} />
{/each}
{/if}
{/if}