anycoder-6af94c43 / index.html
Mathieu-Thomas-JOSSET's picture
Upload folder using huggingface_hub
be373e8 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tank Command - Tactical Strategy</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@400;500;600;700&family=Orbitron:wght@400;500;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg-dark: #0a0e14;
--bg-panel: #141a22;
--bg-panel-light: #1c242e;
--accent-blue: #00d4ff;
--accent-orange: #ff6b35;
--accent-green: #39ff14;
--accent-red: #ff3333;
--text-primary: #e8e8e8;
--text-secondary: #8892a0;
--border-color: #2a3544;
}
body {
font-family: 'Rajdhani', sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
header {
background: linear-gradient(180deg, var(--bg-panel) 0%, var(--bg-dark) 100%);
padding: 12px 24px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
box-shadow: 0 4px 20px rgba(0, 212, 255, 0.1);
}
.logo {
font-family: 'Orbitron', sans-serif;
font-size: 1.6rem;
font-weight: 700;
color: var(--accent-blue);
text-shadow: 0 0 10px rgba(0, 212, 255, 0.5);
letter-spacing: 2px;
}
.logo span {
color: var(--accent-orange);
}
.header-links {
display: flex;
gap: 20px;
align-items: center;
}
.header-links a {
color: var(--text-secondary);
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
font-size: 0.9rem;
}
.header-links a:hover {
color: var(--accent-blue);
}
/* Main Game Container */
.game-container {
display: flex;
flex: 1;
overflow: hidden;
}
/* Game Canvas Area */
.canvas-area {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
background:
radial-gradient(circle at 30% 30%, rgba(0, 212, 255, 0.03) 0%, transparent 50%),
radial-gradient(circle at 70% 70%, rgba(255, 107, 53, 0.03) 0%, transparent 50%),
var(--bg-dark);
}
#gameCanvas {
border: 2px solid var(--border-color);
border-radius: 4px;
box-shadow:
0 0 30px rgba(0, 0, 0, 0.5),
inset 0 0 60px rgba(0, 212, 255, 0.02);
cursor: crosshair;
}
/* Side Panel */
.side-panel {
width: 280px;
background: var(--bg-panel);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
padding: 16px;
gap: 16px;
overflow-y: auto;
}
.panel-section {
background: var(--bg-panel-light);
border-radius: 8px;
padding: 16px;
border: 1px solid var(--border-color);
}
.panel-title {
font-family: 'Orbitron', sans-serif;
font-size: 0.75rem;
color: var(--accent-blue);
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-color);
}
/* Turn Indicator */
.turn-indicator {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
background: var(--bg-dark);
border-radius: 6px;
border: 1px solid var(--border-color);
}
.turn-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
.turn-dot.player {
background: var(--accent-green);
box-shadow: 0 0 10px var(--accent-green);
}
.turn-dot.enemy {
background: var(--accent-red);
box-shadow: 0 0 10px var(--accent-red);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.turn-text {
font-weight: 600;
font-size: 1rem;
}
/* Unit Info */
.unit-info {
min-height: 100px;
}
.no-selection {
color: var(--text-secondary);
font-style: italic;
text-align: center;
padding: 20px;
}
.unit-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.unit-icon {
width: 40px;
height: 40px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.4rem;
}
.unit-icon.player {
background: linear-gradient(135deg, #1a4d1a 0%, #0d260d 100%);
border: 2px solid var(--accent-green);
}
.unit-icon.enemy {
background: linear-gradient(135deg, #4d1a1a 0%, #260d0d 100%);
border: 2px solid var(--accent-red);
}
.unit-name {
font-weight: 700;
font-size: 1.1rem;
}
.unit-class {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 1px;
}
.unit-stats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
background: var(--bg-dark);
border-radius: 4px;
font-size: 0.85rem;
}
.stat-label {
color: var(--text-secondary);
}
.stat-value {
font-weight: 600;
color: var(--accent-blue);
}
.stat-value.hp {
color: var(--accent-green);
}
.stat-value.armor {
color: #ffd700;
}
.stat-value.damage {
color: var(--accent-orange);
}
/* Action Buttons */
.action-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
.btn {
padding: 12px 16px;
border: none;
border-radius: 6px;
font-family: 'Rajdhani', sans-serif;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary {
background: linear-gradient(135deg, var(--accent-blue) 0%, #0099cc 100%);
color: var(--bg-dark);
box-shadow: 0 4px 15px rgba(0, 212, 255, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 212, 255, 0.4);
}
.btn-primary:disabled {
background: #333;
color: #666;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: var(--bg-panel-light);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--bg-panel);
border-color: var(--accent-blue);
}
/* Objective */
.objective-box {
background: linear-gradient(135deg, rgba(255, 107, 53, 0.1) 0%, transparent 100%);
border: 1px solid var(--accent-orange);
border-radius: 6px;
padding: 12px;
}
.objective-title {
color: var(--accent-orange);
font-weight: 700;
font-size: 0.9rem;
margin-bottom: 6px;
}
.objective-text {
font-size: 0.85rem;
color: var(--text-secondary);
}
/* Terrain Legend */
.terrain-legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.8rem;
padding: 4px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 3px;
border: 1px solid var(--border-color);
}
/* Overlays */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s;
}
.overlay.active {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--bg-panel);
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 40px;
text-align: center;
max-width: 400px;
transform: scale(0.9);
transition: transform 0.3s;
box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
}
.overlay.active .modal {
transform: scale(1);
}
.modal-title {
font-family: 'Orbitron', sans-serif;
font-size: 2rem;
margin-bottom: 16px;
text-transform: uppercase;
letter-spacing: 3px;
}
.modal-title.victory {
color: var(--accent-green);
text-shadow: 0 0 20px rgba(57, 255, 20, 0.5);
}
.modal-title.defeat {
color: var(--accent-red);
text-shadow: 0 0 20px rgba(255, 51, 51, 0.5);
}
.modal-title.start {
color: var(--accent-blue);
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
}
.modal-message {
color: var(--text-secondary);
margin-bottom: 24px;
font-size: 1.1rem;
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: center;
}
/* Toast Messages */
.toast-container {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 500;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.toast {
background: var(--bg-panel);
border: 1px solid var(--border-color);
padding: 12px 24px;
border-radius: 6px;
font-weight: 600;
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.toast.player-turn {
border-color: var(--accent-green);
color: var(--accent-green);
}
.toast.enemy-turn {
border-color: var(--accent-red);
color: var(--accent-red);
}
.toast.event {
border-color: var(--accent-blue);
color: var(--accent-blue);
}
.toast.damage {
border-color: var(--accent-orange);
color: var(--accent-orange);
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-20px); }
}
/* Responsive */
@media (max-width: 900px) {
.side-panel {
width: 220px;
padding: 12px;
}
.game-container {
flex-direction: column;
}
.side-panel {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
border-left: none;
border-top: 1px solid var(--border-color);
max-height: 200px;
overflow-y: auto;
}
.panel-section {
min-width: 45%;
}
}
/* Tooltip */
.tooltip {
position: fixed;
background: var(--bg-panel);
border: 1px solid var(--accent-blue);
border-radius: 6px;
padding: 10px 14px;
font-size: 0.85rem;
pointer-events: none;
z-index: 600;
max-width: 200px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
opacity: 0;
transition: opacity 0.2s;
}
.tooltip.visible {
opacity: 1;
}
.tooltip-title {
font-weight: 700;
color: var(--accent-blue);
margin-bottom: 4px;
}
.tooltip-desc {
color: var(--text-secondary);
}
</style>
</head>
<body>
<header>
<div class="logo">TANK <span>COMMAND</span></div>
<div class="header-links">
<a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank">Built with anycoder</a>
</div>
</header>
<div class="game-container">
<div class="canvas-area">
<canvas id="gameCanvas" width="640" height="640"></canvas>
</div>
<div class="side-panel">
<div class="panel-section">
<div class="panel-title">Turn</div>
<div class="turn-indicator">
<div class="turn-dot player" id="turnDot"></div>
<span class="turn-text" id="turnText">Player Turn</span>
</div>
</div>
<div class="panel-section unit-info">
<div class="panel-title">Selected Unit</div>
<div id="unitInfo">
<div class="no-selection">Click a tank to select</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">Actions</div>
<div class="action-buttons">
<button class="btn btn-primary" id="endTurnBtn">End Turn</button>
<button class="btn btn-secondary" id="restartBtn">Restart Game</button>
</div>
</div>
<div class="panel-section">
<div class="panel-title">Objective</div>
<div class="objective-box">
<div class="objective-title">⚔️ Destroy All Enemies</div>
<div class="objective-text">Eliminate all 3 enemy tanks to achieve victory</div>
</div>
</div>
<div class="panel-section">
<div class="panel-title">Terrain Legend</div>
<div class="terrain-legend">
<div class="legend-item">
<div class="legend-color" style="background: #3d5c3d;"></div>
<span>Plains</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #1a3d1a;"></div>
<span>Forest (+Def)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #4a4a4a;"></div>
<span>Rocks</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #3d3d2a;"></div>
<span>Mud (Slow)</span>
</div>
</div>
</div>
</div>
</div>
<!-- Start Screen -->
<div class="overlay active" id="startOverlay">
<div class="modal">
<h1 class="modal-title start">Tank Command</h1>
<p class="modal-message">Lead your tank squad to victory in tactical combat</p>
<div class="modal-buttons">
<button class="btn btn-primary" id="startGameBtn">Start Game</button>
</div>
</div>
</div>
<!-- Game Over Overlay -->
<div class="overlay" id="gameOverOverlay">
<div class="modal">
<h1 class="modal-title" id="gameOverTitle">Victory</h1>
<p class="modal-message" id="gameOverMessage">All enemies destroyed!</p>
<div class="modal-buttons">
<button class="btn btn-primary" id="playAgainBtn">Play Again</button>
</div>
</div>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<!-- Tooltip -->
<div class="tooltip" id="tooltip">
<div class="tooltip-title"></div>
<div class="tooltip-desc"></div>
</div>
<script>
// ==================== GAME CONSTANTS ====================
const GRID_SIZE = 12;
const TILE_SIZE = 48;
const CANVAS_SIZE = GRID_SIZE * TILE_SIZE;
// Terrain Types
const TERRAIN = {
PLAINS: { id: 0, name: 'Plains', color: '#3d5c3d', moveCost: 1, defense: 0, description: 'Normal terrain' },
FOREST: { id: 1, name: 'Forest', color: '#1a3d1a', moveCost: 1.5, defense: 20, description: '+20% Defense, partial cover' },
ROCKS: { id: 2, name: 'Rocks', color: '#4a4a4a', moveCost: Infinity, defense: 0, description: 'Impassable terrain' },
MUD: { id: 3, name: 'Mud', color: '#3d3d2a', moveCost: 2, defense: 0, description: 'Movement slow, no defense' }
};
// Tank Classes
const TANK_CLASSES = {
LIGHT: {
name: 'Light Tank',
icon: '⚡',
hp: 80,
armor: 10,
movement: 4,
attackRange: 3,
damage: 25,
critChance: 15,
color: '#00ff88'
},
MEDIUM: {
name: 'Medium Tank',
icon: '🎯',
hp: 120,
armor: 25,
movement: 3,
attackRange: 3,
damage: 35,
critChance: 10,
color: '#00aaff'
},
HEAVY: {
name: 'Heavy Tank',
icon: '💪',
hp: 180,
armor: 45,
movement: 2,
attackRange: 2,
damage: 50,
critChance: 5,
color: '#ffaa00'
}
};
// ==================== GAME STATE ====================
let gameState = {
grid: [],
playerTanks: [],
enemyTanks: [],
selectedTank: null,
currentTurn: 'player', // 'player' or 'enemy'
phase: 'select', // 'select', 'move', 'attack', 'enemy'
gameOver: false,
winner: null,
animations: [],
effects: []
};
// ==================== CANVAS SETUP ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
canvas.width = CANVAS_SIZE;
canvas.height = CANVAS_SIZE;
// ==================== UI ELEMENTS ====================
const turnDot = document.getElementById('turnDot');
const turnText = document.getElementById('turnText');
const unitInfo = document.getElementById('unitInfo');
const endTurnBtn = document.getElementById('endTurnBtn');
const restartBtn = document.getElementById('restartBtn');
const startOverlay = document.getElementById('startOverlay');
const gameOverOverlay = document.getElementById('gameOverOverlay');
const gameOverTitle = document.getElementById('gameOverTitle');
const gameOverMessage = document.getElementById('gameOverMessage');
const toastContainer = document.getElementById('toastContainer');
const tooltip = document.getElementById('tooltip');
// ==================== INITIALIZATION ====================
function initGame() {
gameState = {
grid: generateMap(),
playerTanks: [],
enemyTanks: [],
selectedTank: null,
currentTurn: 'player',
phase: 'select',
gameOver: false,
winner: null,
animations: [],
effects: []
};
// Create player tanks
gameState.playerTanks = [
createTank(0, TANK_CLASSES.LIGHT, 1, 10, 'player'),
createTank(1, TANK_CLASSES.MEDIUM, 1, 8, 'player'),
createTank(2, TANK_CLASSES.HEAVY, 2, 9, 'player')
];
// Create enemy tanks
gameState.enemyTanks = [
createTank(3, TANK_CLASSES.LIGHT, 10, 1, 'enemy'),
createTank(4, TANK_CLASSES.MEDIUM, 10, 3, 'enemy'),
createTank(5, TANK_CLASSES.HEAVY, 9, 2, 'enemy')
];
updateUI();
render();
showToast('Player Turn', 'player-turn');
}
function generateMap() {
const grid = [];
for (let y = 0; y < GRID_SIZE; y++) {
grid[y] = [];
for (let x = 0; x < GRID_SIZE; x++) {
// Default to plains
grid[y][x] = TERRAIN.PLAINS.id;
// Add some terrain variation
const rand = Math.random();
// Add forest patches
if (rand < 0.08 && !isObjectiveZone(x, y)) {
grid[y][x] = TERRAIN.FOREST.id;
}
// Add mud patches
if (rand > 0.92 && !isObjectiveZone(x, y)) {
grid[y][x] = TERRAIN.MUD.id;
}
// Add rock formations (impassable)
if (rand > 0.85 && rand < 0.88) {
grid[y][x] = TERRAIN.ROCKS.id;
}
}
}
// Ensure starting zones are clear
for (let x = 0; x < 3; x++) {
for (let y = 8; y < GRID_SIZE; y++) {
if (grid[y][x] === TERRAIN.ROCKS.id) grid[y][x] = TERRAIN.PLAINS.id;
}
}
for (let x = GRID_SIZE - 3; x < GRID_SIZE; x++) {
for (let y = 0; y < 4; y++) {
if (grid[y][x] === TERRAIN.ROCKS.id) grid[y][x] = TERRAIN.PLAINS.id;
}
}
return grid;
}
function isObjectiveZone(x, y) {
return (x >= 4 && x <= 7 && y >= 4 && y <= 7);
}
function createTank(id, tankClass, x, y, team) {
return {
id,
class: tankClass,
x,
y,
team,
hp: tankClass.hp,
maxHp: tankClass.hp,
armor: tankClass.armor,
movement: tankClass.movement,
attackRange: tankClass.attackRange,
damage: tankClass.damage,
critChance: tankClass.critChance,
hasActed: false,
direction: team === 'player' ? 'left' : 'right'
};
}
// ==================== CORE GAME LOGIC ====================
function getTerrainAt(x, y) {
if (x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE) return null;
const terrainId = gameState.grid[y][x];
return Object.values(TERRAIN).find(t => t.id === terrainId);
}
function getTankAt(x, y) {
const allTanks = [...gameState.playerTanks, ...gameState.enemyTanks];
return allTanks.find(t => t.x === x && t.y === y && t.hp > 0);
}
function getValidMovementTiles(tank) {
if (tank.hasActed) return [];
const tiles = [];
const visited = new Set();
const queue = [{ x: tank.x, y: tank.y, cost: 0 }];
while (queue.length > 0) {
const current = queue.shift();
const key = `${current.x},${current.y}`;
if (visited.has(key)) continue;
visited.add(key);
const terrain = getTerrainAt(current.x, current.y);
if (!terrain || terrain.moveCost === Infinity) continue;
const totalCost = current.cost + terrain.moveCost;
if (totalCost <= tank.movement) {
if (current.x !== tank.x || current.y !== tank.y) {
if (!getTankAt(current.x, current.y)) {
tiles.push({ x: current.x, y: current.y, cost: totalCost });
}
}
// Check adjacent tiles
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];
for (const [dx, dy] of directions) {
const nx = current.x + dx;
const ny = current.y + dy;
if (nx >= 0 && nx < GRID_SIZE && ny >= 0 && ny < GRID_SIZE) {
queue.push({ x: nx, y: ny, cost: totalCost });
}
}
}
}
return tiles;
}
function getValidAttackTargets(tank) {
if (tank.hasActed) return [];
const targets = [];
const allTanks = tank.team === 'player' ? gameState.enemyTanks : gameState.playerTanks;
for (const target of allTanks) {
if (target.hp <= 0) continue;
const distance = Math.abs(target.x - tank.x) + Math.abs(target.y - tank.y);
if (distance <= tank.attackRange) {
// Check line of sight (simple - no blocking)
targets.push(target);
}
}
return targets;
}
function canAttack(tank) {
return getValidAttackTargets(tank).length > 0;
}
function performAttack(attacker, target) {
// Calculate damage with defense
let damage = attacker.damage;
// Check for critical hit
const isCrit = Math.random() * 100 < attacker.critChance;
if (isCrit) {
damage *= 2;
showToast('CRITICAL HIT!', 'damage');
}
// Apply terrain defense bonus
const terrain = getTerrainAt(target.x, target.y);
const defenseReduction = terrain ? terrain.defense : 0;
const effectiveArmor = target.armor + defenseReduction;
damage = Math.max(1, damage - (effectiveArmor * 0.5));
// Apply damage
target.hp = Math.max(0, target.hp - damage);
// Visual effects
createExplosionEffect(target.x, target.y, isCrit ? '#ff6600' : '#ff3333');
createMuzzleFlash(attacker.x, attacker.y, target.x, target.y);
// Update direction
attacker.direction = target.x > attacker.x ? 'right' : 'left';
// Check if target destroyed
if (target.hp <= 0) {
showToast(`${target.team === 'player' ? 'Enemy' : 'Your'} ${target.class.name} Destroyed!`, 'event');
createExplosionEffect(target.x, target.y, '#ff6600', true);
}
attacker.hasActed = true;
gameState.selectedTank = null;
gameState.phase = 'select';
checkWinCondition();
updateUI();
render();
}
function moveTank(tank, newX, newY) {
tank.x = newX;
tank.y = newY;
tank.hasActed = true;
gameState.phase = 'attack';
updateUI();
render();
}
function checkWinCondition() {
const alivePlayerTanks = gameState.playerTanks.filter(t => t.hp > 0);
const aliveEnemyTanks = gameState.enemyTanks.filter(t => t.hp > 0);
if (aliveEnemyTanks.length === 0) {
endGame('victory');
} else if (alivePlayerTanks.length === 0) {
endGame('defeat');
}
}
function endGame(result) {
gameState.gameOver = true;
gameState.winner = result;
setTimeout(() => {
gameOverTitle.textContent = result === 'victory' ? 'Victory!' : 'Defeat';
gameOverTitle.className = `modal-title ${result}`;
gameOverMessage.textContent = result === 'victory'
? 'All enemy tanks destroyed!'
: 'Your squad has been eliminated!';
gameOverOverlay.classList.add('active');
}, 1000);
}
// ==================== TURN MANAGEMENT ====================
function endTurn() {
if (gameState.gameOver) return;
// Deselect any tank
gameState.selectedTank = null;
gameState.phase = 'select';
// Switch turn
if (gameState.currentTurn === 'player') {
gameState.currentTurn = 'enemy';
gameState.phase = 'enemy';
// Reset enemy action states
gameState.enemyTanks.forEach(t => {
if (t.hp > 0) t.hasActed = false;
});
showToast('Enemy Turn', 'enemy-turn');
// Start enemy AI
setTimeout(runEnemyAI, 500);
} else {
gameState.currentTurn = 'player';
gameState.phase = 'select';
// Reset player action states
gameState.playerTanks.forEach(t => {
if (t.hp > 0) t.hasActed = false;
});
showToast('Player Turn', 'player-turn');
}
updateUI();
render();
}
// ==================== AI SYSTEM ====================
async function runEnemyAI() {
const aliveEnemies = gameState.enemyTanks.filter(t => t.hp > 0);
const alivePlayers = gameState.playerTanks.filter(t => t.hp > 0);
if (alivePlayers.length === 0 || aliveEnemies.length.length === 0) {
endTurn();
return;
}
for (const enemy of aliveEnemies) {
if (enemy.hasActed) continue;
// Find nearest player tank
let nearestPlayer = null;
let minDistance = Infinity;
for (const player of alivePlayers) {
const dist = Math.abs(player.x - enemy.x) + Math.abs(player.y - enemy.y);
if (dist < minDistance) {
minDistance = dist;
nearestPlayer = player;
}
}
if (!nearestPlayer) continue;
// Check if can attack from current position
const canAttackNow = getValidAttackTargets(enemy).includes(nearestPlayer);
if (canAttackNow) {
// Attack!
performAttack(enemy, nearestPlayer);
await sleep(600);
} else {
// Move toward player
const moveTiles = getValidMovementTiles(enemy);
if (moveTiles.length > 0) {
// Find best move tile (closest to player, not occupied)
let bestTile = null;
let bestDist = Infinity;
for (const tile of moveTiles) {
const dist = Math.abs(tile.x - nearestPlayer.x) + Math.abs(tile.y - nearestPlayer.y);
if (dist < bestDist) {
bestDist = dist;
bestTile = tile;
}
}
if (bestTile) {
moveTank(enemy, bestTile.x, bestTile.y);
await sleep(400);
// Try to attack after moving
const targetsAfterMove = getValidAttackTargets(enemy);
if (targetsAfterMove.length > 0) {
// Attack the nearest/vulnerable target
const target = targetsAfterMove.reduce((prev, curr) =>
curr.hp < prev.hp ? curr : prev
);
performAttack(enemy, target);
await sleep(600);
}
}
}
}
await sleep(300);
}
// All enemies have acted, end turn
setTimeout(endTurn, 500);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ==================== RENDERING ====================
function render() {
ctx.clearRect(0, 0, CANVAS_SIZE, CANVAS_SIZE);
drawGrid();
drawTerrain();
drawObjectiveZone();
drawTanks();
drawSelectionOverlay();
drawEffects();
}
function drawGrid() {
ctx.strokeStyle = 'rgba(42, 53, 68, 0.5)';
ctx.lineWidth = 1;
for (let x = 0; x <= GRID_SIZE; x++) {
ctx.beginPath();
ctx.moveTo(x * TILE_SIZE, 0);
ctx.lineTo(x * TILE_SIZE, CANVAS_SIZE);
ctx.stroke();
}
for (let y = 0; y <= GRID_SIZE; y++) {
ctx.beginPath();
ctx.moveTo(0, y * TILE_SIZE);
ctx.lineTo(CANVAS_SIZE, y * TILE_SIZE);
ctx.stroke();
}
}
function drawTerrain() {
for (let y = 0; y < GRID_SIZE; y++) {
for (let x = 0; x < GRID_SIZE; x++) {
const terrainId = gameState.grid[y][x];
const terrain = Object.values(TERRAIN).find(t => t.id === terrainId);
if (terrain) {
ctx.fillStyle = terrain.color;
ctx.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
// Add texture detail
if (terrain.id === TERRAIN.FOREST.id) {
drawForestTexture(x, y);
} else if (terrain.id === TERRAIN.MUD.id) {
drawMudTexture(x, y);
} else if (terrain.id === TERRAIN.ROCKS.id) {
drawRocks(x, y);
}
}
}
}
}
function drawForestTexture(x, y) {
ctx.fillStyle = 'rgba(0, 50, 0, 0.3)';
const seed = (x * 7 + y * 13) % 5;
for (let i = 0; i < 3; i++) {
const sx = x * TILE_SIZE + 10 + ((seed + i) * 11) % 28;
const sy = y * TILE_SIZE + 8 + ((seed + i * 3) * 7) % 32;
ctx.beginPath();
ctx.arc(sx, sy, 6 + (i % 2) * 3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawMudTexture(x, y) {
ctx.fillStyle = 'rgba(80, 70, 40, 0.2)';
const seed = (x * 5 + y * 11) % 4;
for (let i = 0; i < seed + 2; i++) {
const sx = x * TILE_SIZE + 8 + (i * 13) % 32;
const sy = y * TILE_SIZE + 10 + (i * 17) % 28;
ctx.beginPath();
ctx.ellipse(sx, sy, 8, 5, 0, 0, Math.PI * 2);
ctx.fill();
}
}
function drawRocks(x, y) {
ctx.fillStyle = '#5a5a5a';
const seed = (x * 3 + y * 7) % 3;
for (let i = 0; i < seed + 2; i++) {
const sx = x * TILE_SIZE + 12 + (i * 15) % 24;
const sy = y * TILE_SIZE + 14 + (i * 11) % 20;
ctx.beginPath();
ctx.moveTo(sx, sy - 8);
ctx.lineTo(sx + 8, sy);
ctx.lineTo(sx + 6, sy + 8);
ctx.lineTo(sx - 4, sy + 6);
ctx.closePath();
ctx.fill();
}
}
function drawObjectiveZone() {
ctx.fillStyle = 'rgba(255, 107, 53, 0.1)';
ctx.fillRect(4 * TILE_SIZE, 4 * TILE_SIZE, 4 * TILE_SIZE, 4 * TILE_SIZE);
ctx.strokeStyle = 'rgba(255, 107, 53, 0.4)';
ctx.lineWidth = 2;
ctx.setLineDash([10, 5]);
ctx.strokeRect(4 * TILE_SIZE, 4 * TILE_SIZE, 4 * TILE_SIZE, 4 * TILE_SIZE);
ctx.setLineDash([]);
}
function drawTanks() {
const allTanks = [...gameState.playerTanks, ...gameState.enemyTanks];
for (const tank of allTanks) {
if (tank.hp <= 0) {
drawWreckage(tank);
continue;
}
drawTank(tank);
}
}
function drawTank(tank) {
const cx = tank.x * TILE_SIZE + TILE_SIZE / 2;
const cy = tank.y * TILE_SIZE + TILE_SIZE / 2;
// Tank body
const bodyColor = tank.team === 'player' ? '#2a5a2a' : '#5a2a2a';
const accentColor = tank.class.color;
// Shadow
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath();
ctx.ellipse(cx + 3, cy + 20, 18, 8, 0, 0, Math.PI * 2);
ctx.fill();
// Body
ctx.fillStyle = bodyColor;
ctx.beginPath();
ctx.roundRect(cx - 16, cy - 10, 32, 24, 4);
ctx.fill();
// Body accent stripe
ctx.fillStyle = accentColor;
ctx.fillRect(cx - 14, cy - 2, 28, 4);
// Turret
ctx.fillStyle = tank.team === 'player' ? '#3a7a3a' : '#7a3a3a';
ctx.beginPath();
ctx.arc(cx, cy - 2, 10, 0, Math.PI * 2);
ctx.fill();
// Turret accent
ctx.fillStyle = accentColor;
ctx.beginPath();
ctx.arc(cx, cy - 2, 5, 0, Math.PI * 2);
ctx.fill();
// Barrel
const barrelDir = tank.direction === 'right' ? 1 :