import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCurrentTrack, useSound } from '../audio/SoundContext';
import { useGameSounds } from '../audio/useGameSounds';
import { BoardCanvas } from '../components/BoardCanvas';
import { ItemBar } from '../components/ItemBar';
import { ItemFlashOverlay } from '../components/ItemFlashOverlay';
import { Logo } from '../components/Logo';
import { MiniBoard } from '../components/MiniBoard';
import { PiecePreview } from '../components/PiecePreview';
import { CornerControls } from '../components/CornerControls';
import { useKeyboardInput } from '../input/useKeyboardInput';
import type { InputAction, Lobby, MatchState } from '../net/types';
import type { GameActions, ItemFlash } from '../net/useGameConnection';
import { itemVisual } from '../theme/assets';
import styles from './GamePage.module.css';
interface Props {
match: MatchState;
lobby: Lobby;
playerId: string;
flashes: ItemFlash[];
actions: GameActions;
}
const INVENTORY_SLOTS = 10;
function useWindowHeight() {
const [height, setHeight] = useState(window.innerHeight);
useEffect(() => {
const onResize = () => setHeight(window.innerHeight);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
return height;
}
export function GamePage({ match, lobby, playerId, flashes, actions }: Props) {
const me = match.players.find((p) => p.id === playerId);
const opponents = useMemo(() => match.players.filter((p) => p.id !== playerId), [match.players, playerId]);
const [targetId, setTargetId] = useState(playerId);
const windowHeight = useWindowHeight();
const cell = Math.max(16, Math.min(32, Math.floor((windowHeight - 170) / match.height)));
const miniCell = opponents.length <= 2 ? 14 : opponents.length <= 4 ? 12 : 9;
// Fall back to self when the selected opponent is gone or eliminated (the server does the same).
const target = match.players.find((p) => p.id === targetId);
useEffect(() => {
if (targetId !== playerId && (!target || !target.alive)) setTargetId(playerId);
}, [target, targetId, playerId]);
const onTarget = useCallback(
(slot: number) => {
const next = slot === 1 ? me : opponents[slot - 2];
if (!next || !next.alive) return;
setTargetId(next.id);
actions.selectTarget(next.id);
},
[me, opponents, actions],
);
const { player } = useSound();
const track = useCurrentTrack();
useGameSounds(match, playerId);
// A hard drop can't be told apart from a normal lock in the snapshots, so it sounds on input.
const onAction = useCallback(
(action: InputAction) => {
if (action === 'HardDrop') player.play('hardDrop');
actions.sendInput(action);
},
[actions, player],
);
useKeyboardInput({
enabled: !!me?.alive && match.phase !== 'finished',
onAction,
onTarget,
});
const flashesFor = (id: string) => flashes.filter((f) => f.targetId === id);
const winner = match.players.find((p) => p.id === match.winnerId);
const nextItem = me?.inventory[0];
return (