491 lines
No EOL
14 KiB
JavaScript
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;
|
|
} |