Add multi-player support (2-4 players) and AI bots
This commit is contained in:
121
public/ai-bot.js
Normal file
121
public/ai-bot.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* AI Bot for Hexo game
|
||||||
|
* Controls computer-controlled players
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CELL_TYPES } from './map.js';
|
||||||
|
|
||||||
|
export class AIBot {
|
||||||
|
constructor(playerId, map, gameUI) {
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.map = map;
|
||||||
|
this.gameUI = gameUI;
|
||||||
|
this.thinkingTime = 1000; // ms delay between moves
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute AI turn
|
||||||
|
*/
|
||||||
|
async playTurn() {
|
||||||
|
// Get all player cells
|
||||||
|
const playerCells = this.map.getPlayerCells(this.playerId);
|
||||||
|
|
||||||
|
if (playerCells.length === 0) return;
|
||||||
|
|
||||||
|
// Find all possible moves
|
||||||
|
const moves = this.findPossibleMoves(playerCells);
|
||||||
|
|
||||||
|
if (moves.length === 0) {
|
||||||
|
// No moves available, end turn
|
||||||
|
this.gameUI.endTurn();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort moves by priority (attack > expand > reinforce)
|
||||||
|
moves.sort((a, b) => this.movePriority(b) - this.movePriority(a));
|
||||||
|
|
||||||
|
// Execute best move
|
||||||
|
const bestMove = moves[0];
|
||||||
|
|
||||||
|
// Wait for thinking time
|
||||||
|
await this.wait(this.thinkingTime);
|
||||||
|
|
||||||
|
// Execute the move
|
||||||
|
this.executeMove(bestMove);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all possible moves for AI
|
||||||
|
*/
|
||||||
|
findPossibleMoves(playerCells) {
|
||||||
|
const moves = [];
|
||||||
|
|
||||||
|
for (const cell of playerCells) {
|
||||||
|
if (cell.getStrength() <= 1) continue;
|
||||||
|
|
||||||
|
const neighbors = this.map.getNeighbors(cell.q, cell.r);
|
||||||
|
|
||||||
|
for (const neighbor of neighbors) {
|
||||||
|
// Skip own cells
|
||||||
|
if (neighbor.getOwner() === this.playerId) continue;
|
||||||
|
|
||||||
|
const attackStrength = cell.getStrength() - 1;
|
||||||
|
const defenseStrength = neighbor.getStrength();
|
||||||
|
|
||||||
|
moves.push({
|
||||||
|
from: cell,
|
||||||
|
to: neighbor,
|
||||||
|
attackStrength,
|
||||||
|
defenseStrength,
|
||||||
|
type: neighbor.type === CELL_TYPES.EMPTY ? 'expand' : 'attack'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return moves;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate move priority (higher = better)
|
||||||
|
*/
|
||||||
|
movePriority(move) {
|
||||||
|
let priority = 0;
|
||||||
|
|
||||||
|
// Prefer attacks on weak enemies
|
||||||
|
if (move.type === 'attack') {
|
||||||
|
if (move.attackStrength > move.defenseStrength) {
|
||||||
|
priority += 100; // Likely to win
|
||||||
|
priority += move.attackStrength - move.defenseStrength;
|
||||||
|
} else {
|
||||||
|
priority -= 50; // Risky attack
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer expanding to empty cells
|
||||||
|
if (move.type === 'expand') {
|
||||||
|
priority += 50;
|
||||||
|
priority += move.attackStrength; // Stronger placement = better
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer moves that create strong positions
|
||||||
|
priority += move.attackStrength * 0.5;
|
||||||
|
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a move
|
||||||
|
*/
|
||||||
|
executeMove(move) {
|
||||||
|
this.gameUI.selectedCell = move.from;
|
||||||
|
this.gameUI.currentTarget = move.to;
|
||||||
|
this.gameUI.executeAttack();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for specified time
|
||||||
|
*/
|
||||||
|
wait(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
280
public/game.js
280
public/game.js
@@ -1,20 +1,32 @@
|
|||||||
/**
|
/**
|
||||||
* Hexo Game UI - Canvas Rendering and Interactions
|
* Hexo Game UI - Canvas Rendering and Interactions
|
||||||
|
* Supports 2-4 players with AI bots
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { HexMap, CELL_TYPES } from './map.js';
|
import { HexMap, CELL_TYPES } from './map.js';
|
||||||
|
import { AIBot } from './ai-bot.js';
|
||||||
|
|
||||||
// Game constants
|
// Game constants
|
||||||
const HEX_SIZE = 18;
|
const HEX_SIZE = 18;
|
||||||
const MAP_SIZE = 20;
|
const MAP_SIZE = 20;
|
||||||
const ANIMATION_DURATION = 300;
|
const ANIMATION_DURATION = 300;
|
||||||
|
|
||||||
|
// Player colors
|
||||||
|
const PLAYER_COLORS = {
|
||||||
|
1: '#4ecca3',
|
||||||
|
2: '#e94560',
|
||||||
|
3: '#f9ed69',
|
||||||
|
4: '#a8e6cf'
|
||||||
|
};
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
blocked: '#2a2a4a',
|
blocked: '#2a2a4a',
|
||||||
empty: '#3a5a6a',
|
empty: '#3a5a6a',
|
||||||
player1: '#4ecca3',
|
player1: '#4ecca3',
|
||||||
player2: '#e94560',
|
player2: '#e94560',
|
||||||
|
player3: '#f9ed69',
|
||||||
|
player4: '#a8e6cf',
|
||||||
highlight: 'rgba(255, 255, 255, 0.3)',
|
highlight: 'rgba(255, 255, 255, 0.3)',
|
||||||
selected: 'rgba(233, 69, 96, 0.6)',
|
selected: 'rgba(233, 69, 96, 0.6)',
|
||||||
target: 'rgba(78, 204, 163, 0.5)',
|
target: 'rgba(78, 204, 163, 0.5)',
|
||||||
@@ -36,8 +48,12 @@ class GameUI {
|
|||||||
this.selectedCell = null;
|
this.selectedCell = null;
|
||||||
this.currentTarget = null;
|
this.currentTarget = null;
|
||||||
this.currentPlayer = 1;
|
this.currentPlayer = 1;
|
||||||
this.gamePhase = 'movement'; // movement, supply, gameover
|
this.playerCount = 2;
|
||||||
|
this.playerTypes = {}; // 1: 'human', 2: 'ai', etc.
|
||||||
|
this.aiBots = {};
|
||||||
|
this.gamePhase = 'movement';
|
||||||
this.hasMoved = false;
|
this.hasMoved = false;
|
||||||
|
this.isAIThinking = false;
|
||||||
|
|
||||||
this.offsetX = 0;
|
this.offsetX = 0;
|
||||||
this.offsetY = 0;
|
this.offsetY = 0;
|
||||||
@@ -46,48 +62,129 @@ class GameUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
this.setupStartScreen();
|
||||||
this.setupEventListeners();
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupStartScreen() {
|
||||||
|
const playerCountSelect = document.getElementById('player-count');
|
||||||
|
const playerTypeRows = document.querySelectorAll('.player-type-row');
|
||||||
|
|
||||||
|
playerCountSelect.addEventListener('change', (e) => {
|
||||||
|
const count = parseInt(e.target.value);
|
||||||
|
playerTypeRows.forEach((row, index) => {
|
||||||
|
row.style.display = index < count ? 'flex' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('start-game-btn').addEventListener('click', () => {
|
||||||
|
this.startGame();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('back-menu-btn').addEventListener('click', () => {
|
||||||
|
this.showStartScreen();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showStartScreen() {
|
||||||
|
document.getElementById('start-screen').style.display = 'flex';
|
||||||
|
document.getElementById('game-screen').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
startGame() {
|
||||||
|
// Get settings
|
||||||
|
this.playerCount = parseInt(document.getElementById('player-count').value);
|
||||||
|
|
||||||
|
for (let i = 1; i <= this.playerCount; i++) {
|
||||||
|
const typeSelect = document.getElementById(`player${i}-type`);
|
||||||
|
this.playerTypes[i] = typeSelect.value;
|
||||||
|
|
||||||
|
if (this.playerTypes[i] === 'ai') {
|
||||||
|
this.aiBots[i] = new AIBot(i, this.map, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show game screen
|
||||||
|
document.getElementById('start-screen').style.display = 'none';
|
||||||
|
document.getElementById('game-screen').style.display = 'flex';
|
||||||
|
|
||||||
this.newGame();
|
this.newGame();
|
||||||
console.log('Game initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newGame() {
|
newGame() {
|
||||||
this.map = new HexMap(MAP_SIZE);
|
this.map = new HexMap(MAP_SIZE);
|
||||||
console.log('Map created, cells:', this.map.cells.size);
|
|
||||||
this.selectedCell = null;
|
this.selectedCell = null;
|
||||||
this.currentTarget = null;
|
this.currentTarget = null;
|
||||||
this.currentPlayer = 1;
|
this.currentPlayer = 1;
|
||||||
this.gamePhase = 'movement';
|
this.gamePhase = 'movement';
|
||||||
this.hasMoved = false;
|
this.hasMoved = false;
|
||||||
|
this.isAIThinking = false;
|
||||||
|
|
||||||
// Initialize starting positions
|
// Initialize starting positions for all players
|
||||||
this.initializePlayers();
|
this.initializePlayers();
|
||||||
|
|
||||||
this.centerMap();
|
this.centerMap();
|
||||||
this.render();
|
this.render();
|
||||||
|
this.createPlayerCards();
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
this.log('New game started! Player 1\'s turn.');
|
this.log(`New game started with ${this.playerCount} players!`);
|
||||||
|
|
||||||
|
// Start first player's turn (AI if needed)
|
||||||
|
this.checkAndRunAITurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
createPlayerCards() {
|
||||||
|
const container = document.getElementById('players-container');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
for (let i = 1; i <= this.playerCount; i++) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = `player-card player-${i}${i === 1 ? ' active' : ''}${this.playerTypes[i] === 'ai' ? ' ai-controlled' : ''}`;
|
||||||
|
card.id = `player${i}-card`;
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<h3>Player ${i}${this.playerTypes[i] === 'ai' ? ' (AI)' : ''}</h3>
|
||||||
|
<div class="player-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">Cells:</span>
|
||||||
|
<span class="stat-value" id="player${i}-cells">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">Supply:</span>
|
||||||
|
<span class="stat-value" id="player${i}-supply">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="stat-label">Strength:</span>
|
||||||
|
<span class="stat-value" id="player${i}-strength">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initializePlayers() {
|
initializePlayers() {
|
||||||
// Get random empty cells for each player
|
|
||||||
const emptyCells = this.map.getEmptyCells();
|
const emptyCells = this.map.getEmptyCells();
|
||||||
|
|
||||||
// Shuffle and pick starting positions
|
|
||||||
const shuffled = emptyCells.sort(() => Math.random() - 0.5);
|
const shuffled = emptyCells.sort(() => Math.random() - 0.5);
|
||||||
|
|
||||||
// Player 1 starting position (top-left area)
|
// Place starting units for each player
|
||||||
const p1Cell = shuffled.find(c => c.q < 8 && c.r < 8);
|
const positions = [
|
||||||
if (p1Cell) {
|
{ q: 2, r: 2 }, // Player 1 - top area
|
||||||
this.map.setOwner(p1Cell.q, p1Cell.r, 1);
|
{ q: MAP_SIZE - 3, r: MAP_SIZE - 3 }, // Player 2 - bottom area
|
||||||
p1Cell.setStrength(8); // Starting strength
|
{ q: 2, r: MAP_SIZE - 3 }, // Player 3
|
||||||
}
|
{ q: MAP_SIZE - 3, r: 2 } // Player 4
|
||||||
|
];
|
||||||
|
|
||||||
// Player 2 starting position (bottom-right area)
|
for (let i = 1; i <= this.playerCount; i++) {
|
||||||
const p2Cell = shuffled.find(c => c.q > 10 && c.r > 10 && c.type === CELL_TYPES.EMPTY);
|
const pos = positions[i - 1] || shuffled[i];
|
||||||
if (p2Cell) {
|
if (pos) {
|
||||||
this.map.setOwner(p2Cell.q, p2Cell.r, 2);
|
const cell = this.map.getCell(pos.q, pos.r);
|
||||||
p2Cell.setStrength(8);
|
if (cell && cell.isPassable()) {
|
||||||
|
this.map.setOwner(pos.q, pos.r, i);
|
||||||
|
cell.setStrength(8);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +193,6 @@ class GameUI {
|
|||||||
const canvasHeight = this.canvas.height;
|
const canvasHeight = this.canvas.height;
|
||||||
|
|
||||||
const sqrt3 = Math.sqrt(3);
|
const sqrt3 = Math.sqrt(3);
|
||||||
// Map dimensions for pointy-top hex grid
|
|
||||||
const mapWidth = HEX_SIZE * sqrt3 * (MAP_SIZE + MAP_SIZE/2);
|
const mapWidth = HEX_SIZE * sqrt3 * (MAP_SIZE + MAP_SIZE/2);
|
||||||
const mapHeight = HEX_SIZE * 1.5 * (MAP_SIZE - 1) + HEX_SIZE * 2;
|
const mapHeight = HEX_SIZE * 1.5 * (MAP_SIZE - 1) + HEX_SIZE * 2;
|
||||||
|
|
||||||
@@ -109,21 +205,13 @@ class GameUI {
|
|||||||
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
this.canvas.addEventListener('mousemove', (e) => this.handleMouseMove(e));
|
||||||
|
|
||||||
document.getElementById('end-turn-btn').addEventListener('click', () => this.endTurn());
|
document.getElementById('end-turn-btn').addEventListener('click', () => this.endTurn());
|
||||||
document.getElementById('new-game-btn').addEventListener('click', () => this.newGame());
|
|
||||||
document.getElementById('cancel-btn').addEventListener('click', () => this.cancelSelection());
|
document.getElementById('cancel-btn').addEventListener('click', () => this.cancelSelection());
|
||||||
}
|
}
|
||||||
|
|
||||||
hexToPixel(q, r) {
|
hexToPixel(q, r) {
|
||||||
// Pointy-top hex grid with proper adjacency
|
|
||||||
// For pointy-top: width = sqrt(3) * size, height = 2 * size
|
|
||||||
// Horizontal spacing = width = sqrt(3) * size
|
|
||||||
// Vertical spacing = 3/4 * height = 1.5 * size
|
|
||||||
const sqrt3 = Math.sqrt(3);
|
const sqrt3 = Math.sqrt(3);
|
||||||
|
|
||||||
// Convert axial to pixel coordinates for pointy-top hex
|
|
||||||
const x = this.offsetX + HEX_SIZE * sqrt3 * (q + r/2);
|
const x = this.offsetX + HEX_SIZE * sqrt3 * (q + r/2);
|
||||||
const y = this.offsetY + HEX_SIZE * 1.5 * r;
|
const y = this.offsetY + HEX_SIZE * 1.5 * r;
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,16 +232,6 @@ class GameUI {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get neighboring cells for pointy-top hex grid
|
|
||||||
* Directions for pointy-top hex in axial coordinates:
|
|
||||||
* - north-east: (+1, -1)
|
|
||||||
* - north-west: (0, -1)
|
|
||||||
* - west: (-1, 0)
|
|
||||||
* - south-west: (-1, +1)
|
|
||||||
* - south-east: (0, +1)
|
|
||||||
* - east: (+1, 0)
|
|
||||||
*/
|
|
||||||
getValidTargets(q, r) {
|
getValidTargets(q, r) {
|
||||||
const directions = [
|
const directions = [
|
||||||
[+1, -1], // north-east
|
[+1, -1], // north-east
|
||||||
@@ -180,14 +258,11 @@ class GameUI {
|
|||||||
|
|
||||||
drawHex(q, r, fillStyle, strokeStyle = COLORS.stroke, lineWidth = 2) {
|
drawHex(q, r, fillStyle, strokeStyle = COLORS.stroke, lineWidth = 2) {
|
||||||
const { x, y } = this.hexToPixel(q, r);
|
const { x, y } = this.hexToPixel(q, r);
|
||||||
|
const size = HEX_SIZE * 0.98;
|
||||||
|
|
||||||
// Draw pointy-top hexagon - size matches grid spacing
|
|
||||||
const size = HEX_SIZE * 0.98; // Almost full size for tight fit
|
|
||||||
|
|
||||||
// Pointy-top hexagon vertices (flat sides left/right)
|
|
||||||
const vertices = [];
|
const vertices = [];
|
||||||
for (let i = 0; i < 6; i++) {
|
for (let i = 0; i < 6; i++) {
|
||||||
const angle = Math.PI / 6 + (Math.PI / 3) * i; // Start at 30 degrees
|
const angle = Math.PI / 6 + (Math.PI / 3) * i;
|
||||||
vertices.push({
|
vertices.push({
|
||||||
x: x + size * Math.cos(angle),
|
x: x + size * Math.cos(angle),
|
||||||
y: y + size * Math.sin(angle)
|
y: y + size * Math.sin(angle)
|
||||||
@@ -215,14 +290,12 @@ class GameUI {
|
|||||||
const { x, y } = this.hexToPixel(cell.q, cell.r);
|
const { x, y } = this.hexToPixel(cell.q, cell.r);
|
||||||
const strength = cell.getStrength();
|
const strength = cell.getStrength();
|
||||||
|
|
||||||
// Draw strength number
|
|
||||||
this.ctx.fillStyle = COLORS.text;
|
this.ctx.fillStyle = COLORS.text;
|
||||||
this.ctx.font = 'bold 11px Arial';
|
this.ctx.font = 'bold 11px Arial';
|
||||||
this.ctx.textAlign = 'center';
|
this.ctx.textAlign = 'center';
|
||||||
this.ctx.textBaseline = 'middle';
|
this.ctx.textBaseline = 'middle';
|
||||||
this.ctx.fillText(strength.toString(), x, y - 3);
|
this.ctx.fillText(strength.toString(), x, y - 3);
|
||||||
|
|
||||||
// Draw dice count indicator (small dots)
|
|
||||||
if (cell.dice.length > 1) {
|
if (cell.dice.length > 1) {
|
||||||
const dotY = y + 8;
|
const dotY = y + 8;
|
||||||
for (let i = 0; i < Math.min(cell.dice.length, 5); i++) {
|
for (let i = 0; i < Math.min(cell.dice.length, 5); i++) {
|
||||||
@@ -238,9 +311,7 @@ class GameUI {
|
|||||||
if (!cell.isOwned()) return;
|
if (!cell.isOwned()) return;
|
||||||
|
|
||||||
const { x, y } = this.hexToPixel(cell.q, cell.r);
|
const { x, y } = this.hexToPixel(cell.q, cell.r);
|
||||||
const ownerColor = cell.getOwner() === 1 ? COLORS.player1 : COLORS.player2;
|
const ownerColor = PLAYER_COLORS[cell.getOwner()] || COLORS.player1;
|
||||||
|
|
||||||
// Draw border matching the hexagon shape
|
|
||||||
const size = HEX_SIZE * 0.98 - 3;
|
const size = HEX_SIZE * 0.98 - 3;
|
||||||
|
|
||||||
const vertices = [];
|
const vertices = [];
|
||||||
@@ -264,13 +335,11 @@ class GameUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Clear canvas
|
|
||||||
this.ctx.fillStyle = COLORS.stroke;
|
this.ctx.fillStyle = COLORS.stroke;
|
||||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
if (!this.map) return;
|
if (!this.map) return;
|
||||||
|
|
||||||
// Draw all cells
|
|
||||||
for (let r = 0; r < MAP_SIZE; r++) {
|
for (let r = 0; r < MAP_SIZE; r++) {
|
||||||
for (let q = 0; q < MAP_SIZE; q++) {
|
for (let q = 0; q < MAP_SIZE; q++) {
|
||||||
const cell = this.map.getCell(q, r);
|
const cell = this.map.getCell(q, r);
|
||||||
@@ -282,11 +351,14 @@ class GameUI {
|
|||||||
color = COLORS.player1;
|
color = COLORS.player1;
|
||||||
} else if (cell.type === CELL_TYPES.PLAYER2) {
|
} else if (cell.type === CELL_TYPES.PLAYER2) {
|
||||||
color = COLORS.player2;
|
color = COLORS.player2;
|
||||||
|
} else if (cell.type === CELL_TYPES.PLAYER3) {
|
||||||
|
color = COLORS.player3;
|
||||||
|
} else if (cell.type === CELL_TYPES.PLAYER4) {
|
||||||
|
color = COLORS.player4;
|
||||||
} else {
|
} else {
|
||||||
color = COLORS.empty;
|
color = COLORS.empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply transparency for non-owned cells
|
|
||||||
if (!cell.isOwned()) {
|
if (!cell.isOwned()) {
|
||||||
color = this.hexToRgba(color, 0.6);
|
color = this.hexToRgba(color, 0.6);
|
||||||
}
|
}
|
||||||
@@ -297,7 +369,6 @@ class GameUI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw selection highlight
|
|
||||||
if (this.selectedCell) {
|
if (this.selectedCell) {
|
||||||
this.drawHex(
|
this.drawHex(
|
||||||
this.selectedCell.q,
|
this.selectedCell.q,
|
||||||
@@ -307,7 +378,6 @@ class GameUI {
|
|||||||
3
|
3
|
||||||
);
|
);
|
||||||
|
|
||||||
// Highlight valid targets (not blocked, not own)
|
|
||||||
const targets = this.getValidTargets(this.selectedCell.q, this.selectedCell.r);
|
const targets = this.getValidTargets(this.selectedCell.q, this.selectedCell.r);
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
if (target.getOwner() !== this.currentPlayer) {
|
if (target.getOwner() !== this.currentPlayer) {
|
||||||
@@ -315,7 +385,7 @@ class GameUI {
|
|||||||
target.q,
|
target.q,
|
||||||
target.r,
|
target.r,
|
||||||
COLORS.target,
|
COLORS.target,
|
||||||
COLORS.player1,
|
PLAYER_COLORS[this.currentPlayer] || COLORS.player1,
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -332,6 +402,7 @@ class GameUI {
|
|||||||
|
|
||||||
handleClick(e) {
|
handleClick(e) {
|
||||||
if (this.gamePhase !== 'movement') return;
|
if (this.gamePhase !== 'movement') return;
|
||||||
|
if (this.playerTypes[this.currentPlayer] === 'ai') return;
|
||||||
|
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
@@ -343,29 +414,22 @@ class GameUI {
|
|||||||
const cell = this.map.getCell(hexPos.q, hexPos.r);
|
const cell = this.map.getCell(hexPos.q, hexPos.r);
|
||||||
if (!cell || !cell.isPassable()) return;
|
if (!cell || !cell.isPassable()) return;
|
||||||
|
|
||||||
// Handle cell selection
|
|
||||||
if (this.selectedCell === null) {
|
if (this.selectedCell === null) {
|
||||||
// Select own cell with strength > 1
|
|
||||||
if (cell.getOwner() === this.currentPlayer && cell.getStrength() > 1) {
|
if (cell.getOwner() === this.currentPlayer && cell.getStrength() > 1) {
|
||||||
this.selectedCell = cell;
|
this.selectedCell = cell;
|
||||||
this.instruction = `Select target cell or cancel`;
|
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check if clicking on same cell - deselect
|
|
||||||
if (cell === this.selectedCell) {
|
if (cell === this.selectedCell) {
|
||||||
this.cancelSelection();
|
this.cancelSelection();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if valid target using isometric-aware adjacency
|
|
||||||
const targets = this.getValidTargets(this.selectedCell.q, this.selectedCell.r);
|
const targets = this.getValidTargets(this.selectedCell.q, this.selectedCell.r);
|
||||||
const isValidTarget = targets.some(n => n.q === cell.q && n.r === cell.r);
|
const isValidTarget = targets.some(n => n.q === cell.q && n.r === cell.r);
|
||||||
|
|
||||||
// Cannot move to own cells - only attack enemy or capture empty
|
|
||||||
if (cell.getOwner() === this.currentPlayer) {
|
if (cell.getOwner() === this.currentPlayer) {
|
||||||
// Select different own cell instead
|
|
||||||
if (cell.getStrength() > 1) {
|
if (cell.getStrength() > 1) {
|
||||||
this.selectedCell = cell;
|
this.selectedCell = cell;
|
||||||
this.updateUI();
|
this.updateUI();
|
||||||
@@ -384,7 +448,7 @@ class GameUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove(e) {
|
handleMouseMove(e) {
|
||||||
// Could add hover effects here
|
// Hover effects could be added here
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAttack() {
|
executeAttack() {
|
||||||
@@ -395,39 +459,34 @@ class GameUI {
|
|||||||
const attackStrength = attacker.getStrength() - 1;
|
const attackStrength = attacker.getStrength() - 1;
|
||||||
|
|
||||||
if (defender.type === CELL_TYPES.EMPTY || defender.getOwner() !== this.currentPlayer) {
|
if (defender.type === CELL_TYPES.EMPTY || defender.getOwner() !== this.currentPlayer) {
|
||||||
// Attack empty or enemy cell
|
|
||||||
let defenseStrength = defender.getStrength();
|
let defenseStrength = defender.getStrength();
|
||||||
|
|
||||||
if (defenseStrength > 0) {
|
if (defenseStrength > 0) {
|
||||||
// Combat! Roll dice
|
|
||||||
const attackRoll = Math.floor(Math.random() * attackStrength) + 1;
|
const attackRoll = Math.floor(Math.random() * attackStrength) + 1;
|
||||||
const defenseRoll = Math.floor(Math.random() * defenseStrength) + 1;
|
const defenseRoll = Math.floor(Math.random() * defenseStrength) + 1;
|
||||||
|
|
||||||
this.log(`Attack: ${attackStrength} vs ${defenseStrength} | Roll: ${attackRoll} vs ${defenseRoll}`);
|
this.log(`P${this.currentPlayer} Attack: ${attackStrength} vs ${defenseStrength} | Roll: ${attackRoll} vs ${defenseRoll}`);
|
||||||
|
|
||||||
if (attackRoll > defenseRoll) {
|
if (attackRoll > defenseRoll) {
|
||||||
// Attacker wins
|
|
||||||
const remainingStrength = attackRoll - 1;
|
const remainingStrength = attackRoll - 1;
|
||||||
attacker.setStrength(1);
|
attacker.setStrength(1);
|
||||||
|
|
||||||
if (remainingStrength > 0) {
|
if (remainingStrength > 0) {
|
||||||
defender.setStrength(remainingStrength);
|
defender.setStrength(remainingStrength);
|
||||||
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
|
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
|
||||||
this.log(`Victory! Captured cell with strength ${remainingStrength}`, 'victory');
|
this.log(`Victory! Captured with strength ${remainingStrength}`, 'victory');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Defender wins - attacker loses all but 1 die
|
|
||||||
const remainingDefense = defenseRoll - attackRoll;
|
const remainingDefense = defenseRoll - attackRoll;
|
||||||
defender.setStrength(Math.max(1, remainingDefense));
|
defender.setStrength(Math.max(1, remainingDefense));
|
||||||
attacker.setStrength(1); // Attacker reduced to 1
|
attacker.setStrength(1);
|
||||||
this.log(`Attack repelled! Attacker reduced to 1, Defender has ${Math.max(1, remainingDefense)} strength`, 'defeat');
|
this.log(`Attack repelled! Defender has ${Math.max(1, remainingDefense)}`, 'defeat');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Move to empty cell - transfer attackStrength (original - 1)
|
|
||||||
attacker.setStrength(1);
|
attacker.setStrength(1);
|
||||||
defender.setStrength(attackStrength);
|
defender.setStrength(attackStrength);
|
||||||
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
|
this.map.setOwner(defender.q, defender.r, this.currentPlayer);
|
||||||
this.log(`Moved to empty cell with strength ${attackStrength}`);
|
this.log(`Captured empty cell with strength ${attackStrength}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,17 +503,17 @@ class GameUI {
|
|||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
endTurn() {
|
async endTurn() {
|
||||||
if (this.gamePhase !== 'movement') return;
|
if (this.gamePhase !== 'movement') return;
|
||||||
|
if (this.isAIThinking) return;
|
||||||
|
|
||||||
// Apply supply
|
// Apply supply
|
||||||
const supply = this.map.calculateSupply(this.currentPlayer);
|
const supply = this.map.calculateSupply(this.currentPlayer);
|
||||||
this.distributeSupply(supply);
|
this.distributeSupply(supply);
|
||||||
|
|
||||||
this.log(`Player ${this.currentPlayer} received ${supply} supply`);
|
this.log(`Player ${this.currentPlayer} received ${supply} supply`);
|
||||||
|
|
||||||
// Switch player
|
// Next player
|
||||||
this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
|
this.currentPlayer = (this.currentPlayer % this.playerCount) + 1;
|
||||||
this.hasMoved = false;
|
this.hasMoved = false;
|
||||||
|
|
||||||
this.cancelSelection();
|
this.cancelSelection();
|
||||||
@@ -462,30 +521,40 @@ class GameUI {
|
|||||||
this.render();
|
this.render();
|
||||||
|
|
||||||
this.log(`Player ${this.currentPlayer}'s turn`);
|
this.log(`Player ${this.currentPlayer}'s turn`);
|
||||||
|
|
||||||
|
// Check if next player is AI
|
||||||
|
this.checkAndRunAITurn();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkAndRunAITurn() {
|
||||||
|
if (this.playerTypes[this.currentPlayer] === 'ai' && this.aiBots[this.currentPlayer]) {
|
||||||
|
this.isAIThinking = true;
|
||||||
|
this.updateUI();
|
||||||
|
|
||||||
|
// Run AI turn
|
||||||
|
this.aiBots[this.currentPlayer].playTurn().then(() => {
|
||||||
|
this.isAIThinking = false;
|
||||||
|
// AI will call endTurn() when done
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
distributeSupply(supply) {
|
distributeSupply(supply) {
|
||||||
const playerCells = this.map.getPlayerCells(this.currentPlayer);
|
const playerCells = this.map.getPlayerCells(this.currentPlayer);
|
||||||
|
|
||||||
// Find cells that can receive more dice
|
|
||||||
const eligibleCells = playerCells.filter(cell => !cell.isMaxStrength());
|
const eligibleCells = playerCells.filter(cell => !cell.isMaxStrength());
|
||||||
|
|
||||||
if (eligibleCells.length === 0 || supply === 0) return;
|
if (eligibleCells.length === 0 || supply === 0) return;
|
||||||
|
|
||||||
// Distribute supply one by one to random eligible cells
|
|
||||||
let remainingSupply = supply;
|
let remainingSupply = supply;
|
||||||
while (remainingSupply > 0 && eligibleCells.length > 0) {
|
while (remainingSupply > 0 && eligibleCells.length > 0) {
|
||||||
// Pick random cell
|
|
||||||
const randomCell = eligibleCells[Math.floor(Math.random() * eligibleCells.length)];
|
const randomCell = eligibleCells[Math.floor(Math.random() * eligibleCells.length)];
|
||||||
const currentStrength = randomCell.getStrength();
|
const currentStrength = randomCell.getStrength();
|
||||||
|
|
||||||
if (currentStrength < 48) {
|
if (currentStrength < 48) {
|
||||||
// Add 1 strength to this cell
|
|
||||||
randomCell.setStrength(currentStrength + 1);
|
randomCell.setStrength(currentStrength + 1);
|
||||||
remainingSupply--;
|
remainingSupply--;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove cell from eligible if it's now at max strength
|
|
||||||
if (randomCell.isMaxStrength()) {
|
if (randomCell.isMaxStrength()) {
|
||||||
eligibleCells.splice(eligibleCells.indexOf(randomCell), 1);
|
eligibleCells.splice(eligibleCells.indexOf(randomCell), 1);
|
||||||
}
|
}
|
||||||
@@ -493,29 +562,32 @@ class GameUI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUI() {
|
updateUI() {
|
||||||
// Update current player stats
|
// Update all player cards
|
||||||
const currentCells = this.map.getPlayerCells(this.currentPlayer);
|
for (let i = 1; i <= this.playerCount; i++) {
|
||||||
const currentStrength = currentCells.reduce((sum, c) => sum + c.getStrength(), 0);
|
const playerCells = this.map.getPlayerCells(i);
|
||||||
const currentSupply = this.map.calculateSupply(this.currentPlayer);
|
const playerStrength = playerCells.reduce((sum, c) => sum + c.getStrength(), 0);
|
||||||
|
const playerSupply = this.map.calculateSupply(i);
|
||||||
|
|
||||||
// Update player 1 card (only player for now)
|
document.getElementById(`player${i}-cells`).textContent = playerCells.length;
|
||||||
document.getElementById('player1-cells').textContent = currentCells.length;
|
document.getElementById(`player${i}-supply`).textContent = playerSupply;
|
||||||
document.getElementById('player1-supply').textContent = currentSupply;
|
document.getElementById(`player${i}-strength`).textContent = playerStrength;
|
||||||
document.getElementById('player1-strength').textContent = currentStrength;
|
|
||||||
|
const card = document.getElementById(`player${i}-card`);
|
||||||
|
card.classList.toggle('active', i === this.currentPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
// Update game info
|
|
||||||
document.getElementById('current-turn').textContent = this.currentPlayer;
|
document.getElementById('current-turn').textContent = this.currentPlayer;
|
||||||
document.getElementById('game-phase').textContent = this.gamePhase;
|
document.getElementById('game-phase').textContent = this.gamePhase;
|
||||||
|
|
||||||
// Update instruction
|
|
||||||
const instruction = document.getElementById('action-instruction');
|
const instruction = document.getElementById('action-instruction');
|
||||||
if (this.selectedCell) {
|
if (this.selectedCell) {
|
||||||
instruction.textContent = `Select target to attack (strength: ${this.selectedCell.getStrength()})`;
|
instruction.textContent = `Select target (strength: ${this.selectedCell.getStrength()})`;
|
||||||
|
} else if (this.playerTypes[this.currentPlayer] === 'ai') {
|
||||||
|
instruction.textContent = 'AI is thinking...';
|
||||||
} else {
|
} else {
|
||||||
instruction.textContent = 'Select a cell with dice to move';
|
instruction.textContent = 'Select a cell with dice to move';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update selected cell info - always show with placeholder if nothing selected
|
|
||||||
const cellInfo = document.getElementById('selected-cell-info');
|
const cellInfo = document.getElementById('selected-cell-info');
|
||||||
cellInfo.style.display = 'block';
|
cellInfo.style.display = 'block';
|
||||||
|
|
||||||
@@ -527,38 +599,24 @@ class GameUI {
|
|||||||
document.getElementById('cell-dice').textContent = '-';
|
document.getElementById('cell-dice').textContent = '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update buttons
|
|
||||||
const cancelBtn = document.getElementById('cancel-btn');
|
const cancelBtn = document.getElementById('cancel-btn');
|
||||||
const endTurnBtn = document.getElementById('end-turn-btn');
|
const endTurnBtn = document.getElementById('end-turn-btn');
|
||||||
|
|
||||||
cancelBtn.disabled = !this.selectedCell;
|
cancelBtn.disabled = !this.selectedCell || this.playerTypes[this.currentPlayer] === 'ai';
|
||||||
// End turn is always enabled
|
endTurnBtn.disabled = this.playerTypes[this.currentPlayer] === 'ai';
|
||||||
endTurnBtn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log(message, type = '') {
|
log(message, type = '') {
|
||||||
const logContainer = document.getElementById('battle-log');
|
const logContainer = document.getElementById('battle-log');
|
||||||
const entry = document.createElement('div');
|
const entry = document.createElement('div');
|
||||||
entry.className = `log-entry ${type}`;
|
entry.className = `log-entry ${type}`;
|
||||||
entry.textContent = `[Turn ${this.currentPlayer}] ${message}`;
|
entry.textContent = `[P${this.currentPlayer}] ${message}`;
|
||||||
logContainer.insertBefore(entry, logContainer.firstChild);
|
logContainer.insertBefore(entry, logContainer.firstChild);
|
||||||
|
|
||||||
// Keep only last 50 entries
|
|
||||||
while (logContainer.children.length > 50) {
|
while (logContainer.children.length > 50) {
|
||||||
logContainer.removeChild(logContainer.lastChild);
|
logContainer.removeChild(logContainer.lastChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showMessage(message) {
|
|
||||||
const overlay = document.getElementById('message-overlay');
|
|
||||||
const messageEl = document.getElementById('overlay-message');
|
|
||||||
messageEl.textContent = message;
|
|
||||||
overlay.classList.add('visible');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
overlay.classList.remove('visible');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize game when DOM is ready
|
// Initialize game when DOM is ready
|
||||||
|
|||||||
@@ -8,6 +8,65 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="game-container">
|
<div class="game-container">
|
||||||
|
<!-- Start Screen -->
|
||||||
|
<div id="start-screen" class="start-screen">
|
||||||
|
<div class="start-panel">
|
||||||
|
<h1>HEXO</h1>
|
||||||
|
<p class="subtitle">DiceWars Clone</p>
|
||||||
|
|
||||||
|
<div class="setup-section">
|
||||||
|
<h3>Game Setup</h3>
|
||||||
|
|
||||||
|
<div class="setup-group">
|
||||||
|
<label for="player-count">Total Players:</label>
|
||||||
|
<select id="player-count">
|
||||||
|
<option value="2">2 Players</option>
|
||||||
|
<option value="3" selected>3 Players</option>
|
||||||
|
<option value="4">4 Players</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setup-group">
|
||||||
|
<label>Player Types:</label>
|
||||||
|
<div id="player-types">
|
||||||
|
<div class="player-type-row">
|
||||||
|
<span>Player 1:</span>
|
||||||
|
<select id="player1-type">
|
||||||
|
<option value="human">Human</option>
|
||||||
|
<option value="ai">AI Bot</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="player-type-row">
|
||||||
|
<span>Player 2:</span>
|
||||||
|
<select id="player2-type">
|
||||||
|
<option value="human">Human</option>
|
||||||
|
<option value="ai" selected>AI Bot</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="player-type-row" style="display:none;">
|
||||||
|
<span>Player 3:</span>
|
||||||
|
<select id="player3-type">
|
||||||
|
<option value="human">Human</option>
|
||||||
|
<option value="ai">AI Bot</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="player-type-row" style="display:none;">
|
||||||
|
<span>Player 4:</span>
|
||||||
|
<select id="player4-type">
|
||||||
|
<option value="human">Human</option>
|
||||||
|
<option value="ai">AI Bot</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary btn-large" id="start-game-btn">Start Game</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Game Screen -->
|
||||||
|
<div id="game-screen" class="game-screen" style="display:none;">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="game-header">
|
<header class="game-header">
|
||||||
<h1>HEXO</h1>
|
<h1>HEXO</h1>
|
||||||
@@ -18,22 +77,8 @@
|
|||||||
<div class="game-area">
|
<div class="game-area">
|
||||||
<!-- Left Panel - Player Info -->
|
<!-- Left Panel - Player Info -->
|
||||||
<aside class="side-panel left-panel">
|
<aside class="side-panel left-panel">
|
||||||
<div class="player-card player-1 active" id="player1-card">
|
<div id="players-container">
|
||||||
<h3>Player 1</h3>
|
<!-- Player cards will be inserted here -->
|
||||||
<div class="player-stats">
|
|
||||||
<div class="stat">
|
|
||||||
<span class="stat-label">Cells:</span>
|
|
||||||
<span class="stat-value" id="player1-cells">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<span class="stat-label">Supply:</span>
|
|
||||||
<span class="stat-value" id="player1-supply">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<span class="stat-label">Total Strength:</span>
|
|
||||||
<span class="stat-value" id="player1-strength">0</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="game-info">
|
<div class="game-info">
|
||||||
@@ -49,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-primary" id="end-turn-btn">End Turn</button>
|
<button class="btn btn-primary" id="end-turn-btn">End Turn</button>
|
||||||
<button class="btn btn-secondary" id="new-game-btn">New Game</button>
|
<button class="btn btn-secondary" id="back-menu-btn">Main Menu</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Canvas -->
|
<!-- Canvas -->
|
||||||
@@ -68,16 +113,16 @@
|
|||||||
<button class="btn btn-action" id="cancel-btn" style="width:100%" disabled>Cancel</button>
|
<button class="btn btn-action" id="cancel-btn" style="width:100%" disabled>Cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selected-cell-info" id="selected-cell-info" style="display:none;">
|
<div class="selected-cell-info" id="selected-cell-info">
|
||||||
<h3>Selected Cell</h3>
|
<h3>Selected Cell</h3>
|
||||||
<div class="cell-stats">
|
<div class="cell-stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-label">Strength:</span>
|
<span class="stat-label">Strength:</span>
|
||||||
<span class="stat-value" id="cell-strength">0</span>
|
<span class="stat-value" id="cell-strength">-</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<span class="stat-label">Dice:</span>
|
<span class="stat-label">Dice:</span>
|
||||||
<span class="stat-value" id="cell-dice">0</span>
|
<span class="stat-value" id="cell-dice">-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +141,7 @@
|
|||||||
<p>Click on your cell with dice, then click adjacent cell to attack</p>
|
<p>Click on your cell with dice, then click adjacent cell to attack</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script type="module" src="game.js"></script>
|
<script type="module" src="game.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -30,6 +30,110 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Start Screen */
|
||||||
|
.start-screen {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-panel {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 90%;
|
||||||
|
border: 2px solid var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-panel h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.start-panel .subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-section {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-section h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border: 1px solid var(--accent-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-type-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-panel);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-type-row span {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-type-row select {
|
||||||
|
width: 120px;
|
||||||
|
padding: 6px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--accent-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
.game-container {
|
.game-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -89,6 +193,7 @@ body {
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-left: 4px solid transparent;
|
border-left: 4px solid transparent;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-card.active {
|
.player-card.active {
|
||||||
@@ -96,7 +201,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-1 {
|
.player-1 {
|
||||||
border-left-color: var(--player1-color);
|
border-left-color: #4ecca3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-1.active {
|
.player-1.active {
|
||||||
@@ -104,13 +209,44 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-2 {
|
.player-2 {
|
||||||
border-left-color: var(--player2-color);
|
border-left-color: #e94560;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-2.active {
|
.player-2.active {
|
||||||
background: linear-gradient(135deg, var(--bg-panel), rgba(233, 69, 96, 0.1));
|
background: linear-gradient(135deg, var(--bg-panel), rgba(233, 69, 96, 0.1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.player-3 {
|
||||||
|
border-left-color: #f9ed69;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-3.active {
|
||||||
|
background: linear-gradient(135deg, var(--bg-panel), rgba(249, 237, 105, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-4 {
|
||||||
|
border-left-color: #a8e6cf;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-4.active {
|
||||||
|
background: linear-gradient(135deg, var(--bg-panel), rgba(168, 230, 207, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card.ai-controlled {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-card.ai-controlled::after {
|
||||||
|
content: 'AI';
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: var(--accent-secondary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.player-card h3 {
|
.player-card h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|||||||
Reference in New Issue
Block a user