Letter the warp openings and show where they lead

Paired openings now share a printed letter (A, B, C ...), exactly as
the physical boards mark them — making the 2-player crossed pairings
legible at a glance. While standing on an opening, its destination
square pulses with a dashed green ring, so "leave at B, re-enter at B"
stops being a leap of faith.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-15 23:31:42 -04:00
co-authored by Claude Fable 5
parent 87a5067c77
commit d7afe3a5ca
+50
View File
@@ -64,6 +64,20 @@
return boxes; return boxes;
}); });
// Warp pairs share a letter, like the printed openings on the real boards.
const warpLetters = $derived.by(() => {
const letters = new Map<string, string>();
let next = 0;
for (const w of view.board.warps) {
const a = `${w.from.cell.x},${w.from.cell.y}`;
const b = `${w.to.cell.x},${w.to.cell.y}`;
const key = [a, b].sort().join("|");
if (!letters.has(key)) letters.set(key, String.fromCharCode(65 + next++));
letters.set(a, letters.get(key)!);
}
return letters;
});
// Group players by cell so co-located wizards fan out. // Group players by cell so co-located wizards fan out.
const wizardsByCell = $derived.by(() => { const wizardsByCell = $derived.by(() => {
const map = new Map<string, typeof view.players>(); const map = new Map<string, typeof view.players>();
@@ -209,6 +223,7 @@
(w.from.side === "S" ? CELL * 0.42 : w.from.side === "N" ? -CELL * 0.42 : 0)} (w.from.side === "S" ? CELL * 0.42 : w.from.side === "N" ? -CELL * 0.42 : 0)}
{@const standing = view.players.some((p) => {@const standing = view.players.some((p) =>
p.id === view.you && p.position.x === w.from.cell.x && p.position.y === w.from.cell.y)} p.id === view.you && p.position.x === w.from.cell.x && p.position.y === w.from.cell.y)}
{@const letter = warpLetters.get(`${w.from.cell.x},${w.from.cell.y}`) ?? ""}
<g <g
class="warp-g" class:standing class="warp-g" class:standing
role="button" tabindex="-1" role="button" tabindex="-1"
@@ -218,7 +233,19 @@
<circle cx={wx} cy={wy} r={11} class="warp-hit" /> <circle cx={wx} cy={wy} r={11} class="warp-hit" />
<text x={wx} y={wy + 4.5} class="warp" <text x={wx} y={wy + 4.5} class="warp"
>{w.from.side === "N" ? "↑" : w.from.side === "S" ? "↓" : w.from.side === "E" ? "→" : "←"}</text> >{w.from.side === "N" ? "↑" : w.from.side === "S" ? "↓" : w.from.side === "E" ? "→" : "←"}</text>
<text
x={wx + (w.from.side === "E" ? -13 : w.from.side === "W" ? 13 : 12)}
y={wy + (w.from.side === "N" ? 13 : w.from.side === "S" ? -9 : 4)}
class="warp-letter"
>{letter}</text>
</g> </g>
{#if standing}
<!-- show where this opening leads -->
<circle
cx={w.to.cell.x * CELL + CELL / 2} cy={w.to.cell.y * CELL + CELL / 2}
r={CELL * 0.42} class="warp-dest"
/>
{/if}
{/each} {/each}
<!-- wizards --> <!-- wizards -->
@@ -342,6 +369,29 @@
stroke-width: 2; stroke-width: 2;
} }
.warp-g.standing .warp { opacity: 1; } .warp-g.standing .warp { opacity: 1; }
.warp-letter {
font-family: "Oswald", sans-serif;
font-size: 10px;
font-weight: 600;
text-anchor: middle;
fill: #6a5c44;
pointer-events: none;
}
.warp-dest {
fill: none;
stroke: #2e7d32;
stroke-width: 2.5;
stroke-dasharray: 7 4;
pointer-events: none;
animation: warp-pulse 1.6s ease-in-out infinite;
}
@keyframes warp-pulse {
0%, 100% { opacity: 0.9; }
50% { opacity: 0.35; }
}
@media (prefers-reduced-motion: reduce) {
.warp-dest { animation: none; }
}
.wizard { cursor: pointer; } .wizard { cursor: pointer; }
.wizard-label { .wizard-label {
font-family: "Oswald", sans-serif; font-family: "Oswald", sans-serif;