monquigrid/public/game.js
2025-07-16 23:56:37 +00:00

491 lines
No EOL
14 KiB
JavaScript

// 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;
}