import React, { useEffect, useCallback } from 'react'; import type { AvailableMovesResponse, BoardState, Player } from '../types'; interface MovementControlsProps { boardState: BoardState; selectedPlayer: Player | null; availableMoves: AvailableMovesResponse | null; onMove: (playerId: string, direction: string) => Promise; onPass: (playerId: string) => Promise; onChallengeWizard?: (playerId: string) => Promise; onStepBot: () => void; isAutoPlaying: boolean; onToggleAutoPlay: () => void; } export const MovementControls: React.FC = ({ boardState, selectedPlayer, availableMoves, onMove, onPass, onChallengeWizard, onStepBot, isAutoPlaying, onToggleAutoPlay, }) => { const isStarted = Boolean(boardState.turn?.game_started); const currentTurnId = boardState.turn.current_player_id; const activePlayer = boardState.players.find((p) => p.id === currentTurnId) || null; // Always resolve the live coordinates from boardState.players to prevent stale player coordinates const liveSelectedPlayer = selectedPlayer ? boardState.players.find((p) => p.id === selectedPlayer.id) || selectedPlayer : null; const controlledPlayer = liveSelectedPlayer || activePlayer; const isMyTurn = isStarted && Boolean(controlledPlayer && controlledPlayer.id === currentTurnId); const isAdjacentToWizard = Boolean( controlledPlayer && boardState.wizard && Math.max( Math.abs(controlledPlayer.x - boardState.wizard.x), Math.abs(controlledPlayer.y - boardState.wizard.y) ) <= 1 ); const canInitiateWizardChallenge = !controlledPlayer?.party_id || controlledPlayer?.is_party_leader; const handleChallengeWizardClick = useCallback(() => { if (!controlledPlayer) return; if (!isStarted) { alert("Game has not started yet. Click 'Start Game' in header first."); return; } if (!isMyTurn) { alert(`It is not ${controlledPlayer.name}'s turn! Current turn belongs to ${activePlayer?.name || 'another bot'}.`); return; } if (!canInitiateWizardChallenge) { alert("Party member cannot challenge the wizard individually. Only party leader can initiate challenges."); return; } if (!isAdjacentToWizard) { alert("Must be within 1 space of the wizard to challenge."); return; } onChallengeWizard?.(controlledPlayer.id).catch((err: unknown) => { if (err instanceof Error) alert(err.message); }); }, [controlledPlayer, isStarted, isMyTurn, canInitiateWizardChallenge, isAdjacentToWizard, activePlayer, onChallengeWizard]); const handleDirectionClick = useCallback( (dir: string) => { if (!isStarted || !controlledPlayer || !isMyTurn) return; onMove(controlledPlayer.id, dir).catch((err) => alert(err.message)); }, [isStarted, controlledPlayer, isMyTurn, onMove] ); const handlePassClick = useCallback(() => { if (!isStarted || !controlledPlayer || !isMyTurn) return; onPass(controlledPlayer.id).catch((err) => alert(err.message)); }, [isStarted, controlledPlayer, isMyTurn, onPass]); // Keyboard shortcut listener useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Don't trigger if user is typing in an input if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement)?.tagName)) { return; } if (!isStarted || !isMyTurn || !controlledPlayer) return; switch (e.key) { case 'ArrowUp': case 'w': case 'W': case '8': e.preventDefault(); handleDirectionClick('UP'); break; case 'ArrowDown': case 's': case 'S': case '2': e.preventDefault(); handleDirectionClick('DOWN'); break; case 'ArrowLeft': case 'a': case 'A': case '4': e.preventDefault(); handleDirectionClick('LEFT'); break; case 'ArrowRight': case 'd': case 'D': case '6': e.preventDefault(); handleDirectionClick('RIGHT'); break; case '7': case 'q': case 'Q': e.preventDefault(); handleDirectionClick('UP_LEFT'); break; case '9': case 'e': case 'E': e.preventDefault(); handleDirectionClick('UP_RIGHT'); break; case '1': case 'z': case 'Z': e.preventDefault(); handleDirectionClick('DOWN_LEFT'); break; case '3': case 'c': case 'C': e.preventDefault(); handleDirectionClick('DOWN_RIGHT'); break; case ' ': e.preventDefault(); handlePassClick(); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isStarted, isMyTurn, controlledPlayer, handleDirectionClick, handlePassClick]); if (boardState.players.length === 0) { return null; } const moves = availableMoves?.moves || {}; const renderDirButton = (dir: string, label: string) => { const check = moves[dir]; const isAvailable = check?.available ?? false; const disabled = !isStarted || !isMyTurn || !isAvailable; const reason = !isStarted ? "Game has not started yet. Click 'Start Game' in header." : check?.reason || (isAvailable ? `Move to (${check?.target_x}, ${check?.target_y})` : 'Blocked'); return ( ); }; return (
{/* Turn Status Banner */}
{!isStarted ? (
LOBBY PHASE Waiting for "Start Game"
) : (
{activePlayer ? ( <>
Round {boardState.turn.round_number} • Turn {boardState.turn.turn_number} {activePlayer.name}
) : ( Waiting for bots... )}
)} {!isStarted ? ( NOT STARTED ) : isMyTurn ? ( YOUR TURN ) : ( WAITING )}
{/* 8-Directional D-Pad */}
{/* Top Row: NW, UP, NE */}
{renderDirButton('UP_LEFT', '↖')} {renderDirButton('UP', '↑')} {renderDirButton('UP_RIGHT', '↗')}
{/* Middle Row: LEFT, Pass, RIGHT */}
{renderDirButton('LEFT', '←')} {renderDirButton('RIGHT', '→')}
{/* Bottom Row: SW, DOWN, SE */}
{renderDirButton('DOWN_LEFT', '↙')} {renderDirButton('DOWN', '↓')} {renderDirButton('DOWN_RIGHT', '↘')}
{/* Challenge Wizard Button if adjacent */} {isAdjacentToWizard && onChallengeWizard && ( )} {/* Simulation / Bot Controls */}
{isStarted ? 'WASD / Arrows / Numpad to move' : 'Bots can join & depart in Lobby'}
); };