Worms
thinkingcap qwen3.6 27b · basic

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 51,287 bytes · SHA-256 cfec4757c437
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worm Warfare</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0a1a; overflow: hidden; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; user-select: none; }
canvas { display: block; cursor: crosshair; }
#ui-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
#ui-overlay > * { pointer-events: auto; }
#menu-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #0a0a2e 0%, #1a0a3e 50%, #0a1a2e 100%); z-index: 100; }
#menu-screen h1 { font-size: 72px; color: #ff6b35; text-shadow: 0 0 30px rgba(255,107,53,0.5), 0 4px 8px rgba(0,0,0,0.8); margin-bottom: 10px; letter-spacing: 4px; }
#menu-screen .subtitle { color: #aaa; font-size: 18px; margin-bottom: 40px; letter-spacing: 2px; }
.menu-btn { background: linear-gradient(135deg, #ff6b35, #e74c3c); border: none; color: white; padding: 16px 48px; font-size: 22px; border-radius: 12px; cursor: pointer; margin: 8px; transition: all 0.2s; box-shadow: 0 4px 15px rgba(231,76,60,0.4); letter-spacing: 1px; }
.menu-btn:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 8px 25px rgba(231,76,60,0.6); }
.menu-btn:active { transform: translateY(0) scale(0.98); }
.menu-btn.secondary { background: linear-gradient(135deg, #3498db, #2980b9); box-shadow: 0 4px 15px rgba(52,152,219,0.4); }
.menu-btn.secondary:hover { box-shadow: 0 8px 25px rgba(52,152,219,0.6); }
#setup-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #0a0a2e 0%, #1a0a3e 50%, #0a1a2e 100%); z-index: 99; }
#setup-screen h2 { color: #ff6b35; font-size: 42px; margin-bottom: 30px; text-shadow: 0 0 20px rgba(255,107,53,0.4); }
.setup-row { display: flex; align-items: center; gap: 20px; margin: 8px 0; color: #ccc; font-size: 18px; }
.setup-row label { min-width: 160px; text-align: right; }
.setup-row select, .setup-row input { background: #1a1a3e; border: 2px solid #444; color: white; padding: 8px 16px; border-radius: 8px; font-size: 16px; }
.setup-row select:focus, .setup-row input:focus { outline: none; border-color: #ff6b35; }
#hud-top { position: absolute; top: 0; left: 0; width: 100%; padding: 12px 24px; display: flex; justify-content: space-between; align-items: flex-start; pointer-events: none; z-index: 50; }
.team-info { background: rgba(0,0,0,0.7); border-radius: 12px; padding: 8px 16px; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); }
.team-info .team-name { font-weight: bold; font-size: 14px; margin-bottom: 4px; }
.team-info .unit-hp { display: flex; gap: 6px; align-items: center; }
.hp-bar-mini { width: 50px; height: 8px; background: #333; border-radius: 4px; overflow: hidden; }
.hp-bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
.unit-label { font-size: 11px; color: #aaa; min-width: 28px; }
#turn-timer-container { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; z-index: 50; text-align: center; display: none; }
#turn-timer-bar { width: 200px; height: 12px; background: rgba(0,0,0,0.6); border-radius: 6px; overflow: hidden; margin-top: 8px; }
#turn-timer-fill { height: 100%; background: linear-gradient(90deg, #e74c3c, #f39c12, #2ecc71); border-radius: 6px; transition: width 0.1s linear; }
#turn-timer-text { color: white; font-size: 28px; font-weight: bold; text-shadow: 0 2px 8px rgba(0,0,0,0.8); }
#weapon-panel { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 8px; padding: 12px 20px; background: rgba(0,0,0,0.75); border-radius: 16px; backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,0.1); z-index: 50; }
.weapon-slot { width: 64px; height: 64px; background: rgba(255,255,255,0.08); border-radius: 12px; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; border: 2px solid transparent; position: relative; }
.weapon-slot:hover { background: rgba(255,255,255,0.15); transform: translateY(-4px); }
.weapon-slot.selected { border-color: #ff6b35; background: rgba(255,107,53,0.2); box-shadow: 0 0 20px rgba(255,107,53,0.3); }
.weapon-slot .w-icon { font-size: 24px; }
.weapon-slot .w-name { font-size: 9px; color: #ccc; margin-top: 2px; text-align: center; line-height: 1.1; }
.weapon-slot .w-count { position: absolute; top: -6px; right: -6px; background: #e74c3c; color: white; font-size: 10px; width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: bold; }
#power-meter { position: absolute; right: 30px; bottom: 120px; width: 30px; height: 200px; background: rgba(0,0,0,0.6); border-radius: 15px; overflow: hidden; z-index: 50; display: none; }
#power-fill { position: absolute; bottom: 0; width: 100%; background: linear-gradient(to top, #2ecc71, #f39c12, #e74c3c); border-radius: 15px; transition: height 0.05s; }
#power-text { position: absolute; right: 68px; bottom: 120px; color: white; font-size: 14px; z-index: 50; display: none; text-shadow: 0 1px 4px rgba(0,0,0,0.8); }
#wind-indicator { position: absolute; top: 70px; left: 50%; transform: translateX(-50%); color: white; font-size: 13px; z-index: 50; text-align: center; background: rgba(0,0,0,0.5); padding: 6px 16px; border-radius: 20px; backdrop-filter: blur(5px); }
#wind-arrow { font-size: 20px; display: inline-block; transition: transform 0.3s; }
#action-buttons { position: absolute; bottom: 16px; right: 30px; display: flex; gap: 8px; z-index: 50; }
.action-btn { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; transition: all 0.2s; }
.action-btn:hover { background: rgba(255,255,255,0.2); }
#game-over-screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: none; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.85); z-index: 200; }
#game-over-screen h2 { font-size: 64px; color: #ff6b35; text-shadow: 0 0 40px rgba(255,107,53,0.5); margin-bottom: 20px; }
#game-over-screen p { color: #ccc; font-size: 24px; margin-bottom: 30px; }
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui-overlay">
<div id="menu-screen">
<h1>🐛 WORM WARFARE</h1>
<p class="subtitle">TACTICAL ARTILLERY COMBAT</p>
<button class="menu-btn" onclick="showSetup()">⚔️ START GAME</button>
</div>
<div id="setup-screen">
<h2>BATTLE SETUP</h2>
<div class="setup-row"><label>Teams:</label><select id="num-teams"><option value="2">2 Teams</option><option value="3">3 Teams</option><option value="4" selected>4 Teams</option></select></div>
<div class="setup-row"><label>Terrain:</label><select id="terrain-type"><option value="island">Single Island</option><option value="two-islands">Two Islands</option><option value="cave">Cave System</option></select></div>
<div class="setup-row"><label>CPU Players:</label><select id="cpu-count"><option value="0">None (Hot-seat)</option><option value="1">1 CPU</option><option value="2" selected>2 CPUs</option><option value="3">3 CPUs</option></select></div>
<button class="menu-btn" onclick="startGame()" style="margin-top:20px;">🚀 LAUNCH BATTLE</button>
<button class="menu-btn secondary" onclick="backToMenu()">← BACK</button>
</div>
<div id="hud-top"></div>
<div id="wind-indicator">Wind: <span id="wind-arrow">→</span> <span id="wind-val">0</span></div>
<div id="turn-timer-container"><div id="turn-timer-text">25</div><div id="turn-timer-bar"><div id="turn-timer-fill"></div></div></div>
<div id="weapon-panel"></div>
<div id="power-meter"><div id="power-fill" style="height:0%"></div></div>
<div id="power-text">Power</div>
<div id="action-buttons">
<button class="action-btn" onclick="skipTurn()">⏭ Skip Turn</button>
</div>
<div id="game-over-screen"><h2 id="winner-text"></h2><p>Battle Complete!</p><button class="menu-btn" onclick="backToMenu()">🔄 PLAY AGAIN</button></div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
resize();
window.addEventListener('resize', resize);
// Audio system
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(freq, duration, type='sine', vol=0.3, slide=0) {
const o = audioCtx.createOscillator();
const g = audioCtx.createGain();
o.type = type;
o.frequency.setValueAtTime(freq, audioCtx.currentTime);
if (slide) o.frequency.exponentialRampToValueAtTime(Math.max(slide,20), audioCtx.currentTime + duration);
g.gain.setValueAtTime(vol, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
o.connect(g); g.connect(audioCtx.destination);
o.start(); o.stop(audioCtx.currentTime + duration);
}
function sfxClick() { playSound(800, 0.05, 'square', 0.15); }
function sfxExplosion(big) {
const dur = big ? 0.6 : 0.3;
for(let i=0;i<3;i++) setTimeout(()=>playSound(80+i*20, dur, 'sawtooth', big?0.4:0.25, 20), i*30);
}
function sfxHit() { playSound(300, 0.15, 'square', 0.2, 100); }
function sfxJump() { playSound(400, 0.1, 'sine', 0.15, 600); }
function sfxLaser() { playSound(1200, 0.3, 'sawtooth', 0.2, 200); }
function sfxTick() { playSound(1000, 0.03, 'square', 0.08); }
function sfxTurnEnd() { for(let i=0;i<5;i++) setTimeout(()=>playSound(600-i*80,0.1,'sine',0.2),i*60); }
// Game state
let gameState = 'menu'; // menu, playing, gameover
let terrainData = null;
let terrainWidth = 3000;
let terrainHeight = 800;
let teams = [];
let currentTeamIdx = 0;
let currentUnitIdx = 0;
let turnTimeLeft = 25;
let turnTimerActive = false;
let selectedWeapon = null;
let isAiming = false;
let aimAngle = -Math.PI / 4;
let aimPower = 0;
let powerCharging = false;
let projectiles = [];
let particles = [];
let floatingTexts = [];
let debrisChunks = [];
let camera = { x: 0, y: 0, zoom: 1, targetX: 0, targetY: 0, targetZoom: 1 };
let wind = 0;
let screenShake = { x: 0, y: 0, intensity: 0 };
let impactFreeze = 0;
let waterLevel = -999;
let terrainImage = null;
const TEAM_COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'];
const TEAM_NAMES = ['Red Fury', 'Blue Storm', 'Green Viper', 'Gold Eagle'];
const WEAPONS = [
{ id:'rocket', name:'Rocket', icon:'🚀', type:'indirect', damage:40, aoe:60, speed:8, count:3, color:'#ff6b35' },
{ id:'grenade', name:'Grenade', icon:'💣', type:'indirect', damage:25, aoe:45, speed:10, count:4, color:'#f39c12' },
{ id:'shotgun', name:'Shotgun', icon:'🔫', type:'direct', damage:35, range:300, count:2, color:'#e74c3c' },
{ id:'laser', name:'Laser', icon:'⚡', type:'instant', damage:30, range:600, count:2, color:'#9b59b6' },
{ id:'nuke', name:'Nuke', icon:'☢️', type:'aoe', damage:80, aoe:150, speed:4, count:1, color:'#ff0000' },
{ id:'dynamite', name:'Dynamite', icon:'🧨', type:'placeable', damage:50, aoe:70, count:2, color:'#e67e22' },
{ id:'minigun', name:'Minigun', icon:'💥', type:'direct', damage:8, range:350, count:1, color:'#c0392b' },
];
const UTILITIES = [
{ id:'parachute', name:'Parachute', icon:'🪂', count:1 },
{ id:'jetpack', name:'Jetpack', icon:'🎒', count:1 },
{ id:'extratime', name:'+Time', icon:'⏰', count:1 },
];
class Unit {
constructor(teamIdx, x, y) {
this.team = teamIdx;
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.hp = 100;
this.maxHp = 100;
this.alive = true;
this.onGround = false;
this.facing = 1;
this.hasParachute = false;
this.hasJetpack = false;
this.jetpackFuel = 0;
this.inWater = false;
this.swimTimer = 0;
this.width = 24;
this.height = 36;
this.animFrame = 0;
this.bobTimer = Math.random() * Math.PI * 2;
}
}
class Projectile {
constructor(x, y, vx, vy, weapon, ownerTeam) {
this.x = x; this.y = y;
this.vx = vx; this.vy = vy;
this.weapon = weapon;
this.ownerTeam = ownerTeam;
this.alive = true;
this.trail = [];
this.bounces = 0;
this.fuseTimer = 0;
if (weapon.id === 'dynamite') {
this.isDynamite = true;
this.vx = 0; this.vy = 0;
this.placed = false;
this.timer = 180;
}
}
}
class Particle {
constructor(x, y, vx, vy, color, life, size) {
this.x = x; this.y = y;
this.vx = vx; this.vy = vy;
this.color = color;
this.life = life;
this.maxLife = life;
this.size = size || 3;
}
}
class FloatingText {
constructor(x, y, text, color) {
this.x = x; this.y = y;
this.text = text;
this.color = color;
this.life = 60;
this.maxLife = 60;
this.vy = -2;
}
}
class DebrisChunk {
constructor(x, y, w, h) {
this.x = x; this.y = y;
this.w = w; this.h = h;
this.vx = (Math.random()-0.5)*4;
this.vy = -Math.random()*3-1;
this.rotation = 0;
this.rotSpeed = (Math.random()-0.5)*0.2;
this.alive = true;
}
}
// Terrain generation
function generateTerrain(type) {
const data = new Uint8Array(terrainWidth * terrainHeight);
if (type === 'island') {
for (let x = 0; x < terrainWidth; x++) {
let baseH = terrainHeight * 0.55;
let h = baseH + Math.sin(x*0.008)*60 + Math.sin(x*0.02)*30 + Math.sin(x*0.05)*15 + Math.sin(x*0.003)*40;
for (let y = 0; y < terrainHeight; y++) {
if (y > h) data[y * terrainWidth + x] = 200;
}
}
waterLevel = terrainHeight * 0.75;
} else if (type === 'two-islands') {
for (let x = 0; x < terrainWidth; x++) {
let baseH = terrainHeight * 0.65;
let island1 = Math.exp(-Math.pow((x - terrainWidth*0.3)/200, 2)) * 200;
let island2 = Math.exp(-Math.pow((x - terrainWidth*0.7)/200, 2)) * 200;
let h = baseH + island1 + island2 + Math.sin(x*0.03)*20 + Math.sin(x*0.07)*10;
for (let y = 0; y < terrainHeight; y++) {
if (y > h) data[y * terrainWidth + x] = 200;
}
}
waterLevel = terrainHeight * 0.68;
} else if (type === 'cave') {
for (let x = 0; x < terrainWidth; x++) {
let topH = terrainHeight * 0.2 + Math.sin(x*0.01)*30 + Math.sin(x*0.03)*15;
let botH = terrainHeight * 0.7 + Math.sin(x*0.008+1)*40 + Math.sin(x*0.025)*20;
for (let y = 0; y < terrainHeight; y++) {
if (y < topH || y > botH) data[y * terrainWidth + x] = 200;
}
}
waterLevel = -999;
}
return data;
}
function getTerrainPixel(x, y) {
if (x < 0 || x >= terrainWidth || y < 0 || y >= terrainHeight) return 255;
return terrainData[y * terrainWidth + x];
}
function setTerrainPixel(x, y, val) {
if (x < 0 || x >= terrainWidth || y < 0 || y >= terrainHeight) return;
terrainData[y * terrainWidth + x] = val;
}
function destroyTerrain(cx, cy, radius) {
const r2 = radius * radius;
let chunks = [];
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (dx*dx + dy*dy > r2) continue;
const tx = Math.floor(cx + dx);
const ty = Math.floor(cy + dy);
if (tx < 0 || tx >= terrainWidth || ty < 0 || ty >= terrainHeight) continue;
if (terrainData[ty * terrainWidth + tx] > 0) {
// Spawn debris chunks for larger explosions
if (radius > 40 && Math.random() < 0.15) {
const cw = 3 + Math.floor(Math.random()*8);
const ch = 2 + Math.floor(Math.random()*6);
chunks.push(new DebrisChunk(tx, ty, cw, ch));
}
setTerrainPixel(tx, ty, 0);
}
}
}
return chunks;
}
function spawnParticles(x, y, count, color, speed, life) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const spd = Math.random() * speed;
particles.push(new Particle(x, y, Math.cos(angle)*spd, Math.sin(angle)*spd, color, life + Math.random()*life*0.5, 1+Math.random()*3));
}
}
function spawnExplosionParticles(x, y, radius) {
const colors = ['#ff6b35','#e74c3c','#f39c12','#fff','#ffaa00'];
for (let i = 0; i < radius * 2; i++) {
const angle = Math.random() * Math.PI * 2;
const spd = Math.random() * radius * 0.4 + 1;
particles.push(new Particle(x, y, Math.cos(angle)*spd, Math.sin(angle)*spd, colors[Math.floor(Math.random()*colors.length)], 30+Math.random()*40, 2+Math.random()*5));
}
}
function addFloatingText(x, y, text, color) {
floatingTexts.push(new FloatingText(x, y, text, color || '#fff'));
}
// Terrain rendering (cached)
let terrainCanvas = null;
function renderTerrainToCanvas() {
terrainCanvas = document.createElement('canvas');
terrainCanvas.width = terrainWidth;
terrainCanvas.height = terrainHeight;
const tctx = terrainCanvas.getContext('2d');
const imgData = tctx.createImageData(terrainWidth, terrainHeight);
for (let i = 0; i < terrainData.length; i++) {
const v = terrainData[i];
if (v > 0) {
const y = Math.floor(i / terrainWidth);
// Gradient based on depth
const depthFactor = Math.min(1, y / terrainHeight);
const r = Math.floor(60 + depthFactor * 40);
const g = Math.floor(120 - depthFactor * 30 + (y < waterLevel ? 20 : 0));
const b = Math.floor(50 + depthFactor * 20);
imgData.data[i*4] = r;
imgData.data[i*4+1] = g;
imgData.data[i*4+2] = b;
imgData.data[i*4+3] = 255;
} else {
imgData.data[i*4+3] = 0;
}
}
tctx.putImageData(imgData, 0, 0);
}
// Initialize game
function initGame() {
const numTeams = parseInt(document.getElementById('num-teams').value);
const terrainType = document.getElementById('terrain-type').value;
const cpuCount = parseInt(document.getElementById('cpu-count').value);
terrainData = generateTerrain(terrainType);
renderTerrainToCanvas();
teams = [];
for (let t = 0; t < numTeams; t++) {
teams.push({
name: TEAM_NAMES[t],
color: TEAM_COLORS[t],
isCPU: t >= numTeams - cpuCount,
units: [],
weapons: WEAPONS.map(w => ({...w})),
utilities: UTILITIES.map(u => ({...u}))
});
}
// Place units on terrain
for (let t = 0; t < numTeams.length; t++) {
const baseX = terrainWidth * (t + 1) / (numTeams + 1);
for (let u = 0; u < 4; u++) {
let px = baseX + (u - 1.5) * 60;
// Find ground level
let py = 0;
for (let y = 0; y < terrainHeight; y++) {
if (getTerrainPixel(Math.floor(px), y) > 0) { py = y - 20; break; }
}
teams[t].units.push(new Unit(t, px, py));
}
}
currentTeamIdx = 0;
currentUnitIdx = 0;
wind = (Math.random() - 0.5) * 4;
projectiles = [];
particles = [];
floatingTexts = [];
debrisChunks = [];
selectedWeapon = null;
isAiming = false;
startTurn();
}
function getActiveUnit() {
if (currentTeamIdx >= teams.length) return null;
const team = teams[currentTeamIdx];
for (let i = currentUnitIdx; i < team.units.length; i++) {
if (team.units[i].alive) return team.units[i];
}
return null;
}
function startTurn() {
// Find next alive unit
let found = false;
while (currentTeamIdx < teams.length) {
const team = teams[currentTeamIdx];
for (let i = currentUnitIdx; i < team.units.length; i++) {
if (team.units[i].alive) {
currentUnitIdx = i;
found = true;
break;
}
}
if (!found) {
currentTeamIdx++;
currentUnitIdx = 0;
} else break;
}
if (currentTeamIdx >= teams.length) {
endGame();
return;
}
const unit = getActiveUnit();
if (!unit) { endGame(); return; }
turnTimeLeft = 25;
selectedWeapon = null;
isAiming = false;
aimAngle = -Math.PI / 4;
aimPower = 0;
// Change wind occasionally
if (Math.random() < 0.3) {
wind += (Math.random() - 0.5) * 2;
wind = Math.max(-6, Math.min(6, wind));
}
updateHUD();
updateWeaponPanel();
document.getElementById('turn-timer-container').style.display = 'block';
turnTimerActive = true;
if (teams[currentTeamIdx].isCPU) {
setTimeout(() => cpuTurn(), 1000);
}
}
function endTurn() {
turnTimerActive = false;
sfxTurnEnd();
// Advance to next unit/team
currentUnitIdx++;
if (currentUnitIdx >= teams[currentTeamIdx].units.length) {
currentUnitIdx = 0;
currentTeamIdx++;
}
startTurn();
}
function skipTurn() {
sfxClick();
endTurn();
}
// Weapon firing
function fireWeapon(weapon, angle, power) {
const unit = getActiveUnit();
if (!unit || !weapon) return;
// Decrement weapon count
const teamWeapons = teams[currentTeamIdx].weapons;
const w = teamWeapons.find(w => w.id === weapon.id);
if (w && w.count > 0) {
w.count--;
} else return;
sfxClick();
if (weapon.type === 'instant') {
// Laser - instant hit
const range = weapon.range || 500;
const endX = unit.x + Math.cos(angle) * range;
const endY = unit.y + Math.sin(angle) * range;
sfxLaser();
// Check collision with units along the line
for (let t = 0; t < teams.length; t++) {
if (t === currentTeamIdx && weapon.id !== 'nuke') continue;
for (const u of teams[t].units) {
if (!u.alive) continue;
const dist = pointToSegmentDist(u.x, u.y, unit.x, unit.y, endX, endY);
if (dist < 20) {
damageUnit(u, weapon.damage, currentTeamIdx);
}
}
}
// Laser particles
const steps = 30;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const lx = unit.x + (endX - unit.x) * t;
const ly = unit.y + (endY - unit.y) * t;
spawnParticles(lx, ly, 2, '#9b59b6', 2, 15);
}
updateWeaponPanel();
return;
}
if (weapon.type === 'direct') {
// Shotgun or minigun - multiple pellets
const count = weapon.id === 'shotgun' ? 8 : 15;
const spread = weapon.id === 'shotgun' ? 0.3 : 0.2;
for (let i = 0; i < count; i++) {
const a = angle + (Math.random() - 0.5) * spread;
const spd = power * 15 + weapon.speed * 5;
projectiles.push(new Projectile(unit.x, unit.y - 20, Math.cos(a)*spd, Math.sin(a)*spd, weapon, currentTeamIdx));
}
updateWeaponPanel();
return;
}
if (weapon.type === 'placeable') {
// Dynamite - place at aim point
const dist = power * 10 + 50;
const dx = unit.x + Math.cos(angle) * dist;
const dy = unit.y + Math.sin(angle) * dist;
projectiles.push(new Projectile(dx, dy, 0, 0, weapon, currentTeamIdx));
updateWeaponPanel();
return;
}
// Indirect projectile (rocket, grenade, nuke)
const spd = power * 12 + weapon.speed * 3;
projectiles.push(new Projectile(unit.x, unit.y - 20, Math.cos(angle)*spd, Math.sin(angle)*spd, weapon, currentTeamIdx));
updateWeaponPanel();
}
function pointToSegmentDist(px, py, x1, y1, x2, y2) {
const dx = x2-x1, dy = y2-y1;
const len2 = dx*dx+dy*dy;
if (len2 === 0) return Math.hypot(px-x1, py-y1);
let t = ((px-x1)*dx+(py-y1)*dy)/len2;
t = Math.max(0, Math.min(1, t));
return Math.hypot(px-(x1+t*dx), py-(y1+t*dy));
}
function damageUnit(unit, damage, attackerTeam) {
if (!unit.alive) return;
unit.hp -= damage;
addFloatingText(unit.x, unit.y - 30, `-${damage}`, '#ff4444');
sfxHit();
spawnParticles(unit.x, unit.y, 15, TEAM_COLORS[unit.team], 4, 25);
if (unit.hp <= 0) {
unit.alive = false;
addFloatingText(unit.x, unit.y - 40, '💀', '#ff0000');
spawnParticles(unit.x, unit.y, 40, TEAM_COLORS[unit.team], 6, 50);
// Check if team is eliminated
const team = teams[attackerTeam === undefined ? unit.team : attackerTeam];
if (teams[unit.team].units.every(u => !u.alive)) {
addFloatingText(unit.x, unit.y - 60, `${TEAM_NAMES[unit.team]} ELIMINATED!`, '#ff0000');
}
}
}
function explodeProjectile(proj) {
proj.alive = false;
const w = proj.weapon;
sfxExplosion(w.id === 'nuke' || w.aoe > 100);
// Terrain destruction
const newDebris = destroyTerrain(proj.x, proj.y, w.aoe);
debrisChunks.push(...newDebris);
spawnExplosionParticles(proj.x, proj.y, w.aoe);
if (w.id === 'nuke') {
screenShake.intensity = 15;
impactFreeze = 20;
} else if (w.aoe > 60) {
screenShake.intensity = 8;
impactFreeze = 8;
} else {
screenShake.intensity = 4;
impactFreeze = 3;
}
// Damage units in AoE
for (const team of teams) {
for (const unit of team.units) {
if (!unit.alive) continue;
const dist = Math.hypot(unit.x - proj.x, unit.y - proj.y);
if (dist < w.aoe) {
const dmg = Math.floor(w.damage * (1 - dist / w.aoe));
damageUnit(unit, Math.max(1, dmg), proj.ownerTeam);
// Knockback
const kb = 5 * (1 - dist / w.aoe);
const angle = Math.atan2(unit.y - proj.y, unit.x - proj.x);
unit.vx += Math.cos(angle) * kb;
unit.vy += Math.sin(angle) * kb - 3;
}
}
}
// Check game over
checkGameOver();
}
function checkGameOver() {
const aliveTeams = teams.filter(t => t.units.some(u => u.alive));
if (aliveTeams.length <= 1) {
setTimeout(() => endGame(), 2000);
}
}
function endGame() {
gameState = 'gameover';
const winner = teams.find(t => t.units.some(u => u.alive));
document.getElementById('winner-text').textContent = winner ? `${winner.name} WINS!` : 'DRAW!';
document.getElementById('game-over-screen').style.display = 'flex';
document.getElementById('turn-timer-container').style.display = 'none';
}
// Physics update
function updatePhysics(dt) {
const gravity = 0.4;
// Update units
for (const team of teams) {
for (const unit of team.units) {
if (!unit.alive) continue;
unit.bobTimer += 0.05;
// Gravity
unit.vy += gravity;
// Wind effect on airborne units
if (!unit.onGround && !unit.hasParachute) {
unit.vx += wind * 0.01;
}
// Friction
if (unit.onGround) {
unit.vx *= 0.85;
} else {
unit.vx *= 0.99;
}
// Jetpack
if (unit.hasJetpack && unit.jetpackFuel > 0 && keys['KeyW']) {
unit.vy -= 1.2;
unit.jetpackFuel--;
spawnParticles(unit.x, unit.y + 15, 2, '#ffaa00', 3, 15);
}
// Parachute
if (unit.hasParachute && !unit.onGround) {
unit.vy = Math.min(unit.vy, 2);
spawnParticles(unit.x + (Math.random()-0.5)*20, unit.y - 10, 1, '#fff', 1, 20);
}
// Move
unit.x += unit.vx;
unit.y += unit.vy;
// Terrain collision
unit.onGround = false;
const feetY = Math.floor(unit.y + unit.height/2);
const leftX = Math.floor(unit.x - unit.width/3);
const rightX = Math.floor(unit.x + unit.width/3);
if (feetY < terrainHeight && getTerrainPixel(leftX, feetY) > 0 || getTerrainPixel(rightX, feetY) > 0) {
// Find exact ground level
for (let gy = feetY; gy >= 0; gy--) {
if (getTerrainPixel(Math.floor(unit.x), gy) === 0) {
unit.y = gy - unit.height/2;
unit.vy = 0;
unit.onGround = true;
// Fall damage
if (unit.vy > 8) {
const fallDmg = Math.floor((unit.vy - 8) * 3);
damageUnit(unit, fallDmg, -1);
}
break;
}
}
}
// Water physics
if (waterLevel > 0 && unit.y + unit.height/2 > waterLevel) {
unit.inWater = true;
unit.swimTimer++;
unit.vy *= 0.9;
unit.vx *= 0.95;
// Drowning
if (unit.swimTimer > 300 && !unit.hasJetpack) {
damageUnit(unit, 1, -1);
}
} else {
unit.inWater = false;
unit.swimTimer = Math.max(0, unit.swimTimer - 2);
}
// Keep in bounds
unit.x = Math.max(20, Math.min(terrainWidth - 20, unit.x));
if (unit.y > terrainHeight + 100) {
damageUnit(unit, 999, -1);
}
}
}
// Update projectiles
for (const proj of projectiles) {
if (!proj.alive) continue;
if (proj.isDynamite) {
proj.timer--;
if (proj.timer <= 0) {
explodeProjectile(proj);
}
// Dynamite bobbing
proj.y += Math.sin(Date.now() * 0.01) * 0.3;
continue;
}
proj.vy += gravity;
proj.vx += wind * 0.02;
proj.x += proj.vx;
proj.y += proj.vy;
// Trail
proj.trail.push({x: proj.x, y: proj.y});
if (proj.trail.length > 20) proj.trail.shift();
spawnParticles(proj.x, proj.y, 1, '#ffaa00', 1, 10);
// Terrain collision
if (getTerrainPixel(Math.floor(proj.x), Math.floor(proj.y)) > 0) {
explodeProjectile(proj);
continue;
}
// Out of bounds
if (proj.x < -50 || proj.x > terrainWidth + 50 || proj.y > terrainHeight + 50) {
proj.alive = false;
}
// Direct hit collision with units
if (proj.weapon.type === 'direct') {
for (const team of teams) {
for (const unit of team.units) {
if (!unit.alive) continue;
const dist = Math.hypot(unit.x - proj.x, unit.y - proj.y);
if (dist < 18) {
damageUnit(unit, proj.weapon.damage, proj.ownerTeam);
spawnParticles(proj.x, proj.y, 5, '#ff4444', 3, 15);
proj.alive = false;
}
}
}
}
// Fuse timer for rockets
if (proj.weapon.id === 'rocket') {
proj.fuseTimer++;
if (proj.fuseTimer > 300) explodeProjectile(proj);
}
}
projectiles = projectiles.filter(p => p.alive);
// Update debris chunks
for (const chunk of debrisChunks) {
if (!chunk.alive) continue;
chunk.vy += gravity * 0.8;
chunk.x += chunk.vx;
chunk.y += chunk.vy;
chunk.rotation += chunk.rotSpeed;
// Ground collision
if (getTerrainPixel(Math.floor(chunk.x), Math.floor(chunk.y + chunk.h/2)) > 0) {
chunk.alive = false;
spawnParticles(chunk.x, chunk.y, 3, '#8B7355', 2, 15);
}
if (chunk.y > terrainHeight + 50) chunk.alive = false;
}
debrisChunks = debrisChunks.filter(d => d.alive);
// Update particles
for (const p of particles) {
p.vy += 0.1;
p.x += p.vx;
p.y += p.vy;
p.life--;
}
particles = particles.filter(p => p.life > 0);
// Update floating texts
for (const ft of floatingTexts) {
ft.y += ft.vy;
ft.life--;
}
floatingTexts = floatingTexts.filter(ft => ft.life > 0);
// Screen shake decay
if (screenShake.intensity > 0) {
screenShake.x = (Math.random() - 0.5) * screenShake.intensity;
screenShake.y = (Math.random() - 0.5) * screenShake.intensity;
screenShake.intensity *= 0.9;
if (screenShake.intensity < 0.5) screenShake.intensity = 0;
}
}
// Camera
function updateCamera() {
const unit = getActiveUnit();
if (unit && unit.alive) {
camera.targetX = unit.x;
camera.targetY = unit.y - 100;
camera.targetZoom = Math.max(0.5, Math.min(1.2, 800 / terrainWidth));
}
camera.x += (camera.targetX - camera.x) * 0.05;
camera.y += (camera.targetY - camera.y) * 0.05;
camera.zoom += (camera.targetZoom - camera.zoom) * 0.03;
}
// Rendering
function render() {
ctx.clearRect(0, 0, W, H);
// Sky gradient
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#1a0533');
skyGrad.addColorStop(0.4, '#2d1b69');
skyGrad.addColorStop(0.7, '#4a2c8a');
skyGrad.addColorStop(1, '#1a3a5c');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// Stars
ctx.save();
for (let i = 0; i < 80; i++) {
const sx = ((i * 137.5 + 50) % terrainWidth - camera.x * 0.3) / camera.zoom + W/2;
const sy = ((i * 97.3 + 30) % (terrainHeight * 0.4)) / camera.zoom + H * 0.1;
if (sx > 0 && sx < W && sy > 0 && sy < H * 0.5) {
const brightness = 0.3 + Math.sin(Date.now() * 0.002 + i) * 0.2;
ctx.fillStyle = `rgba(255,255,255,${brightness})`;
ctx.fillRect(sx, sy, 1.5, 1.5);
}
}
ctx.restore();
if (impactFreeze > 0) { impactFreeze--; return; }
ctx.save();
ctx.translate(screenShake.x, screenShake.y);
ctx.translate(W/2, H/2);
ctx.scale(camera.zoom, camera.zoom);
ctx.translate(-camera.x, -camera.y);
// Draw terrain
if (terrainCanvas) {
ctx.drawImage(terrainCanvas, 0, 0);
}
// Water
if (waterLevel > 0) {
const waterGrad = ctx.createLinearGradient(0, waterLevel, 0, terrainHeight);
waterGrad.addColorStop(0, 'rgba(30,100,200,0.4)');
waterGrad.addColorStop(1, 'rgba(10,50,120,0.7)');
ctx.fillStyle = waterGrad;
ctx.fillRect(0, waterLevel, terrainWidth, terrainHeight - waterLevel);
// Water surface waves
ctx.strokeStyle = 'rgba(100,180,255,0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
for (let x = 0; x < terrainWidth; x += 5) {
const wy = waterLevel + Math.sin(x * 0.05 + Date.now() * 0.003) * 3;
if (x === 0) ctx.moveTo(x, wy);
else ctx.lineTo(x, wy);
}
ctx.stroke();
}
// Draw debris chunks
for (const chunk of debrisChunks) {
ctx.save();
ctx.translate(chunk.x, chunk.y);
ctx.rotate(chunk.rotation);
ctx.fillStyle = '#8B7355';
ctx.fillRect(-chunk.w/2, -chunk.h/2, chunk.w, chunk.h);
ctx.restore();
}
// Draw units
for (const team of teams) {
for (const unit of team.units) {
if (!unit.alive) continue;
drawUnit(unit, team.color);
}
}
// Draw projectiles
for (const proj of projectiles) {
if (!proj.alive) continue;
// Trail
if (proj.trail.length > 1) {
ctx.strokeStyle = 'rgba(255,170,0,0.4)';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < proj.trail.length; i++) {
if (i === 0) ctx.moveTo(proj.trail[i].x, proj.trail[i].y);
else ctx.lineTo(proj.trail[i].x, proj.trail[i].y);
}
ctx.stroke();
}
// Projectile body
const w = proj.weapon;
if (proj.isDynamite) {
ctx.fillStyle = '#8B4513';
ctx.fillRect(proj.x - 6, proj.y - 10, 12, 20);
ctx.fillStyle = '#ff4444';
ctx.beginPath();
ctx.arc(proj.x, proj.y - 12, 3, 0, Math.PI * 2);
ctx.fill();
// Fuse spark
if (proj.timer < 60) {
spawnParticles(proj.x + (Math.random()-0.5)*4, proj.y - 14, 1, '#ffaa00', 2, 10);
}
} else {
ctx.fillStyle = w.color;
ctx.beginPath();
ctx.arc(proj.x, proj.y, w.id === 'nuke' ? 8 : 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(proj.x, proj.y, w.id === 'nuke' ? 4 : 2, 0, Math.PI * 2);
ctx.fill();
}
}
// Draw particles
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// Draw floating texts
for (const ft of floatingTexts) {
const alpha = ft.life / ft.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = ft.color;
ctx.font = 'bold 18px Arial';
ctx.textAlign = 'center';
ctx.fillText(ft.text, ft.x, ft.y);
}
ctx.globalAlpha = 1;
// Draw aiming trajectory
if (isAiming && selectedWeapon && getActiveUnit() && !teams[currentTeamIdx].isCPU) {
drawTrajectory();
}
ctx.restore();
}
function drawUnit(unit, color) {
const bob = Math.sin(unit.bobTimer) * 2;
const x = unit.x;
const y = unit.y + bob;
// Shadow
ctx.fillStyle = 'rgba(0,0,0,0.3)';
ctx.beginPath();
ctx.ellipse(x, unit.y + unit.height/2 + 2, 14, 5, 0, 0, Math.PI * 2);
ctx.fill();
// Body
ctx.fillStyle = color;
ctx.beginPath();
ctx.ellipse(x, y, 12, 16, 0, 0, Math.PI * 2);
ctx.fill();
// Outline
ctx.strokeStyle = 'rgba(0,0,0,0.5)';
ctx.lineWidth = 2;
ctx.stroke();
// Eyes
const eyeOffX = unit.facing * 3;
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x + eyeOffX - 4, y - 4, 4, 0, Math.PI * 2);
ctx.arc(x + eyeOffX + 4, y - 4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#111';
ctx.beginPath();
ctx.arc(x + eyeOffX - 3 + unit.facing, y - 4, 2, 0, Math.PI * 2);
ctx.arc(x + eyeOffX + 5 + unit.facing, y - 4, 2, 0, Math.PI * 2);
ctx.fill();
// Mouth
if (unit.hp < 30) {
ctx.strokeStyle = '#111';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(x + eyeOffX, y + 4, 3, Math.PI, 0);
ctx.stroke();
} else {
ctx.fillStyle = '#111';
ctx.fillRect(x + eyeOffX - 2, y + 3, 4, 2);
}
// Hat/helmet indicator for active unit
const isActive = getActiveUnit() === unit;
if (isActive) {
ctx.fillStyle = '#ffdd00';
ctx.beginPath();
ctx.moveTo(x - 8, y - 16);
ctx.lineTo(x, y - 28);
ctx.lineTo(x + 8, y - 16);
ctx.fill();
// Pulsing indicator
const pulse = Math.sin(Date.now() * 0.005) * 0.3 + 0.7;
ctx.strokeStyle = `rgba(255,221,0,${pulse})`;
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(x, y, 22, 0, Math.PI * 2);
ctx.stroke();
}
// HP bar
if (unit.hp < unit.maxHp) {
const barW = 30;
const hpRatio = unit.hp / unit.maxHp;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(x - barW/2, y - 24, barW, 5);
ctx.fillStyle = hpRatio > 0.5 ? '#2ecc71' : hpRatio > 0.25 ? '#f39c12' : '#e74c3c';
ctx.fillRect(x - barW/2, y - 24, barW * hpRatio, 5);
}
// Parachute visual
if (unit.hasParachute && !unit.onGround) {
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(x, y - 30, 15, Math.PI, 0);
ctx.fill();
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x - 15, y - 30);
ctx.lineTo(x, y - 16);
ctx.moveTo(x + 15, y - 30);
ctx.lineTo(x, y - 16);
ctx.stroke();
}
// Jetpack visual
if (unit.hasJetpack) {
ctx.fillStyle = '#666';
ctx.fillRect(x - 8, y + 2, 5, 10);
ctx.fillRect(x + 3, y + 2, 5, 10);
}
// Water indicator
if (unit.inWater) {
ctx.fillStyle = 'rgba(100,180,255,0.3)';
ctx.beginPath();
ctx.arc(x, y, 18, 0, Math.PI * 2);
ctx.fill();
}
}
function drawTrajectory() {
const unit = getActiveUnit();
if (!unit || !selectedWeapon) return;
const power = aimPower > 0 ? aimPower : 50;
const spd = power * 12 + selectedWeapon.speed * 3;
let px = unit.x, py = unit.y - 20;
let vx = Math.cos(aimAngle) * spd;
let vy = Math.sin(aimAngle) * spd;
ctx.setLineDash([5, 8]);
ctx.strokeStyle = 'rgba(255,255,255,0.4)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(px, py);
for (let i = 0; i < 60; i++) {
vy += 0.4;
vx += wind * 0.02;
px += vx;
py += vy;
if (getTerrainPixel(Math.floor(px), Math.floor(py)) > 0) break;
if (px < 0 || px > terrainWidth || py > terrainHeight) break;
ctx.lineTo(px, py);
}
ctx.stroke();
ctx.setLineDash([]);
// AoE preview
if (selectedWeapon.aoe) {
ctx.strokeStyle = 'rgba(255,100,50,0.3)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(px, py, selectedWeapon.aoe, 0, Math.PI * 2);
ctx.stroke();
}
}
// HUD updates
function updateHUD() {
const hudTop = document.getElementById('hud-top');
hudTop.innerHTML = '';
for (let t = 0; t < teams.length; t++) {
const team = teams[t];
const aliveCount = team.units.filter(u => u.alive).length;
if (aliveCount === 0) continue;
const div = document.createElement('div');
div.className = 'team-info';
div.style.borderLeft = `3px solid ${team.color}`;
let html = `<div class="team-name" style="color:${team.color}">${team.name} ${team.isCPU ? '(CPU)' : ''}</div>`;
html += '<div class="unit-hp">';
for (const unit of team.units) {
if (!unit.alive) continue;
const hpPct = Math.max(0, unit.hp / unit.maxHp * 100);
const hpColor = hpPct > 50 ? '#2ecc71' : hpPct > 25 ? '#f39c12' : '#e74c3c';
html += `<span class="unit-label">W${teams[t].units.indexOf(unit)+1}</span>`;
html += `<div class="hp-bar-mini"><div class="hp-bar-fill" style="width:${hpPct}%;background:${hpColor}"></div></div>`;
}
html += '</div>';
div.innerHTML = html;
hudTop.appendChild(div);
}
// Wind indicator
const windArrow = document.getElementById('wind-arrow');
const windVal = document.getElementById('wind-val');
windVal.textContent = Math.abs(wind).toFixed(1);
if (Math.abs(wind) < 0.3) {
windArrow.style.transform = 'rotate(-90deg)';
windArrow.textContent = '•';
} else {
windArrow.style.transform = `rotate(${wind > 0 ? 0 : 180}deg)`;
windArrow.textContent = '→';
}
}
function updateWeaponPanel() {
const panel = document.getElementById('weapon-panel');
panel.innerHTML = '';
if (currentTeamIdx >= teams.length) return;
const teamWeapons = teams[currentTeamIdx].weapons;
for (const w of teamWeapons) {
if (w.count <= 0) continue;
const slot = document.createElement('div');
slot.className = 'weapon-slot' + (selectedWeapon && selectedWeapon.id === w.id ? ' selected' : '');
slot.innerHTML = `<span class="w-icon">${w.icon}</span><span class="w-name">${w.name}</span><span class="w-count">${w.count}</span>`;
slot.onclick = () => {
sfxClick();
if (selectedWeapon && selectedWeapon.id === w.id) {
selectedWeapon = null;
isAiming = false;
document.getElementById('power-meter').style.display = 'none';
document.getElementById('power-text').style.display = 'none';
} else {
selectedWeapon = w;
isAiming = true;
aimAngle = -Math.PI / 4;
if (w.type === 'direct' || w.type === 'instant') {
document.getElementById('power-meter').style.display = 'none';
document.getElementById('power-text').style.display = 'none';
} else {
document.getElementById('power-meter').style.display = 'block';
document.getElementById('power-text').style.display = 'block';
}
}
updateWeaponPanel();
};
panel.appendChild(slot);
}
// Utility items
const teamUtils = teams[currentTeamIdx].utilities;
for (const u of teamUtils) {
if (u.count <= 0) continue;
const slot = document.createElement('div');
slot.className = 'weapon-slot';
slot.innerHTML = `<span class="w-icon">${u.icon}</span><span class="w-name">${u.name}</span><span class="w-count">${u.count}</span>`;
slot.onclick = () => {
sfxClick();
useUtility(u);
u.count--;
updateWeaponPanel();
};
panel.appendChild(slot);
}
}
function useUtility(util) {
const unit = getActiveUnit();
if (!unit || !unit.alive) return;
switch (util.id) {
case 'parachute':
unit.hasParachute = true;
addFloatingText(unit.x, unit.y - 40, '🪂 Parachute!', '#fff');
break;
case 'jetpack':
unit.hasJetpack = true;
unit.jetpackFuel = 120;
addFloatingText(unit.x, unit.y - 40, '🎒 Jetpack!', '#ffaa00');
break;
case 'extratime':
turnTimeLeft += 15;
addFloatingText(unit.x, unit.y - 40, '+15s', '#2ecc71');
break;
}
}
// Input handling
const keys = {};
let mouseX = 0, mouseY = 0;
let mouseDown = false;
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (gameState !== 'playing') return;
if (teams[currentTeamIdx]?.isCPU) return;
const unit = getActiveUnit();
if (!unit || !unit.alive) return;
// Movement
if (e.code === 'KeyA' || e.code === 'ArrowLeft') {
unit.vx -= 5;
unit.facing = -1;
}
if (e.code === 'KeyD' || e.code === 'ArrowRight') {
unit.vx += 5;
unit.facing = 1;
}
if ((e.code === 'KeyW' || e.code === 'ArrowUp' || e.code === 'Space') && unit.onGround) {
unit.vy = -10;
sfxJump();
}
// Utility keys
if (e.code === 'Digit1') selectWeaponByIndex(0);
if (e.code === 'Digit2') selectWeaponByIndex(1);
if (e.code === 'Digit3') selectWeaponByIndex(2);
if (e.code === 'Digit4') selectWeaponByIndex(3);
if (e.code === 'Digit5') selectWeaponByIndex(4);
if (e.code === 'Digit6') selectWeaponByIndex(5);
if (e.code === 'Digit7') selectWeaponByIndex(6);
});
document.addEventListener('keyup', e => { keys[e.code] = false; });
canvas.addEventListener('mousemove', e => {
mouseX = e.clientX;
mouseY = e.clientY;
if (isAiming && getActiveUnit() && !teams[currentTeamIdx]?.isCPU) {
const unit = getActiveUnit();
const worldX = (e.clientX - W/2) / camera.zoom + camera.x;
const worldY = (e.clientY - H/2) / camera.zoom + camera.y;
aimAngle = Math.atan2(worldY - (unit.y - 20), worldX - unit.x);
// Clamp angle to upward
if (aimAngle > 0.1) aimAngle = 0.1;
if (aimAngle < -Math.PI + 0.1) aimAngle = -Math.PI + 0.1;
}
});
canvas.addEventListener('mousedown', e => {
mouseDown = true;
if (isAiming && selectedWeapon && !teams[currentTeamIdx]?.isCPU) {
powerCharging = true;
aimPower = 0;
}
});
canvas.addEventListener('mouseup', e => {
mouseDown = false;
if (powerCharging && isAiming && selectedWeapon && getActiveUnit() && !teams[currentTeamIdx]?.isCPU) {
powerCharging = false;
fireWeapon(selectedWeapon, aimAngle, aimPower / 100);
isAiming = false;
selectedWeapon = null;
document.getElementById('power-meter').style.display = 'none';
document.getElementById('power-text').style.display = 'none';
updateWeaponPanel();
}
});
function selectWeaponByIndex(idx) {
if (currentTeamIdx >= teams.length || teams[currentTeamIdx].isCPU) return;
const teamWeapons = teams[currentTeamIdx].weapons;
if (idx < teamWeapons.length && teamWeapons[idx].count > 0) {
selectedWeapon = teamWeapons[idx];
isAiming = true;
sfxClick();
updateWeaponPanel();
}
}
// AI System
function cpuTurn() {
const unit = getActiveUnit();
if (!unit || !unit.alive) { endTurn(); return; }
// Find nearest enemy
let nearestEnemy = null;
let nearestDist = Infinity;
for (const team of teams) {
if (team === teams[currentTeamIdx]) continue;
for (const eu of team.units) {
if (!eu.alive) continue;
const d = Math.hypot(eu.x - unit.x, eu.y - unit.y);
if (d < nearestDist) {
nearestDist = d;
nearestEnemy = eu;
}
}
}
if (!nearestEnemy) { endTurn(); return; }
// Risk assessment: check for hazards
const groundBelow = getTerrainPixel(Math.floor(unit.x), Math.floor(unit.y + unit.height/2 + 5));
const nearWater = waterLevel > 0 && unit.y + unit.height/2 > waterLevel - 30;
// Move away from danger if needed
if (nearWater || groundBelow === 0) {
const moveDir = unit.x < terrainWidth / 2 ? 1 : -1;
unit.vx += moveDir * 4;
setTimeout(() => cpuAttack(nearestEnemy), 500);
return;
}
// Try to get closer if far away
if (nearestDist > 400) {
const dir = nearestEnemy.x > unit.x ? 1 : -1;
unit.vx += dir * 3;
setTimeout(() => cpuAttack(nearestEnemy), 500);
return;
}
cpuAttack(nearestEnemy);
}
function cpuAttack(target) {
const unit = getActiveUnit();
if (!unit || !unit.alive) { endTurn(); return; }
// Select weapon based on distance and situation
const dist = Math.hypot(target.x - unit.x, target.y - unit.y);
const teamWeapons = teams[currentTeamIdx].weapons.filter(w => w.count > 0);
if (teamWeapons.length === 0) { endTurn(); return; }
let weapon;
if (dist < 200) {
// Close range - use direct weapons
const direct = teamWeapons.find(w => w.type === 'direct');
weapon = direct || teamWeapons[0];
} else if (dist > 600) {
// Long range - use nuke or rocket
const longRange = teamWeapons.find(w => w.id === 'nuke' || w.id === 'rocket');
weapon = longRange || teamWeapons[0];
} else {
weapon = teamWeapons[Math.floor(Math.random() * teamWeapons.length)];
}
// Calculate aim angle and power
const dx = target.x - unit.x;
const dy = target.y - unit.y;
let angle, power;
if (weapon.type === 'direct' || weapon.type === 'instant') {
angle = Math.atan2(dy, dx);
power = 70 + Math.random() * 30;
} else {
// Parabolic trajectory
const distAbs = Math.abs(dx);
power = 40 + (distAbs / terrainWidth) * 50 + Math.random() * 15;
if (dx > 0) {
angle = -Math.PI/6 - Math.random() * Math.PI/4;
} else {
angle = -Math.PI*5/6 + Math.random() * Math.PI/4;
}
}
// Add some inaccuracy for realism
angle += (Math.random() - 0.5) * 0.1;
fireWeapon(weapon, angle, power);
setTimeout(() => endTurn(), 2000 + Math.random() * 1000);
}
// Turn timer
function updateTurnTimer() {
if (!turnTimerActive || gameState !== 'playing') return;
turnTimeLeft -= 1/60;
const pct = Math.max(0, turnTimeLeft / 25 * 100);
document.getElementById('turn-timer-fill').style.width = pct + '%';
document.getElementById('turn-timer-text').textContent = Math.ceil(turnTimeLeft);
// Escalating tension - timer scales up when low
const container = document.getElementById('turn-timer-container');
if (turnTimeLeft < 5) {
const scale = 1 + (5 - turnTimeLeft) * 0.1;
container.style.transform = `translate(-50%, -50%) scale(${scale})`;
// Tick sound when low
if (Math.floor(turnTimeLeft * 2) !== Math.floor((turnTimeLeft + 1/60) * 2)) {
sfxTick();
}
} else {
container.style.transform = 'translate(-50%, -50%) scale(1)';
}
if (turnTimeLeft <= 0) {
endTurn();
}
}
// Power meter update
function updatePowerMeter() {
if (powerCharging && isAiming) {
aimPower = Math.min(100, aimPower + 1.5);
document.getElementById('power-fill').style.height = aimPower + '%';
} else if (!mouseDown && !isAiming) {
document.getElementById('power-fill').style.height = '0%';
}
}
// Main game loop
function gameLoop() {
requestAnimationFrame(gameLoop);
if (gameState !== 'playing') return;
updatePhysics(1/60);
updateCamera();
updateTurnTimer();
updatePowerMeter();
render();
}
// Menu functions
function showSetup() {
sfxClick();
document.getElementById('menu-screen').style.display = 'none';
document.getElementById('setup-screen').style.display = 'flex';
}
function backToMenu() {
sfxClick();
gameState = 'menu';
document.getElementById('game-over-screen').style.display = 'none';
document.getElementById('setup-screen').style.display = 'none';
document.getElementById('menu-screen').style.display = 'flex';
document.getElementById('turn-timer-container').style.display = 'none';
}
function startGame() {
sfxClick();
gameState = 'playing';
document.getElementById('setup-screen').style.display = 'none';
initGame();
}
// Start the loop
gameLoop();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"6542bf2b-d084-4311-b68e-dcdbf35ceb85","tokensIn":542,"tokensOut":18288,"tokensTotal":18830,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T02:11:54.072Z"} -->