feat(client): wire the phantom opponent-model layer into the game view
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
import ModeratorPanel from './ModeratorPanel.svelte';
|
||||
import CaptureTally from './CaptureTally.svelte';
|
||||
import PromotionDialog from './PromotionDialog.svelte';
|
||||
import PhantomPalette from './PhantomPalette.svelte';
|
||||
import { pieceGlyph } from './pieces.js';
|
||||
import { phantoms } from './stores/phantoms.svelte.js';
|
||||
import { phantomDrag } from './stores/phantom-drag.svelte.js';
|
||||
import type { PromotionType, Square } from '@blind-chess/shared';
|
||||
|
||||
interface Props { gameId: string; }
|
||||
@@ -92,6 +96,37 @@
|
||||
}
|
||||
return 'Opponent thinking';
|
||||
});
|
||||
|
||||
const oppColor = $derived<'w' | 'b'>(game.state.you === 'w' ? 'b' : 'w');
|
||||
|
||||
// Phantom layer is blind-mode-only and shown only during active play.
|
||||
const phantomLayerEnabled = $derived(
|
||||
game.state.mode === 'blind' && game.state.gameStatus === 'active',
|
||||
);
|
||||
|
||||
// The piece type currently being dragged (for the floating ghost), or null.
|
||||
const dragGhost = $derived.by(() => {
|
||||
const a = phantomDrag.state.active;
|
||||
return a && phantomDrag.state.moved ? a.type : null;
|
||||
});
|
||||
|
||||
// Load the phantom layer once `you` is known (blind games only).
|
||||
let phantomsLoaded = $state(false);
|
||||
$effect(() => {
|
||||
if (phantomsLoaded) return;
|
||||
const you = game.state.you;
|
||||
if (you && game.state.mode === 'blind') {
|
||||
untrack(() => phantoms.loadForGame(gameId, you));
|
||||
phantomsLoaded = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Drop the phantom layer when the game ends.
|
||||
$effect(() => {
|
||||
if (game.state.gameStatus === 'finished') {
|
||||
untrack(() => phantoms.clearForGame(gameId));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="game-layout" class:waiting={game.state.gameStatus === 'waiting'}>
|
||||
@@ -129,11 +164,16 @@
|
||||
toMove={game.state.view.toMove}
|
||||
mode={game.state.mode ?? 'blind'}
|
||||
highlightingEnabled={game.state.highlightingEnabled}
|
||||
phantoms={phantomLayerEnabled ? phantoms.state.phantoms : {}}
|
||||
phantomsEnabled={phantomLayerEnabled}
|
||||
armedSquare={armedSquare}
|
||||
touchedSquare={game.state.touchedPiece}
|
||||
{onArm}
|
||||
{onCommit}
|
||||
/>
|
||||
{#if phantomLayerEnabled}
|
||||
<PhantomPalette {oppColor} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<aside class="side">
|
||||
@@ -170,6 +210,13 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if dragGhost}
|
||||
<div
|
||||
class="drag-ghost piece-{oppColor}"
|
||||
style="left: {phantomDrag.state.x}px; top: {phantomDrag.state.y}px;"
|
||||
>{pieceGlyph({ color: oppColor, type: dragGhost })}</div>
|
||||
{/if}
|
||||
|
||||
{#if pendingPromotion && game.state.you}
|
||||
<PromotionDialog
|
||||
color={game.state.you}
|
||||
@@ -221,7 +268,13 @@
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.board-area { display: flex; align-items: center; justify-content: center; }
|
||||
.board-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.side {
|
||||
display: flex;
|
||||
@@ -270,4 +323,16 @@
|
||||
.waiting-card h2 { margin: 0 0 8px; font-size: 18px; }
|
||||
.link-row { display: flex; gap: 8px; margin-top: 16px; }
|
||||
.link { flex: 1; font-family: ui-monospace, monospace; font-size: 13px; }
|
||||
|
||||
.drag-ghost {
|
||||
position: fixed;
|
||||
transform: translate(-50%, -50%);
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
font-size: 44px;
|
||||
line-height: 1;
|
||||
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
.drag-ghost.piece-w { color: #fafafa; }
|
||||
.drag-ghost.piece-b { color: #1a1a1a; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user