feat(ui): Compass component — pinwheel layout, click-to-move wiring
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<script lang="ts">
|
||||
import Board from './Board.svelte';
|
||||
import { gameStore } from './stores/game.svelte';
|
||||
import { PLAYER_BOARDS } from '../engine/boards';
|
||||
import type { BoardId, Player, Square } from '../engine/types';
|
||||
|
||||
const COLORS: Record<Player, string> = {
|
||||
N: '#4a90d9', S: '#d6483f', E: '#9d5bd2', W: '#e08a3a',
|
||||
};
|
||||
// Board centre positions inside the 744x744 compass (see spec §5.1).
|
||||
const POS: Record<BoardId, { left: number; top: number }> = {
|
||||
NW: { left: 200, top: 200 }, NE: { left: 544, top: 200 },
|
||||
SW: { left: 200, top: 544 }, SE: { left: 544, top: 544 },
|
||||
};
|
||||
const BOARD_IDS: BoardId[] = ['NW', 'NE', 'SW', 'SE'];
|
||||
|
||||
let view = $derived(gameStore.view);
|
||||
let active = $derived(gameStore.activeBoards);
|
||||
|
||||
/** Ghost squares for a given board. */
|
||||
function ghostsFor(id: BoardId): Square[] {
|
||||
return view.ghosts.filter((g) => g.board === id).map((g) => g.square);
|
||||
}
|
||||
|
||||
/** Highlight payload for a given board, or null. */
|
||||
function highlightFor(id: BoardId) {
|
||||
const h = gameStore.highlight;
|
||||
if (h === null) return null;
|
||||
if (id !== h.boardA && id !== h.boardB) return null;
|
||||
const local = id === h.boardA ? h.onlyA : h.onlyB;
|
||||
const selectedHere = active.includes(id) ? gameStore.selected : null;
|
||||
return { playable: h.playable, local, selected: selectedHere };
|
||||
}
|
||||
|
||||
function handleSquare(id: BoardId, square: Square): void {
|
||||
if (gameStore.isScrubbing) return;
|
||||
if (!active.includes(id)) return; // only the player-to-move's boards are interactive
|
||||
if (gameStore.selected === null) {
|
||||
gameStore.select(square);
|
||||
} else if (gameStore.highlight?.playable.includes(square)) {
|
||||
gameStore.commitTo(square);
|
||||
} else {
|
||||
gameStore.select(square); // re-grab or cancel
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="compass" style="--glow:{COLORS[view.currentPlayer]}">
|
||||
{#each BOARD_IDS as id (id)}
|
||||
<div class="slot" style="left:{POS[id].left}px; top:{POS[id].top}px;">
|
||||
<Board
|
||||
{id}
|
||||
fen={view.fen[id]}
|
||||
colors={COLORS}
|
||||
ghosts={ghostsFor(id)}
|
||||
highlight={highlightFor(id)}
|
||||
active={active.includes(id)}
|
||||
onSquare={(sq) => handleSquare(id, sq)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="plabel" class:on={view.currentPlayer === 'N'}
|
||||
style="left:372px; top:74px; background:{COLORS.N}">NORTH</div>
|
||||
<div class="plabel" class:on={view.currentPlayer === 'S'}
|
||||
style="left:372px; top:670px; background:{COLORS.S}">SOUTH</div>
|
||||
<div class="plabel vert" class:on={view.currentPlayer === 'E'}
|
||||
style="left:670px; top:372px; background:{COLORS.E}">EAST</div>
|
||||
<div class="plabel vert" class:on={view.currentPlayer === 'W'}
|
||||
style="left:74px; top:372px; background:{COLORS.W}">WEST</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.compass { position: relative; width: 744px; height: 744px; flex: none; }
|
||||
.slot { position: absolute; transform: translate(-50%, -50%); }
|
||||
.plabel {
|
||||
position: absolute; transform: translate(-50%, -50%);
|
||||
color: #fff; font-size: 12px; font-weight: 700; letter-spacing: 0.09em;
|
||||
padding: 6px 12px; border-radius: 7px; white-space: nowrap;
|
||||
}
|
||||
.plabel.vert { writing-mode: vertical-rl; }
|
||||
.plabel.on { box-shadow: 0 0 14px currentColor; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user