// Multiplayer Grid Battle Game Client // Integrates with SQLite backend via REST API // Game configuration - loaded from server let GAME_CONFIG = null; // Multiplayer game state let multiplayerState = null; // Initialize game state after config is loaded function initializeGameState() { multiplayerState = { // Connection info gameId: null, playerId: null, playerNumber: null, playerName: null, // Game state gameStatus: 'disconnected', // disconnected, waiting, playing, finished currentTurn: GAME_CONFIG.INITIAL_TURN, turnStatus: 'waiting_for_submission', // waiting_for_submission, waiting_for_opponent, executed // Grid and players grid: [], players: {}, // Turn management turnActions: [], movesRemaining: GAME_CONFIG.MOVES_PER_TURN, shotAvailable: true, ghostPreviews: [], // Polling pollTimer: null, lastPollTime: 0, // Game end handling pendingGameEnd: null, finalTurnAnimated: false }; } // DOM elements const gameGrid = document.getElementById('game-grid'); const statusText = document.getElementById('status-text'); const playerName = document.getElementById('player-name'); const movesRemaining = document.getElementById('moves-remaining'); const shotAvailable = document.getElementById('shot-available'); const actionsQueued = document.getElementById('actions-queued'); const submitButton = document.getElementById('submit-turn'); const undoButton = document.getElementById('undo-action'); // Initialize when page loads document.addEventListener('DOMContentLoaded', async function() { await loadGameConfig(); initializeGameState(); initializeMultiplayerGame(); setupEventListeners(); }); // Load game configuration from server async function loadGameConfig() { try { console.log('Loading game configuration from server...'); const response = await fetch('/api/config'); const result = await response.json(); if (result.success) { GAME_CONFIG = result.config; console.log('Game configuration loaded:', GAME_CONFIG); } else { throw new Error('Failed to load config from server'); } } catch (error) { console.error('Error loading config, using fallback:', error); // Fallback configuration if server config fails GAME_CONFIG = { GRID_WIDTH: 10, GRID_HEIGHT: 10, MOVES_PER_TURN: 4, SHOTS_PER_TURN: 1, CELL_SIZE_PX: 30, POLLING_INTERVAL: 1500, PLAYER_ONE_NUMBER: 1, PLAYER_TWO_NUMBER: 2, MAX_PLAYERS: 2, INITIAL_TURN: 1, GAME_END_DELAY: 2000, MOVEMENT_ANIMATION_DELAY: 200, ERROR_FLASH_DURATION: 500, COPY_FEEDBACK_DELAY: 2000, DIRECTION_OFFSETS: { NORTH: { row: -1, col: 0 }, SOUTH: { row: 1, col: 0 }, EAST: { row: 0, col: 1 }, WEST: { row: 0, col: -1 } } }; } } async function initializeMultiplayerGame() { console.log('Initializing Multiplayer Grid Battle Game...'); // Initialize UI first (grid needs to exist before adding players) initializeGrid(); renderGrid(); updateGameStatus('Ready to start'); updateMovesDisplay(); updateActionButtons(); // Check if we're joining an existing game via URL const urlPath = window.location.pathname; console.log('Current URL path:', urlPath); const gameIdMatch = urlPath.match(/\/game\/([a-f0-9-]+)/); if (gameIdMatch) { // Joining existing game console.log('Detected game ID in URL:', gameIdMatch[1]); multiplayerState.gameId = gameIdMatch[1]; await showJoinGameInterface(); } else { // Creating new game console.log('No game ID in URL, creating new game'); await showCreateGameInterface(); } } async function showCreateGameInterface() { const playerNameInput = prompt('Enter your name to create a new game:'); if (!playerNameInput || playerNameInput.trim().length === 0) { updateGameStatus('Name required to create game'); return; } multiplayerState.playerName = playerNameInput.trim(); updatePlayerInfo(multiplayerState.playerName); try { updateGameStatus('Creating game...'); console.log('Sending API request to create game for:', multiplayerState.playerName); const response = await fetch('/api/games', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerName: multiplayerState.playerName }) }); console.log('API response status:', response.status); const result = await response.json(); console.log('API response data:', result); if (result.success) { multiplayerState.gameId = result.gameId; multiplayerState.playerId = result.playerId; multiplayerState.playerNumber = result.playerNumber; multiplayerState.gameStatus = 'waiting'; // Add player 1 to grid addPlayerToGrid(result.playerNumber, result.spawnPosition.x, result.spawnPosition.y); // Store player 1 data for movement validation multiplayerState.players[result.playerNumber] = { id: result.playerId, name: multiplayerState.playerName, x: result.spawnPosition.x, y: result.spawnPosition.y }; // Show invite link showInviteLink(result.inviteLink); // Start polling for player 2 startGamePolling(); updateGameStatus('Waiting for player 2 to join...'); } else { throw new Error(result.error || 'Failed to create game'); } } catch (error) { console.error('Error creating game:', error); console.error('Error details:', error.stack); updateGameStatus('Failed to create game: ' + error.message); // Also show alert for debugging alert('Error creating game: ' + error.message + '\nCheck console for details.'); } } async function showJoinGameInterface() { const playerNameInput = prompt('Enter your name to join the game:'); if (!playerNameInput || playerNameInput.trim().length === 0) { updateGameStatus('Name required to join game'); return; } multiplayerState.playerName = playerNameInput.trim(); updatePlayerInfo(multiplayerState.playerName); try { updateGameStatus('Joining game...'); const response = await fetch(`/api/games/${multiplayerState.gameId}/join`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerName: multiplayerState.playerName }) }); const result = await response.json(); if (result.success) { multiplayerState.playerId = result.playerId; multiplayerState.playerNumber = result.playerNumber; multiplayerState.gameStatus = 'playing'; // Load full game state await loadGameState(); // Start polling for game updates startGamePolling(); updateGameStatus('Game started! Plan your moves.'); updateActionButtons(); // Enable buttons for Player 2 } else { throw new Error(result.error || 'Failed to join game'); } } catch (error) { console.error('Error joining game:', error); updateGameStatus('Failed to join game: ' + error.message); } } async function loadGameState() { try { const response = await fetch(`/api/games/${multiplayerState.gameId}`); const gameState = await response.json(); if (gameState.game && gameState.players) { // Update game info multiplayerState.currentTurn = gameState.game.current_turn; multiplayerState.gameStatus = gameState.game.status; // Clear existing players clearPlayersFromGrid(); // Add all players to grid gameState.players.forEach(player => { if (player.is_alive) { addPlayerToGrid(player.player_number, player.current_x, player.current_y); } else { // Show dead player showPlayerDeath(player.current_x, player.current_y); } multiplayerState.players[player.player_number] = { id: player.player_id, name: player.username, x: player.current_x, y: player.current_y, alive: Boolean(player.is_alive) }; }); renderGrid(); } } catch (error) { console.error('Error loading game state:', error); } } function showInviteLink(inviteLink) { console.log('showInviteLink called with:', inviteLink); // Create a temporary modal-like display const linkDisplay = document.createElement('div'); linkDisplay.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #2a2a2a; border: 2px solid #00ff00; border-radius: 8px; padding: 20px; z-index: 1000; max-width: 500px; color: white; text-align: center; `; linkDisplay.innerHTML = `

