{#each ghostSlots ?? [] as g (`${g.x},${g.y}`)} onGhostClick?.(g)} onkeydown={() => {}} /> {/each} {#each cells as c (`${c.x},${c.y}`)} tapCell(c)} onpointerdown={() => pressCell(c)} onpointerup={releasePress} onpointerleave={releasePress} onkeydown={() => {}} /> {/each} {#each view.players as p (p.id)} {@const hx = p.home.x * CELL + CELL / 2} {@const hy = p.home.y * CELL + CELL / 2} {@const R = CELL * 0.36} {@const r = CELL * 0.14} { const rad = (Math.PI / 8) * i - Math.PI / 2; const len = i % 2 === 0 ? R : r; return `${hx + Math.cos(rad) * len},${hy + Math.sin(rad) * len}`; }).join(" ")} class="home-star" fill={playerColor(p.id)} /> {/each} {#each [...view.treasures.reduce((m, t) => { if (!t.position) return m; const k = `${t.position.x},${t.position.y}`; m.set(k, [...(m.get(k) ?? []), t]); return m; }, new Map()).entries()] as [tKey, group] (tKey)} {#each group as t, ti (t.id)} {#if t.position} {@const tx = t.position.x * CELL + CELL / 2 + (group.length > 1 ? (ti - (group.length - 1) / 2) * CELL * 0.3 : 0)} {@const ty = t.position.y * CELL + CELL * 0.72} {#if USE_TOKEN_ART} {:else} {/if} {/if} {/each} {/each} {#each Object.entries(view.squareContents) as [key, content] (key)} {@const sx = Number(key.split(",")[0])} {@const sy = Number(key.split(",")[1])} {#if USE_TOKEN_ART && TERRAIN_ART[content.kind]} {:else if content.kind === "stone"} {:else if content.kind === "thornbush" || content.kind === "rosebush"} {:else if content.kind === "ooze" || content.kind === "slime"} {:else if content.kind === "dust"} {:else if content.kind === "pit"} {:else if content.kind === "tacks"} ✻✻ {:else if content.kind === "safe"} {/if} {/each} {#each view.dimWarps as w, wi (wi)} {#each [w.a, w.b] as tok, i (i)} {#if USE_TOKEN_ART} {:else} {/if} {/each} {/each} {#each view.boobytraps as trap, ti (ti)} {#each trap.cells as tc, i (i)} {/each} {/each} {#each Object.entries(view.groundObjects) as [key, objects] (key)} {@const gx = Number(key.split(",")[0])} {@const gy = Number(key.split(",")[1])} {#each objects as o, i (o.instanceId)} {@const art = USE_TOKEN_ART ? objectArt(o.cardId) : null} {#if art} {o.cardId} {:else} {o.cardId} {/if} {/each} {/each} {#each edges as e (`${e.kind}:${e.x},${e.y}`)} {@const cls = e.state === "door" ? `door${e.lock ? ` ${e.lock}` : ""}` : e.state === "firewall" ? "firewall" : "wall"} {@const lockTitle = e.lock === "removed" ? "lock removed — swings free" : e.lock === "jammed" ? "lock jammed — sealed for good" : e.lock === "ajar" ? "unlocked until end of turn" : null} {#if e.kind === "V"} {#if lockTitle}{lockTitle}{/if} {:else} {#if lockTitle}{lockTitle}{/if} {/if} {/each} {#each Object.entries(view.wallDamage) as [key, dmg] (key)} {@const kind = key.split(":")[0]} {@const wx = Number(key.split(":")[1]?.split(",")[0])} {@const wy = Number(key.split(":")[1]?.split(",")[1])} {@const frac = Math.min(1, dmg / 20)} {#if kind === "V"} {:else} {/if} {/each} {#each view.knownIllusionEdges as key (key)} {@const kind = key.split(":")[0]} {@const ix = Number(key.split(":")[1]?.split(",")[0])} {@const iy = Number(key.split(":")[1]?.split(",")[1])} {#if kind === "V"} {:else} {/if} {/each} {#each view.board.warps as w, i (i)} {@const wx = w.from.cell.x * CELL + CELL / 2 + (w.from.side === "E" ? CELL * 0.42 : w.from.side === "W" ? -CELL * 0.42 : 0)} {@const wy = w.from.cell.y * CELL + CELL / 2 + (w.from.side === "S" ? CELL * 0.42 : w.from.side === "N" ? -CELL * 0.42 : 0)} {@const standing = view.players.some((p) => p.id === view.you && p.position.x === w.from.cell.x && p.position.y === w.from.cell.y) || view.creatures.some((c) => c.id === selectedCreatureId && c.position.x === w.from.cell.x && c.position.y === w.from.cell.y)} {@const letter = warpLetters.get(`${w.from.cell.x},${w.from.cell.y}`) ?? ""} { ev.stopPropagation(); onWarpClick?.(w.from.cell, w.from.side); }} onkeydown={() => {}} > {w.from.side === "N" ? "↑" : w.from.side === "S" ? "↓" : w.from.side === "E" ? "→" : "←"} {letter} {#if standing} {/if} {/each} {#if markedCell} {/if} {#each markedCells ?? [] as m, i (`${m.x},${m.y}`)} {i + 1} {/each} {#if markedSector} {/if} {#each view.players.filter((p) => p.alive) as p (p.id)} {@const group = wizardsByCell.get(`${p.position.x},${p.position.y}`) ?? [p]} {@const gi = group.findIndex((q) => q.id === p.id)} {@const cx = p.position.x * CELL + CELL / 2 + (group.length > 1 ? (gi - (group.length - 1) / 2) * 14 : 0)} {@const cy = p.position.y * CELL + CELL * 0.36} { ev.stopPropagation(); if (peekFired) { peekFired = false; return; } onPlayerClick?.(p.id); }} onpointerdown={() => pressCell(p.position)} onpointerup={releasePress} onpointerleave={releasePress} onkeydown={() => {}} class="wizard" > {#if USE_TOKEN_ART} {@const half = CELL * 0.3 * wizardScale(p.id)} {#if hasSpell(p.id, "sticky-web")} {/if} {:else} {p.id[0]?.toUpperCase()} {/if} {#if p.carriedTreasureId} {/if} {/each} {#each view.creatures as c (c.id)} {@const cGroup = creaturesByCell.get(`${c.position.x},${c.position.y}`) ?? [c]} {@const ci = cGroup.findIndex((q) => q.id === c.id)} {@const ccx = c.position.x * CELL + CELL * 0.72 - (cGroup.length > 1 ? ci * CELL * 0.26 : 0)} {@const ccy = c.position.y * CELL + CELL * 0.7} { ev.stopPropagation(); if (peekFired) { peekFired = false; return; } onCreatureClick?.(c.id); }} onpointerdown={() => pressCell(c.position)} onpointerup={releasePress} onpointerleave={releasePress} onkeydown={() => {}} > {#if USE_TOKEN_ART && CREATURE_ART[c.kind]} {c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg {:else} {c.kind} ({c.controllerId}) — {c.damage}/{Number.isFinite(c.maxDamage) ? c.maxDamage : "∞"} dmg {c.kind === "fire-imp" ? "I" : c.kind === "democratic-monster" ? "D" : c.kind[0]?.toUpperCase()} {/if} {/each} {#each edgeHitboxes as h (`${h.cell.x},${h.cell.y},${h.side}`)} { ev.stopPropagation(); onEdgeClick?.(h.cell, h.side); }} onkeydown={() => {}} /> {/each} {#if litCells} {#each Object.keys(view.board.cells) as key (key)} {#if !litCells.has(key)} {@const dx = Number(key.split(",")[0])} {@const dy = Number(key.split(",")[1])} {/if} {/each} {/if}