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 ModeratorPanel from './ModeratorPanel.svelte';
|
||||||
import CaptureTally from './CaptureTally.svelte';
|
import CaptureTally from './CaptureTally.svelte';
|
||||||
import PromotionDialog from './PromotionDialog.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';
|
import type { PromotionType, Square } from '@blind-chess/shared';
|
||||||
|
|
||||||
interface Props { gameId: string; }
|
interface Props { gameId: string; }
|
||||||
@@ -92,6 +96,37 @@
|
|||||||
}
|
}
|
||||||
return 'Opponent thinking';
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="game-layout" class:waiting={game.state.gameStatus === 'waiting'}>
|
<div class="game-layout" class:waiting={game.state.gameStatus === 'waiting'}>
|
||||||
@@ -129,11 +164,16 @@
|
|||||||
toMove={game.state.view.toMove}
|
toMove={game.state.view.toMove}
|
||||||
mode={game.state.mode ?? 'blind'}
|
mode={game.state.mode ?? 'blind'}
|
||||||
highlightingEnabled={game.state.highlightingEnabled}
|
highlightingEnabled={game.state.highlightingEnabled}
|
||||||
|
phantoms={phantomLayerEnabled ? phantoms.state.phantoms : {}}
|
||||||
|
phantomsEnabled={phantomLayerEnabled}
|
||||||
armedSquare={armedSquare}
|
armedSquare={armedSquare}
|
||||||
touchedSquare={game.state.touchedPiece}
|
touchedSquare={game.state.touchedPiece}
|
||||||
{onArm}
|
{onArm}
|
||||||
{onCommit}
|
{onCommit}
|
||||||
/>
|
/>
|
||||||
|
{#if phantomLayerEnabled}
|
||||||
|
<PhantomPalette {oppColor} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="side">
|
<aside class="side">
|
||||||
@@ -170,6 +210,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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}
|
{#if pendingPromotion && game.state.you}
|
||||||
<PromotionDialog
|
<PromotionDialog
|
||||||
color={game.state.you}
|
color={game.state.you}
|
||||||
@@ -221,7 +268,13 @@
|
|||||||
color: var(--text-dim);
|
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 {
|
.side {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -270,4 +323,16 @@
|
|||||||
.waiting-card h2 { margin: 0 0 8px; font-size: 18px; }
|
.waiting-card h2 { margin: 0 0 8px; font-size: 18px; }
|
||||||
.link-row { display: flex; gap: 8px; margin-top: 16px; }
|
.link-row { display: flex; gap: 8px; margin-top: 16px; }
|
||||||
.link { flex: 1; font-family: ui-monospace, monospace; font-size: 13px; }
|
.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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user