// Game configuration - matches server-side config const GAME_CONFIG = { GRID_WIDTH: 20, GRID_HEIGHT: 20, MOVES_PER_TURN: 4, SHOTS_PER_TURN: 1, CELL_SIZE_PX: 30 }; // Game state let gameState = { grid: [], players: {}, currentPlayer: 1, // For now, assume we're player 1 gamePhase: 'waiting', // waiting, playing, game_over turnActions: [], movesRemaining: GAME_CONFIG.MOVES_PER_TURN, shotAvailable: true, ghostPreviews: [] // Track ghost preview positions }; // 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 game when page loads document.addEventListener('DOMContentLoaded', function() { initializeGame(); setupEventListeners(); }); function initializeGame() { console.log('Initializing Grid Battle Game...'); // Initialize empty grid initializeGrid(); // Render the grid renderGrid(); // Add test players for visualization addTestPlayers(); // Update UI updateGameStatus('Game initialized - Ready for players'); updatePlayerInfo('Test Player'); updateMovesDisplay(); updateActionButtons(); } function initializeGrid() { gameState.grid = []; for (let row = 0; row < GAME_CONFIG.GRID_HEIGHT; row++) { gameState.grid[row] = []; for (let col = 0; col < GAME_CONFIG.GRID_WIDTH; col++) { gameState.grid[row][col] = { type: 'empty', player: null, bullet: 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 = gameState.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; } function addTestPlayers() { // Add player 1 at position (2, 2) gameState.grid[2][2].player = 1; gameState.players[1] = { row: 2, col: 2, name: 'Player 1' }; // Add player 2 at position (17, 17) gameState.grid[17][17].player = 2; gameState.players[2] = { row: 17, col: 17, name: 'Player 2' }; // Re-render to show players renderGrid(); } 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 (gameState.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() }; gameState.turnActions.push(action); gameState.movesRemaining--; updateGhostPreviews(); updateMovesDisplay(); updateActionButtons(); console.log(`Added move action: ${direction}`); updateGameStatus(`Move added: ${direction.toUpperCase()}`); } function addShootAction(direction) { if (!gameState.shotAvailable) { flashErrorButton('shoot', direction); updateGameStatus('Shot already used this turn'); return; } const action = { type: 'shoot', direction: direction, timestamp: Date.now() }; gameState.turnActions.push(action); gameState.shotAvailable = false; updateGhostPreviews(); updateMovesDisplay(); updateActionButtons(); console.log(`Added shoot action: ${direction}`); updateGameStatus(`Shot added: ${direction.toUpperCase()}`); } function submitTurn() { if (gameState.turnActions.length === 0) { updateGameStatus('No actions to submit'); return; } console.log('Submitting turn with actions:', gameState.turnActions); // TODO: Send actions to server via WebSocket // For now, just reset the turn locally gameState.turnActions = []; gameState.movesRemaining = GAME_CONFIG.MOVES_PER_TURN; gameState.shotAvailable = true; clearGhostPreviews(); updateMovesDisplay(); updateActionButtons(); updateGameStatus('Turn submitted - waiting for opponent'); } 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 - gameState.movesRemaining; movesRemaining.textContent = `Moves: ${used}/${GAME_CONFIG.MOVES_PER_TURN}`; if (gameState.shotAvailable) { shotAvailable.textContent = 'Shot: Available'; shotAvailable.classList.remove('used'); } else { shotAvailable.textContent = 'Shot: Used'; shotAvailable.classList.add('used'); } actionsQueued.textContent = `Actions: ${gameState.turnActions.length}`; } function undoLastAction() { if (gameState.turnActions.length === 0) { updateGameStatus('No actions to undo'); return; } // Remove the last action const lastAction = gameState.turnActions.pop(); // Restore the appropriate resource if (lastAction.type === 'move') { gameState.movesRemaining++; } else if (lastAction.type === 'shoot') { gameState.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}`); } function updateActionButtons() { const hasActions = gameState.turnActions.length > 0; submitButton.disabled = !hasActions; undoButton.disabled = !hasActions; } // Ghost preview functions function updateGhostPreviews() { clearGhostPreviews(); if (!gameState.players[gameState.currentPlayer]) { return; } // Calculate the projected player position after all moves let currentPos = { row: gameState.players[gameState.currentPlayer].row, col: gameState.players[gameState.currentPlayer].col }; // Simulate each action to show the ghost trail gameState.turnActions.forEach((action, index) => { if (action.type === 'move') { const offset = getDirectionOffset(action.direction); const newPos = { row: currentPos.row + offset.row, col: currentPos.col + offset.col }; if (isValidPosition(newPos.row, newPos.col)) { currentPos = newPos; addGhostPreview(newPos.row, newPos.col, 'ghost-move'); } } else if (action.type === 'shoot') { // Show bullet path from current position showBulletPath(currentPos.row, currentPos.col, action.direction); } }); } function showBulletPath(startRow, startCol, direction) { const offset = getDirectionOffset(direction); let currentRow = startRow + offset.row; let currentCol = startCol + offset.col; // Show bullet path until it hits a wall or goes off-grid while (isValidPosition(currentRow, currentCol)) { 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); gameState.ghostPreviews.push({ row, col, className }); } } function clearGhostPreviews() { gameState.ghostPreviews.forEach(preview => { const cell = getCellElement(preview.row, preview.col); if (cell) { cell.classList.remove(preview.className); } }); gameState.ghostPreviews = []; } function getCellElement(row, col) { return document.querySelector(`[data-row="${row}"][data-col="${col}"]`); } // Movement validation functions function isValidMoveAction(direction) { if (!gameState.players[gameState.currentPlayer]) { return false; } // Calculate final position after all queued moves let finalPos = calculateFinalPlayerPosition(); // Get the new position for this move const offset = getDirectionOffset(direction); const newPos = { row: finalPos.row + offset.row, col: finalPos.col + offset.col }; // Check if move is within grid bounds if (!isValidPosition(newPos.row, newPos.col)) { updateGameStatus(`Cannot move ${direction.toUpperCase()} - out of bounds`); return false; } // Check if move would go through the player's own path if (isPositionInPlayerPath(newPos)) { updateGameStatus(`Cannot move ${direction.toUpperCase()} - cannot move through yourself`); return false; } return true; } function calculateFinalPlayerPosition() { let currentPos = { row: gameState.players[gameState.currentPlayer].row, col: gameState.players[gameState.currentPlayer].col }; // Apply all queued move actions gameState.turnActions.forEach(action => { if (action.type === 'move') { const offset = getDirectionOffset(action.direction); const newPos = { row: currentPos.row + offset.row, col: currentPos.col + offset.col }; if (isValidPosition(newPos.row, newPos.col)) { currentPos = newPos; } } }); return currentPos; } function isPositionInPlayerPath(targetPos) { // Get all positions the player will occupy during their turn const playerPath = calculatePlayerPath(); // Check if target position conflicts with any position in the path return playerPath.some(pos => pos.row === targetPos.row && pos.col === targetPos.col ); } function calculatePlayerPath() { const path = []; let currentPos = { row: gameState.players[gameState.currentPlayer].row, col: gameState.players[gameState.currentPlayer].col }; // Add starting position path.push({ ...currentPos }); // Apply all queued move actions and track each position gameState.turnActions.forEach(action => { if (action.type === 'move') { const offset = getDirectionOffset(action.direction); const newPos = { row: currentPos.row + offset.row, col: currentPos.col + offset.col }; if (isValidPosition(newPos.row, newPos.col)) { currentPos = newPos; path.push({ ...currentPos }); } } }); return path; } function flashErrorButton(actionType, direction) { // Flash the button 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); } // Flash ghost previews red for move actions if (actionType === 'move') { flashGhostPreviewsRed(); } } function flashGhostPreviewsRed() { // Find all current ghost move cells const ghostCells = document.querySelectorAll('.ghost-move'); ghostCells.forEach(cell => { // Apply red flash animation cell.style.animation = 'ghost-error-flash 0.6s ease-out'; // Reset animation after completion setTimeout(() => { cell.style.animation = 'pulse 1.5s ease-in-out infinite'; }, 600); }); } // Utility functions function getDirectionOffset(direction) { const offsets = { 'north': { row: -1, col: 0 }, 'south': { row: 1, col: 0 }, 'east': { row: 0, col: 1 }, 'west': { row: 0, col: -1 } }; return offsets[direction] || { row: 0, col: 0 }; } function isValidPosition(row, col) { return row >= 0 && row < GAME_CONFIG.GRID_HEIGHT && col >= 0 && col < GAME_CONFIG.GRID_WIDTH; }