Game Created!

Share this link with your opponent:

`; // Add event listener for copy button const copyBtn = linkDisplay.querySelector('#copy-link-btn'); copyBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(inviteLink); copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy Link'; }, 2000); } catch (error) { console.error('Failed to copy link:', error); // Fallback: select the text for manual copying const input = linkDisplay.querySelector('input'); input.select(); input.setSelectionRange(0, 99999); // For mobile devices copyBtn.textContent = 'Selected - Press Ctrl+C'; } }); document.body.appendChild(linkDisplay); } function startGamePolling() { if (multiplayerState.pollTimer) { clearInterval(multiplayerState.pollTimer); } multiplayerState.pollTimer = setInterval(async () => { await pollGameStatus(); }, GAME_CONFIG.POLLING_INTERVAL); console.log('Started polling for game updates'); } async function pollGameStatus() { try { const response = await fetch(`/api/games/${multiplayerState.gameId}/status?playerId=${multiplayerState.playerId}`); const status = await response.json(); if (status.needsUpdate || status.game.current_turn !== multiplayerState.currentTurn) { await handleGameUpdate(status); } } catch (error) { console.error('Polling error:', error); } } async function handleGameUpdate(status) { const previousStatus = multiplayerState.gameStatus; const previousTurn = multiplayerState.currentTurn; // Update local state multiplayerState.gameStatus = status.game.status; multiplayerState.currentTurn = status.game.current_turn; multiplayerState.turnStatus = status.turnStatus; // Handle status changes if (previousStatus === 'waiting' && status.game.status === 'playing') { updateGameStatus('Player 2 joined! Game started.'); await loadGameState(); updateActionButtons(); // Re-enable buttons now that game is playing } if (previousTurn !== status.game.current_turn || (status.game.status === 'finished' && !multiplayerState.finalTurnAnimated)) { // New turn started OR game ended - show what happened in previous turn const animationTurn = status.game.status === 'finished' ? status.game.current_turn : previousTurn; if (status.game.status === 'finished') { multiplayerState.finalTurnAnimated = true; } await showTurnResults(status.recentEvents, animationTurn); // Only reset turn state and load if game is still playing if (status.game.status === 'playing') { resetTurnState(); await loadGameState(); updateGameStatus(`Turn ${status.game.current_turn} - Plan your moves`); } else { // Game ended - load final state but don't reset turn state await loadGameState(); } } // Update turn status display if (status.turnStatus === 'waiting_for_opponent') { updateGameStatus('Waiting for opponent to submit turn...'); } else if (status.turnStatus === 'executed') { updateGameStatus('Turn executed! Plan your next moves.'); } // Handle game end if (status.game.status === 'finished') { handleGameEnd(status.game); } } function handleGameEnd(game) { console.log('Game ended detected, but deferring end screen until animations complete'); clearInterval(multiplayerState.pollTimer); // Disable controls document.querySelectorAll('.move-btn, .shoot-btn').forEach(btn => { btn.disabled = true; }); submitButton.disabled = true; undoButton.disabled = true; // Store game end info but don't show screen yet - let animations finish multiplayerState.pendingGameEnd = { isWinner: game.winner_id === multiplayerState.playerId, game: game }; } function resetTurnState() { multiplayerState.turnActions = []; multiplayerState.movesRemaining = GAME_CONFIG.MOVES_PER_TURN; multiplayerState.shotAvailable = true; clearGhostPreviews(); updateMovesDisplay(); updateActionButtons(); } // API Integration Functions async function submitTurn() { if (multiplayerState.turnActions.length === 0) { updateGameStatus('No actions to submit'); return; } try { updateGameStatus('Submitting turn...'); const response = await fetch(`/api/games/${multiplayerState.gameId}/submit-turn`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerId: multiplayerState.playerId, actions: multiplayerState.turnActions }) }); const result = await response.json(); if (result.success) { if (result.bothPlayersSubmitted) { updateGameStatus('Both players submitted! Executing turn...'); } else { updateGameStatus('Turn submitted. Waiting for opponent...'); } // Disable controls until next turn document.querySelectorAll('.move-btn, .shoot-btn').forEach(btn => { btn.disabled = true; }); submitButton.disabled = true; } else { throw new Error(result.error || 'Failed to submit turn'); } } catch (error) { console.error('Error submitting turn:', error); updateGameStatus('Failed to submit turn: ' + error.message); } } // Grid Management Functions function initializeGrid() { multiplayerState.grid = []; for (let row = 0; row < GAME_CONFIG.GRID_HEIGHT; row++) { multiplayerState.grid[row] = []; for (let col = 0; col < GAME_CONFIG.GRID_WIDTH; col++) { multiplayerState.grid[row][col] = { type: 'empty', player: null, bullet: null }; } } } function addPlayerToGrid(playerNumber, x, y) { console.log(`Adding player ${playerNumber} to grid at position (${x}, ${y})`); if (!multiplayerState.grid || multiplayerState.grid.length === 0) { console.error('Grid not initialized when trying to add player'); return; } if (isValidPosition(x, y)) { multiplayerState.grid[y][x].player = playerNumber; console.log(`Player ${playerNumber} successfully added to grid`); } else { console.error(`Invalid position for player ${playerNumber}: (${x}, ${y})`); } } function clearPlayersFromGrid() { for (let row = 0; row < GAME_CONFIG.GRID_HEIGHT; row++) { for (let col = 0; col < GAME_CONFIG.GRID_WIDTH; col++) { multiplayerState.grid[row][col].player = null; } } } function renderGrid() { // Clear existing grid gameGrid.innerHTML = ''; // Create grid cells for (let row = 0; row < GAME_CONFIG.GRID_HEIGHT; row++) { for (let col = 0; col < GAME_CONFIG.GRID_WIDTH; col++) { const cell = createGridCell(row, col); gameGrid.appendChild(cell); } } } function createGridCell(row, col) { const cell = document.createElement('div'); cell.className = 'grid-cell'; cell.dataset.row = row; cell.dataset.col = col; // Add cell content based on game state const cellData = multiplayerState.grid[row][col]; if (cellData.player) { cell.classList.add(`player-${cellData.player}`); cell.textContent = cellData.player === 1 ? 'P1' : 'P2'; } if (cellData.bullet) { cell.classList.add('bullet'); cell.textContent = '•'; } return cell; } // Action Management (reuse existing functions with API integration) function setupEventListeners() { // Move buttons document.querySelectorAll('.move-btn').forEach(button => { button.addEventListener('click', function() { const direction = this.dataset.direction; addMoveAction(direction); }); }); // Shoot buttons document.querySelectorAll('.shoot-btn').forEach(button => { button.addEventListener('click', function() { const direction = this.dataset.direction; addShootAction(direction); }); }); // Submit turn button submitButton.addEventListener('click', function() { submitTurn(); }); // Undo button undoButton.addEventListener('click', function() { undoLastAction(); }); } function addMoveAction(direction) { if (multiplayerState.gameStatus !== 'playing') { updateGameStatus('Game not active'); return; } if (multiplayerState.movesRemaining <= 0) { flashErrorButton('move', direction); updateGameStatus('No moves remaining this turn'); return; } // Check if this move would be valid if (!isValidMoveAction(direction)) { flashErrorButton('move', direction); return; } const action = { type: 'move', direction: direction, timestamp: Date.now() }; multiplayerState.turnActions.push(action); multiplayerState.movesRemaining--; updateGhostPreviews(); updateMovesDisplay(); updateActionButtons(); console.log(`Added move action: ${direction}`); updateGameStatus(`Move added: ${direction.toUpperCase()}`); } function addShootAction(direction) { if (multiplayerState.gameStatus !== 'playing') { updateGameStatus('Game not active'); return; } if (!multiplayerState.shotAvailable) { flashErrorButton('shoot', direction); updateGameStatus('Shot already used this turn'); return; } const action = { type: 'shoot', direction: direction, timestamp: Date.now() }; multiplayerState.turnActions.push(action); multiplayerState.shotAvailable = false; updateGhostPreviews(); updateMovesDisplay(); updateActionButtons(); console.log(`Added shoot action: ${direction}`); updateGameStatus(`Shot added: ${direction.toUpperCase()}`); } // UI Update Functions function updateGameStatus(message) { statusText.textContent = message; console.log(`Status: ${message}`); } function updatePlayerInfo(name) { playerName.textContent = name; } function updateMovesDisplay() { const used = GAME_CONFIG.MOVES_PER_TURN - multiplayerState.movesRemaining; movesRemaining.textContent = `Moves: ${used}/${GAME_CONFIG.MOVES_PER_TURN}`; if (multiplayerState.shotAvailable) { shotAvailable.textContent = 'Shot: Available'; shotAvailable.classList.remove('used'); } else { shotAvailable.textContent = 'Shot: Used'; shotAvailable.classList.add('used'); } actionsQueued.textContent = `Actions: ${multiplayerState.turnActions.length}`; } function updateActionButtons() { const hasActions = multiplayerState.turnActions.length > 0; const gameActive = multiplayerState.gameStatus === 'playing'; submitButton.disabled = !hasActions || !gameActive; undoButton.disabled = !hasActions || !gameActive; // Enable/disable action buttons based on game state document.querySelectorAll('.move-btn, .shoot-btn').forEach(btn => { btn.disabled = !gameActive; }); } function undoLastAction() { if (multiplayerState.turnActions.length === 0) { updateGameStatus('No actions to undo'); return; } // Remove the last action const lastAction = multiplayerState.turnActions.pop(); // Restore the appropriate resource if (lastAction.type === 'move') { multiplayerState.movesRemaining++; } else if (lastAction.type === 'shoot') { multiplayerState.shotAvailable = true; } // Update visual elements updateGhostPreviews(); updateMovesDisplay(); updateActionButtons(); const actionType = lastAction.type.toUpperCase(); const direction = lastAction.direction.toUpperCase(); console.log(`Undid ${actionType} action: ${direction}`); updateGameStatus(`Undid: ${actionType} ${direction}`); } // Include all the existing validation and preview functions with minimal changes // (using multiplayerState instead of gameState) function isValidMoveAction(direction) { if (!multiplayerState.players[multiplayerState.playerNumber]) { return false; } let finalPos = calculateFinalPlayerPosition(); const offset = getDirectionOffset(direction); const newPos = { row: finalPos.y + offset.row, col: finalPos.x + offset.col }; if (!isValidPosition(newPos.col, newPos.row)) { updateGameStatus(`Cannot move ${direction.toUpperCase()} - out of bounds`); return false; } if (isPositionInPlayerPath(newPos)) { updateGameStatus(`Cannot move ${direction.toUpperCase()} - cannot move through yourself`); return false; } return true; } function calculateFinalPlayerPosition() { const currentPlayer = multiplayerState.players[multiplayerState.playerNumber]; if (!currentPlayer) return { x: 0, y: 0 }; let currentPos = { x: currentPlayer.x, y: currentPlayer.y }; multiplayerState.turnActions.forEach(action => { if (action.type === 'move') { const offset = getDirectionOffset(action.direction); const newPos = { x: currentPos.x + offset.col, y: currentPos.y + offset.row }; if (isValidPosition(newPos.x, newPos.y)) { currentPos = newPos; } } }); return currentPos; } function isPositionInPlayerPath(targetPos) { const playerPath = calculatePlayerPath(); return playerPath.some(pos => pos.x === targetPos.col && pos.y === targetPos.row ); } function calculatePlayerPath() { const path = []; const currentPlayer = multiplayerState.players[multiplayerState.playerNumber]; if (!currentPlayer) return path; let currentPos = { x: currentPlayer.x, y: currentPlayer.y }; path.push({ ...currentPos }); multiplayerState.turnActions.forEach(action => { if (action.type === 'move') { const offset = getDirectionOffset(action.direction); const newPos = { x: currentPos.x + offset.col, y: currentPos.y + offset.row }; if (isValidPosition(newPos.x, newPos.y)) { currentPos = newPos; path.push({ ...currentPos }); } } }); return path; } // Ghost preview functions (adapted for multiplayer state with sequential execution) function updateGhostPreviews() { clearGhostPreviews(); const currentPlayer = multiplayerState.players[multiplayerState.playerNumber]; if (!currentPlayer) return; let currentPos = { x: currentPlayer.x, y: currentPlayer.y }; // Process actions sequentially to show where shots will fire from multiplayerState.turnActions.forEach((action, index) => { if (action.type === 'move') { const offset = getDirectionOffset(action.direction); const newPos = { x: currentPos.x + offset.col, y: currentPos.y + offset.row }; if (isValidPosition(newPos.x, newPos.y)) { currentPos = newPos; addGhostPreview(newPos.y, newPos.x, 'ghost-move'); } } else if (action.type === 'shoot') { // Show bullet path from current position in the sequence showBulletPath(currentPos.y, currentPos.x, action.direction); } }); } function showBulletPath(startRow, startCol, direction) { const offset = getDirectionOffset(direction); let currentRow = startRow + offset.row; let currentCol = startCol + offset.col; while (isValidPosition(currentCol, currentRow)) { addGhostPreview(currentRow, currentCol, 'ghost-bullet'); currentRow += offset.row; currentCol += offset.col; } } function addGhostPreview(row, col, className) { const cell = getCellElement(row, col); if (cell && !cell.classList.contains('player-1') && !cell.classList.contains('player-2')) { cell.classList.add(className); multiplayerState.ghostPreviews.push({ row, col, className }); } } function clearGhostPreviews() { multiplayerState.ghostPreviews.forEach(preview => { const cell = getCellElement(preview.row, preview.col); if (cell) { cell.classList.remove(preview.className); } }); multiplayerState.ghostPreviews = []; } function getCellElement(row, col) { return document.querySelector(`[data-row="${row}"][data-col="${col}"]`); } function flashErrorButton(actionType, direction) { const buttonSelector = actionType === 'move' ? `.move-btn[data-direction="${direction}"]` : `.shoot-btn[data-direction="${direction}"]`; const button = document.querySelector(buttonSelector); if (button) { button.style.animation = 'error-flash 0.5s ease-out'; setTimeout(() => { button.style.animation = ''; }, 500); } if (actionType === 'move') { flashGhostPreviewsRed(); } } function flashGhostPreviewsRed() { const ghostCells = document.querySelectorAll('.ghost-move'); ghostCells.forEach(cell => { cell.style.animation = 'ghost-error-flash 0.6s ease-out'; setTimeout(() => { cell.style.animation = 'pulse 1.5s ease-in-out infinite'; }, 600); }); } // Utility functions function getDirectionOffset(direction) { const directionKey = direction.toUpperCase(); const offset = GAME_CONFIG.DIRECTION_OFFSETS[directionKey]; return offset || { row: 0, col: 0 }; } function isValidPosition(x, y) { return x >= 0 && x < GAME_CONFIG.GRID_WIDTH && y >= 0 && y < GAME_CONFIG.GRID_HEIGHT; } // Show visual feedback for what happened in the previous turn async function showTurnResults(recentEvents, turnNumber) { if (!recentEvents || recentEvents.length === 0) return; // Filter events from the executed turn const turnEvents = recentEvents.filter(event => event.turn_number === turnNumber); // Group events by action_index and organize by type const eventsByActionIndex = {}; turnEvents.forEach(event => { try { const eventData = JSON.parse(event.event_data); const actionIndex = eventData.action_index !== undefined ? eventData.action_index : 0; if (!eventsByActionIndex[actionIndex]) { eventsByActionIndex[actionIndex] = { movements: [], shots: [], hits: [] }; } if (event.event_type === 'player_moved') { eventsByActionIndex[actionIndex].movements.push({ ...event, eventData: eventData }); } else if (event.event_type === 'shot_fired') { eventsByActionIndex[actionIndex].shots.push({ ...event, eventData: eventData }); } else if (event.event_type === 'player_hit') { eventsByActionIndex[actionIndex].hits.push({ ...event, eventData: eventData }); } } catch (error) { console.error('Error parsing event data:', error); } }); // Execute animations in sequence by action index const actionIndices = Object.keys(eventsByActionIndex).sort((a, b) => parseInt(a) - parseInt(b)); for (const actionIndex of actionIndices) { const stepEvents = eventsByActionIndex[actionIndex]; console.log(`Animating action step ${parseInt(actionIndex) + 1}`); updateGameStatus(`Showing turn ${turnNumber} step ${parseInt(actionIndex) + 1}...`); // Step 1: Animate all player movements for this action step const movementPromises = stepEvents.movements.map(event => animatePlayerMovement( event.eventData.from_x, event.eventData.from_y, event.eventData.to_x, event.eventData.to_y, event.player_number ) ); await Promise.all(movementPromises); // Step 2: Fire all shots for this action step (simultaneously with movements) const shotPromises = stepEvents.shots.map(event => animateBulletTrail( event.eventData.bullet_path, event.eventData.shooter_x, event.eventData.shooter_y ) ); await Promise.all(shotPromises); // Step 3: Show hit effects stepEvents.hits.forEach(event => { highlightHit(event.eventData.target_x, event.eventData.target_y); showPlayerDeath(event.eventData.target_x, event.eventData.target_y); }); // Brief pause between action steps if (parseInt(actionIndex) < actionIndices.length - 1) { await new Promise(resolve => setTimeout(resolve, GAME_CONFIG.ACTION_STEP_DELAY)); } } // Check if game ended during this turn and show end screen after animations if (multiplayerState.pendingGameEnd) { console.log('Animations complete, showing game end screen after delay'); updateGameStatus('Battle complete! Determining winner...'); // Wait 2 seconds to let players absorb what happened, then show end screen setTimeout(() => { showGameEndScreen( multiplayerState.pendingGameEnd.isWinner, multiplayerState.pendingGameEnd.game ); multiplayerState.pendingGameEnd = null; }, 2000); } else { updateGameStatus(`Turn ${turnNumber} complete - Plan your next moves`); } } // Animate player movement from one position to another async function animatePlayerMovement(fromX, fromY, toX, toY, playerNumber) { console.log(`Animating player ${playerNumber} from (${fromX},${fromY}) to (${toX},${toY})`); // Clear previous position const fromCell = getCellElement(fromY, fromX); if (fromCell) { fromCell.classList.remove(`player-${playerNumber}`); fromCell.textContent = ''; } // Set new position with movement effect const toCell = getCellElement(toY, toX); if (toCell) { // Add temporary movement highlighting toCell.classList.add('player-moving'); // Brief delay to show movement await new Promise(resolve => setTimeout(resolve, GAME_CONFIG.MOVEMENT_ANIMATION_DELAY)); // Remove movement effect and set final player position toCell.classList.remove('player-moving'); toCell.classList.add(`player-${playerNumber}`); toCell.textContent = playerNumber === 1 ? 'P1' : 'P2'; console.log(`Player ${playerNumber} moved to (${toX},${toY})`); } } // Animate bullet trail visualization async function animateBulletTrail(bulletPath, shooterX, shooterY) { if (!bulletPath || bulletPath.length === 0) return; // Highlight each cell in the bullet path with a small delay for (let i = 0; i < bulletPath.length; i++) { const pathCell = bulletPath[i]; const cell = getCellElement(pathCell.y, pathCell.x); if (cell && !cell.classList.contains('player-1') && !cell.classList.contains('player-2')) { cell.classList.add('bullet-trail'); // Remove the bullet trail class after animation completes setTimeout(() => { cell.classList.remove('bullet-trail'); }, 5000); } // Small delay between bullet trail segments for visual effect if (i < bulletPath.length - 1) { await new Promise(resolve => setTimeout(resolve, GAME_CONFIG.BULLET_SEGMENT_DELAY)); } } } // Highlight where a player was hit function highlightHit(x, y) { const cell = getCellElement(y, x); if (cell) { cell.classList.add('bullet-hit'); // Remove hit highlight after animation setTimeout(() => { cell.classList.remove('bullet-hit'); }, 1000); } } // Show death effect for a player function showPlayerDeath(x, y) { const cell = getCellElement(y, x); if (cell) { // Remove player class and add death effect cell.classList.remove('player-1', 'player-2'); cell.classList.add('player-dead'); // The death effect will persist to show where the player died console.log(`Player death effect shown at (${x},${y})`); } } // Show game end screen function showGameEndScreen(isWinner, game) { // Create overlay const overlay = document.createElement('div'); overlay.className = 'game-end-overlay'; // Create modal const modal = document.createElement('div'); modal.className = `game-end-modal ${isWinner ? 'winner' : 'loser'}`; // Create content const title = document.createElement('div'); title.className = 'game-end-title'; title.textContent = isWinner ? 'YOU WIN!' : 'GAME OVER'; const message = document.createElement('div'); message.className = 'game-end-message'; message.textContent = isWinner ? 'Congratulations! You defeated your opponent!' : 'Better luck next time! You were eliminated.'; // Create buttons const buttonContainer = document.createElement('div'); buttonContainer.style.marginTop = '20px'; const playAgainBtn = document.createElement('button'); playAgainBtn.className = 'game-end-button primary'; playAgainBtn.textContent = 'Play Again'; playAgainBtn.onclick = async () => { await startRematch(); }; const newGameBtn = document.createElement('button'); newGameBtn.className = 'game-end-button'; newGameBtn.textContent = 'New Game'; newGameBtn.onclick = () => { window.location.href = '/'; }; const replayBtn = document.createElement('button'); replayBtn.className = 'game-end-button'; replayBtn.textContent = 'View Replay'; replayBtn.onclick = () => { // Future feature - for now just show an alert alert('Replay feature coming soon!'); }; buttonContainer.appendChild(playAgainBtn); buttonContainer.appendChild(newGameBtn); buttonContainer.appendChild(replayBtn); // Assemble modal modal.appendChild(title); modal.appendChild(message); modal.appendChild(buttonContainer); // Assemble overlay overlay.appendChild(modal); // Add to page document.body.appendChild(overlay); // Update status updateGameStatus(isWinner ? 'Victory!' : 'Defeated!'); } // Start a rematch with the same players async function startRematch() { try { updateGameStatus('Creating rematch...'); // Remove game end overlay const overlay = document.querySelector('.game-end-overlay'); if (overlay) { overlay.remove(); } const response = await fetch(`/api/games/${multiplayerState.gameId}/rematch`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playerId: multiplayerState.playerId, playerName: multiplayerState.playerName }) }); const result = await response.json(); if (result.success) { // Stop old polling first if (multiplayerState.pollTimer) { clearInterval(multiplayerState.pollTimer); multiplayerState.pollTimer = null; } // Reset local state for new game multiplayerState.gameId = result.newGameId; multiplayerState.playerId = result.playerId; multiplayerState.playerNumber = result.playerNumber; multiplayerState.gameStatus = result.status; multiplayerState.currentTurn = 1; multiplayerState.turnStatus = 'waiting_for_submission'; multiplayerState.pendingGameEnd = null; multiplayerState.finalTurnAnimated = false; // Clear old game state resetTurnState(); clearPlayersFromGrid(); // Initialize grid fresh initializeGrid(); renderGrid(); // Add player to new spawn position if (result.spawnPosition) { addPlayerToGrid(result.playerNumber, result.spawnPosition.x, result.spawnPosition.y); multiplayerState.players[result.playerNumber] = { id: result.playerId, name: multiplayerState.playerName, x: result.spawnPosition.x, y: result.spawnPosition.y, alive: true }; } // Load new game state await loadGameState(); // Restart polling for the new game startGamePolling(); // Update URL without page reload window.history.pushState({}, '', `/game/${result.newGameId}`); if (result.status === 'waiting') { updateGameStatus('Waiting for opponent to join rematch...'); showRematchInvite(result.inviteLink); } else { updateGameStatus('Rematch started! Plan your moves.'); updateActionButtons(); } } else { throw new Error(result.error || 'Failed to create rematch'); } } catch (error) { console.error('Error starting rematch:', error); updateGameStatus('Failed to start rematch: ' + error.message); } } // Show rematch invite link function showRematchInvite(inviteLink) { const linkDisplay = document.createElement('div'); linkDisplay.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #2a2a2a; border: 2px solid #00ff00; border-radius: 8px; padding: 20px; z-index: 1000; max-width: 500px; color: white; text-align: center; `; linkDisplay.innerHTML = `

Rematch Created!

Share this link with your opponent for the rematch:

`; // Add event listener for copy button const copyBtn = linkDisplay.querySelector('#copy-rematch-link-btn'); copyBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(inviteLink); copyBtn.textContent = 'Copied!'; setTimeout(() => { copyBtn.textContent = 'Copy Link'; }, 2000); } catch (error) { console.error('Failed to copy link:', error); // Fallback: select the text for manual copying const input = linkDisplay.querySelector('input'); input.select(); input.setSelectionRange(0, 99999); // For mobile devices copyBtn.textContent = 'Selected - Press Ctrl+C'; } }); document.body.appendChild(linkDisplay); }