Welcome to the AI slop.
This commit is contained in:
commit
adc01bb99c
1925 changed files with 238364 additions and 0 deletions
491
public/game.js
Normal file
491
public/game.js
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
// 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;
|
||||
}
|
||||
75
public/index.html
Normal file
75
public/index.html
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Grid Battle Game</title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<header class="game-header">
|
||||
<h1>Grid Battle</h1>
|
||||
<div class="game-status">
|
||||
<span id="status-text">Initializing...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="game-board-container">
|
||||
<div id="game-grid" class="game-grid">
|
||||
<!-- Grid cells will be generated by JavaScript -->
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="game-controls">
|
||||
<div class="player-info">
|
||||
<h3>Player Info</h3>
|
||||
<div id="player-name">Not connected</div>
|
||||
</div>
|
||||
|
||||
<div class="turn-controls">
|
||||
<h3>Movement</h3>
|
||||
<div class="compass-grid move-compass">
|
||||
<div class="compass-empty"></div>
|
||||
<button class="move-btn compass-north" data-direction="north">↑</button>
|
||||
<div class="compass-empty"></div>
|
||||
<button class="move-btn compass-west" data-direction="west">←</button>
|
||||
<div class="compass-center">MOVE</div>
|
||||
<button class="move-btn compass-east" data-direction="east">→</button>
|
||||
<div class="compass-empty"></div>
|
||||
<button class="move-btn compass-south" data-direction="south">↓</button>
|
||||
<div class="compass-empty"></div>
|
||||
</div>
|
||||
|
||||
<h3>Shooting</h3>
|
||||
<div class="compass-grid shoot-compass">
|
||||
<div class="compass-empty"></div>
|
||||
<button class="shoot-btn compass-north" data-direction="north">🔫</button>
|
||||
<div class="compass-empty"></div>
|
||||
<button class="shoot-btn compass-west" data-direction="west">🔫</button>
|
||||
<div class="compass-center">SHOOT</div>
|
||||
<button class="shoot-btn compass-east" data-direction="east">🔫</button>
|
||||
<div class="compass-empty"></div>
|
||||
<button class="shoot-btn compass-south" data-direction="south">🔫</button>
|
||||
<div class="compass-empty"></div>
|
||||
</div>
|
||||
|
||||
<div class="turn-management">
|
||||
<button id="undo-action" class="undo-btn" disabled>↶ Undo Last</button>
|
||||
<button id="submit-turn" class="submit-btn" disabled>Submit Turn</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-info">
|
||||
<h3>Game Info</h3>
|
||||
<div id="moves-remaining">Moves: 0/4</div>
|
||||
<div id="shot-available">Shot: Available</div>
|
||||
<div id="actions-queued">Actions: 0</div>
|
||||
<div id="turn-timer">Turn Timer: --</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<script src="/multiplayer-game.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1286
public/multiplayer-game.js
Normal file
1286
public/multiplayer-game.js
Normal file
File diff suppressed because it is too large
Load diff
582
public/style.css
Normal file
582
public/style.css
Normal file
|
|
@ -0,0 +1,582 @@
|
|||
/* Reset and base styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Courier New', monospace;
|
||||
background-color: #1a1a1a;
|
||||
color: #ffffff;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Main game container */
|
||||
.game-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 300px;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100vh;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.game-header {
|
||||
grid-column: 1 / -1;
|
||||
background-color: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
color: #00ff00;
|
||||
font-size: 24px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.game-status {
|
||||
background-color: #333;
|
||||
padding: 8px 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
/* Game board container */
|
||||
.game-board-container {
|
||||
background-color: #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Grid styling */
|
||||
.game-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 30px);
|
||||
grid-template-rows: repeat(10, 30px);
|
||||
gap: 1px;
|
||||
background-color: #555;
|
||||
border: 2px solid #777;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
background-color: #333;
|
||||
border: 1px solid #444;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.grid-cell:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
/* Player styling */
|
||||
.grid-cell.player-1 {
|
||||
background-color: #0066ff;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.grid-cell.player-2 {
|
||||
background-color: #ff6600;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Ghost preview styling */
|
||||
.grid-cell.ghost-move {
|
||||
background-color: rgba(0, 255, 0, 0.3);
|
||||
border: 2px dashed #00ff00;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.grid-cell.ghost-bullet {
|
||||
background-color: rgba(255, 255, 0, 0.4);
|
||||
border: 1px dashed #ffff00;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell.ghost-bullet::after {
|
||||
content: '•';
|
||||
color: #ffff00;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.grid-cell.bullet {
|
||||
background-color: #ffff00;
|
||||
}
|
||||
|
||||
.grid-cell.explosion {
|
||||
background-color: #ff0000;
|
||||
animation: explosion 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.6;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes error-flash {
|
||||
0%, 100% {
|
||||
background-color: #444;
|
||||
border-color: #666;
|
||||
}
|
||||
50% {
|
||||
background-color: #cc0000;
|
||||
border-color: #ff0000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ghost-error-flash {
|
||||
0%, 100% {
|
||||
background-color: rgba(0, 255, 0, 0.3);
|
||||
border-color: #00ff00;
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(255, 0, 0, 0.8);
|
||||
border-color: #ff0000;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes explosion {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.5); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* Bullet trail visualization */
|
||||
.grid-cell.bullet-trail {
|
||||
background-color: rgba(255, 255, 0, 0.8);
|
||||
border: 1px solid #ffff00;
|
||||
animation: bullet-fade 5s ease-out forwards;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell.bullet-trail::after {
|
||||
content: '•';
|
||||
color: #ffff00;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.grid-cell.bullet-hit {
|
||||
background-color: rgba(255, 0, 0, 0.9);
|
||||
border: 2px solid #ff0000;
|
||||
animation: hit-flash 1s ease-out forwards;
|
||||
}
|
||||
|
||||
.grid-cell.bullet-hit::after {
|
||||
content: '💥';
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
@keyframes bullet-fade {
|
||||
0% {
|
||||
opacity: 1;
|
||||
background-color: rgba(255, 255, 0, 0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
background-color: rgba(255, 255, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hit-flash {
|
||||
0% {
|
||||
opacity: 1;
|
||||
background-color: rgba(255, 0, 0, 0.9);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
background-color: rgba(255, 0, 0, 0);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Player movement effect */
|
||||
.grid-cell.player-moving {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border: 2px solid #ffffff;
|
||||
animation: move-highlight 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* Player death effect */
|
||||
.grid-cell.player-dead {
|
||||
background-color: rgba(139, 0, 0, 0.8) !important;
|
||||
border: 2px solid #8b0000 !important;
|
||||
animation: death-effect 2s ease-out forwards;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.grid-cell.player-dead::after {
|
||||
content: '💀';
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes move-highlight {
|
||||
0% {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes death-effect {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* Game end overlay */
|
||||
.game-end-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
animation: overlay-appear 0.5s ease-out;
|
||||
}
|
||||
|
||||
.game-end-modal {
|
||||
background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
|
||||
border: 3px solid;
|
||||
border-radius: 15px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.8);
|
||||
min-width: 400px;
|
||||
animation: modal-appear 0.5s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.game-end-modal.winner {
|
||||
border-color: #00ff00;
|
||||
background: linear-gradient(135deg, #004d00, #002200);
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.game-end-modal.loser {
|
||||
border-color: #ff0000;
|
||||
background: linear-gradient(135deg, #4d0000, #220000);
|
||||
color: #ff0000;
|
||||
}
|
||||
|
||||
.game-end-title {
|
||||
font-size: 48px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
text-shadow: 0 0 10px currentColor;
|
||||
}
|
||||
|
||||
.game-end-message {
|
||||
font-size: 20px;
|
||||
margin-bottom: 30px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.game-end-button {
|
||||
background: #444;
|
||||
color: white;
|
||||
border: 2px solid #666;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin: 0 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.game-end-button:hover {
|
||||
background: #555;
|
||||
border-color: #888;
|
||||
}
|
||||
|
||||
.game-end-button.primary {
|
||||
background: #006600;
|
||||
border-color: #00aa00;
|
||||
}
|
||||
|
||||
.game-end-button.primary:hover {
|
||||
background: #008800;
|
||||
border-color: #00cc00;
|
||||
}
|
||||
|
||||
.game-end-modal.winner .game-end-button {
|
||||
border-color: #00ff00;
|
||||
}
|
||||
|
||||
.game-end-modal.loser .game-end-button {
|
||||
border-color: #ff0000;
|
||||
}
|
||||
|
||||
@keyframes overlay-appear {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes modal-appear {
|
||||
from {
|
||||
transform: scale(0.7) translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Controls sidebar */
|
||||
.game-controls {
|
||||
background-color: #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.game-controls h3 {
|
||||
color: #00ff00;
|
||||
margin-bottom: 10px;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Player info */
|
||||
.player-info {
|
||||
background-color: #333;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
/* Compass layout for action buttons */
|
||||
.compass-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
background-color: #333;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.compass-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #444;
|
||||
border: 1px solid #666;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.move-compass .compass-center {
|
||||
border-color: #0066ff;
|
||||
}
|
||||
|
||||
.shoot-compass .compass-center {
|
||||
border-color: #ff6600;
|
||||
}
|
||||
|
||||
.compass-empty {
|
||||
/* Empty grid cells for spacing */
|
||||
}
|
||||
|
||||
.compass-north, .compass-south, .compass-east, .compass-west {
|
||||
padding: 12px 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.move-btn, .shoot-btn, .submit-btn, .undo-btn {
|
||||
background-color: #444;
|
||||
color: #ffffff;
|
||||
border: 1px solid #666;
|
||||
padding: 10px 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.move-btn:hover {
|
||||
background-color: #0066ff;
|
||||
border-color: #0088ff;
|
||||
}
|
||||
|
||||
.shoot-btn:hover {
|
||||
background-color: #ff6600;
|
||||
border-color: #ff8800;
|
||||
}
|
||||
|
||||
.turn-controls h3 {
|
||||
margin-bottom: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.turn-controls h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.turn-management {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.undo-btn {
|
||||
background-color: #cc6600;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.undo-btn:hover:not(:disabled) {
|
||||
background-color: #ff8800;
|
||||
}
|
||||
|
||||
.undo-btn:disabled {
|
||||
background-color: #333;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #006600;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
background-color: #008800;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background-color: #333;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Game info */
|
||||
.game-info {
|
||||
background-color: #333;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.game-info div {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#shot-available {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
#shot-available.used {
|
||||
color: #ff6600;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 1024px) {
|
||||
.game-container {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
order: 2;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.game-board-container {
|
||||
order: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.game-grid {
|
||||
grid-template-columns: repeat(10, 20px);
|
||||
grid-template-rows: repeat(10, 20px);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue