Worms
Qwen 3.6 35b a3b · full

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 45,559 bytes · SHA-256 f4a5782978bd
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worm Wars</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a1a;overflow:hidden;font-family:'Segoe UI',Tahoma,sans-serif;user-select:none}
canvas{display:block}
#ui-layer{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}
#ui-layer>*{pointer-events:auto}
#hud-top{position:absolute;top:8px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:flex-start;gap:8px}
.team-panel{background:rgba(0,0,0,0.55);border-radius:8px;padding:6px 12px;backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.08);min-width:140px;transition:all 0.3s}
.team-panel.active-team{border-color:rgba(255,255,0,0.4);box-shadow:0 0 12px rgba(255,255,0,0.15)}
.team-panel h3{font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:3px}
.team-panel .hp-bar{width:100%;height:5px;background:rgba(255,255,255,0.12);border-radius:3px;overflow:hidden;margin-top:2px}
.team-panel .hp-fill{height:100%;border-radius:3px;transition:width 0.4s}
#wind-indicator{position:absolute;top:55px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.5);border-radius:20px;padding:5px 14px;color:#fff;font-size:12px;display:flex;align-items:center;gap:6px;backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.08)}
#wind-arrow{font-size:16px;transition:transform 0.3s}
#turn-timer{position:absolute;top:55px;left:50%;transform:translateX(-50%);margin-top:38px}
#timer-bar{width:180px;height:4px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden}
#timer-fill{height:100%;border-radius:2px;transition:width 0.1s linear}
#timer-text{color:#fff;font-size:11px;text-align:center;margin-top:2px;font-weight:600}
#scoreboard{position:absolute;top:8px;right:12px;background:rgba(0,0,0,0.55);border-radius:8px;padding:8px 12px;backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,0.08);min-width:130px}
#scoreboard h4{font-size:10px;text-transform:uppercase;letter-spacing:1px;color:rgba(255,255,255,0.4);margin-bottom:5px}
.score-row{display:flex;justify-content:space-between;align-items:center;padding:2px 0;font-size:12px}
.score-dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:5px}
#weapon-panel{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:5px;background:rgba(0,0,0,0.65);border-radius:14px;padding:7px 10px;backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.08)}
.weapon-btn{width:52px;height:52px;border:2px solid rgba(255,255,255,0.15);border-radius:10px;background:rgba(255,255,255,0.04);color:#fff;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;font-size:18px;transition:all 0.15s;position:relative}
.weapon-btn:hover{border-color:rgba(255,255,255,0.4);background:rgba(255,255,255,0.08);transform:translateY(-2px)}
.weapon-btn.selected{border-color:#ff0;background:rgba(255,255,0,0.12);box-shadow:0 0 12px rgba(255,255,0,0.25);transform:translateY(-2px)}
.weapon-btn .label{font-size:7px;margin-top:2px;text-transform:uppercase;letter-spacing:0.3px;opacity:0.7}
.weapon-btn .key-hint{position:absolute;top:-7px;right:-7px;background:rgba(0,0,0,0.75);color:#aaa;font-size:8px;padding:1px 3px;border-radius:3px}
#power-bar{position:absolute;bottom:82px;left:50%;transform:translateX(-50%);width:180px;height:7px;background:rgba(0,0,0,0.5);border-radius:4px;overflow:hidden;display:none;border:1px solid rgba(255,255,255,0.1)}
#power-fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#4f4,#ff4,#f44);transition:width 0.04s}
#turn-info{position:absolute;bottom:140px;left:50%;transform:translateX(-50%);color:#fff;text-align:center}
#turn-info .unit-name{font-size:22px;font-weight:bold;text-shadow:0 2px 12px rgba(0,0,0,0.6)}
#turn-info .action-text{font-size:12px;color:rgba(255,255,255,0.6);margin-top:3px}
#menu-overlay{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:100;overflow:hidden}
#menu-overlay.hidden{display:none}
#menu-bg{position:absolute;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#0a0a2e,#1a0a3e,#0a1a2e);z-index:-1}
#menu-overlay h1{font-size:52px;color:#fff;text-shadow:0 0 40px rgba(255,100,0,0.4),0 4px 8px rgba(0,0,0,0.5);margin-bottom:6px;letter-spacing:-1px;z-index:1}
#menu-overlay .subtitle{color:rgba(255,255,255,0.4);font-size:13px;margin-bottom:35px;letter-spacing:4px;text-transform:uppercase;z-index:1}
.menu-section{margin-bottom:25px;text-align:center;z-index:1}
.menu-section h3{color:rgba(255,255,255,0.5);font-size:11px;text-transform:uppercase;letter-spacing:2px;margin-bottom:8px}
.menu-row{display:flex;gap:8px;justify-content:center;margin-bottom:8px}
.menu-btn{padding:9px 22px;border:2px solid rgba(255,255,255,0.15);border-radius:8px;background:rgba(255,255,255,0.04);color:#fff;cursor:pointer;font-size:13px;transition:all 0.15s}
.menu-btn:hover{border-color:rgba(255,255,255,0.4);background:rgba(255,255,255,0.08)}
.menu-btn.active{border-color:#ff0;background:rgba(255,255,0,0.1);color:#ff0}
.menu-btn.start-btn{padding:13px 44px;font-size:17px;background:linear-gradient(135deg,#ff6600,#ff3300);border-color:#ff6600;margin-top:8px;font-weight:600;letter-spacing:1px}
.menu-btn.start-btn:hover{background:linear-gradient(135deg,#ff7722,#ff4411);box-shadow:0 0 25px rgba(255,100,0,0.35);transform:translateY(-1px)}
.combat-text{position:absolute;pointer-events:none;font-weight:bold;font-size:17px;animation:floatUp 1.1s ease-out forwards;text-shadow:0 2px 4px rgba(0,0,0,0.8)}
@keyframes floatUp{0%{opacity:1;transform:translateY(0) scale(1)}50%{opacity:1;transform:translateY(-30px) scale(1.1)}100%{opacity:0;transform:translateY(-55px) scale(0.8)}}
#game-over-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);display:none;flex-direction:column;align-items:center;justify-content:center;z-index:200;backdrop-filter:blur(8px)}
#game-over-overlay.show{display:flex}
#game-over-overlay h1{font-size:44px;margin-bottom:8px;text-shadow:0 0 30px rgba(255,100,0,0.4)}
#game-over-overlay .winner{font-size:22px;margin-bottom:28px}
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui-layer">
<div id="hud-top"><div id="team-panels"></div></div>
<div id="wind-indicator"><span id="wind-arrow">→</span><span id="wind-speed">0</span> m/s</div>
<div id="turn-timer"><div id="timer-bar"><div id="timer-fill"></div></div><div id="timer-text">30s</div></div>
<div id="scoreboard"><h4>Scoreboard</h4><div id="score-rows"></div></div>
<div id="turn-info"><div class="unit-name"></div><div class="action-text"></div></div>
<div id="power-bar"><div id="power-fill"></div></div>
<div id="weapon-panel"></div>
</div>
<div id="menu-overlay">
<div id="menu-bg"></div>
<h1>⚔️ WORM WARS</h1>
<div class="subtitle">Artillery Combat</div>
<div class="menu-section"><h3>Teams</h3><div class="menu-row"><button class="menu-btn" data-cat="teams" data-val="2">2</button><button class="menu-btn active" data-cat="teams" data-val="3">3</button><button class="menu-btn" data-cat="teams" data-val="4">4</button></div></div>
<div class="menu-section"><h3>Map</h3><div class="menu-row"><button class="menu-btn" data-cat="map" data-val="islands">Islands</button><button class="menu-btn active" data-cat="map" data-val="cave">Cave</button><button class="menu-btn" data-cat="map" data-val="single">Single</button></div></div>
<div class="menu-section"><h3>Mode</h3><div class="menu-row"><button class="menu-btn active" data-cat="mode" data-val="hotseat">Hot-Seat</button><button class="menu-btn" data-cat="mode" data-val="cpu">vs CPU</button></div></div>
<button class="menu-btn start-btn" id="start-btn">START BATTLE</button>
</div>
<div id="game-over-overlay"><h1 id="go-title">GAME OVER</h1><div class="winner" id="go-winner"></div><button class="menu-btn start-btn" id="go-restart">PLAY AGAIN</button></div>
</div>
<script>
// ============================================================
// AUDIO SYSTEM
// ============================================================
let audioCtx;
function initAudio(){if(!audioCtx)audioCtx=new(window.AudioContext||window.webkitAudioContext)()}
function resumeAudio(){if(audioCtx&&audioCtx.state==='suspended')audioCtx.resume()}
function playTone(freq,dur,type='sine',vol=0.12,ramp=true){
if(!audioCtx)return;resumeAudio();
const o=audioCtx.createOscillator(),g=audioCtx.createGain();
o.type=type;o.frequency.setValueAtTime(freq,audioCtx.currentTime);
g.gain.setValueAtTime(vol,audioCtx.currentTime);
if(ramp)g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
o.connect(g);g.connect(audioCtx.destination);o.start();o.stop(audioCtx.currentTime+dur);
}
function playNoise(dur,vol=0.2,filterFreq=3000){
if(!audioCtx)return;resumeAudio();
const bufSize=audioCtx.sampleRate*dur,buf=audioCtx.createBuffer(1,bufSize,audioCtx.sampleRate);
const data=buf.getChannelData(0);for(let i=0;i<bufSize;i++)data[i]=Math.random()*2-1;
const src=audioCtx.createBufferSource();src.buffer=buf;
const g=audioCtx.createGain();g.gain.setValueAtTime(vol,audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001,audioCtx.currentTime+dur);
const f=audioCtx.createBiquadFilter();f.type='lowpass';f.frequency.value=filterFreq;
src.connect(f);f.connect(g);g.connect(audioCtx.destination);src.start();src.stop(audioCtx.currentTime+dur);
}
function sfxClick(){playTone(900,0.04,'square',0.06)}
function sfxSelect(){playTone(700,0.06,'sine',0.1);setTimeout(()=>playTone(1000,0.05,'sine',0.07),40)}
function sfxCharge(){playTone(250,0.2,'sawtooth',0.04)}
function sfxShoot(){playNoise(0.35,0.25,4500);playTone(130,0.25,'sawtooth',0.12)}
function sfxExplosion(big){playNoise(big?1.0:0.4,big?0.45:0.25,big?700:1800);playTone(big?55:90,big?0.9:0.35,'sine',big?0.25:0.12)}
function sfxJump(){playTone(280,0.08,'triangle',0.08);setTimeout(()=>playTone(450,0.07,'triangle',0.06),30)}
function sfxHit(){playNoise(0.12,0.18,3500);playTone(180,0.12,'square',0.08)}
function sfxMenuOpen(){playTone(450,0.08,'sine',0.06)}
function sfxWindChange(){playNoise(0.25,0.08,1200)}
function sfxDeath(){playTone(350,0.15,'sawtooth',0.12);setTimeout(()=>playTone(180,0.4,'sawtooth',0.08),100);playNoise(0.3,0.15,800)}
function sfxMedkit(){playTone(500,0.1,'sine',0.1);setTimeout(()=>playTone(700,0.1,'sine',0.08),80);setTimeout(()=>playTone(900,0.12,'sine',0.06),160)}
function sfxGameOver(){playTone(400,0.3,'sine',0.15);setTimeout(()=>playTone(300,0.3,'sine',0.12),200);setTimeout(()=>playTone(200,0.5,'sine',0.1),400)}
// ============================================================
// CONSTANTS
// ============================================================
const GRAVITY=0.14,MAX_POWER=22,TURN_TIME=30,UNIT_RADIUS=14,UNIT_HP=100;
const TEAM_COLORS=['#ff4466','#44aaff','#44ff88','#ffaa44'];
const TEAM_NAMES=['Red','Blue','Green','Orange'];
const WEAPONS=[
{name:'Cannon',icon:'💣',desc:'Standard explosive',type:'indirect',aoe:55,damage:28,speed:16},
{name:'Rocket',icon:'🚀',desc:'Guided, high damage',type:'indirect',aoe:45,damage:42,speed:20},
{name:'Sniper',icon:'🔫',desc:'Instant hit, precise',type:'direct',damage:48,speed:50},
{name:'Grenade',icon:'💣',desc:'Small AoE, fast',type:'indirect',aoe:30,damage:18,speed:12},
{name:'Nuke',icon:'☢️',desc:'Massive destruction',type:'aoe',aoe:110,damage:55,speed:10},
{name:'Lightning',icon:'⚡',desc:'Instant beam damage',type:'direct',damage:32,speed:200},
{name:'Vortex',icon:'🌀',desc:'Pulls enemies in',type:'indirect',aoe:70,damage:12,speed:14},
{name:'Medkit',icon:'💊',desc:'Heal 30 HP',type:'utility',heal:30},
];
// ============================================================
// GAME STATE
// ============================================================
let canvas,ctx;
let terrain=[],terrainAbove=[];
let waterLevel,gameWidth=3000,gameHeight=1500;
let teams=[],units=[],projectile=null;
let particles=[],debris=[],lightningBolts=[];
let camera={x:0,y:0,zoom:1,tx:0,ty:0,tz:1};
let wind={x:0,y:-0.002};
let shake={x:0,y:0,intensity:0};
let impactFreeze=0,turnTimer=TURN_TIME;
let activeTeamIndex=0,currentUnitIdx=0;
let gameState='menu',power=0,powerDir=1,selectedWeapon=0;
let mapType='cave',teamCount=3,mode='hotseat';
let seed=Math.random()*10000,keys={},mouse={x:0,y:0};
let lastTime=0,score={};
let menuSel={teams:3,map:'cave',mode:'hotseat'};
let bgCanvas,bgCtx;
let cloudPositions=[];
// ============================================================
// TERRAIN GENERATION
// ============================================================
function noise(x,s){
return Math.sin(x*0.008+s)*0.5+Math.sin(x*0.025+s*2.3)*0.3+Math.sin(x*0.06+s*0.7)*0.15+Math.sin(x*0.14+s*3.1)*0.05;
}
function generateTerrain(){
terrain=[];terrainAbove=[];waterLevel=gameHeight*0.72;
for(let y=0;y<gameHeight;y++)terrain[y]=new Uint8Array(gameWidth);
if(mapType==='islands'){
const cx1=gameWidth*0.3,cx2=gameWidth*0.7,h1=gameHeight*0.5,h2=gameHeight*0.58,a1=200,a2=180;
for(let x=0;x<gameWidth;x++){
let h=gameHeight;
const d1=Math.abs(x-cx1),d2=Math.abs(x-cx2);
h=Math.min(h,Math.floor(h1-Math.exp(-(d1*d1)/(a1*a1))*280+noise(x,seed)*35));
h=Math.min(h,Math.floor(h2-Math.exp(-(d2*d2)/(a2*a2))*240+noise(x,seed+50)*30));
terrainAbove[x]=Math.max(40,Math.min(h,waterLevel-30));
}
}else if(mapType==='cave'){
const cx1=gameWidth*0.35,cx2=gameWidth*0.65,r1=130,r2=110;
for(let x=0;x<gameWidth;x++){
let h=gameHeight;
const d1=Math.abs(x-cx1),d2=Math.abs(x-cx2);
const base=gameHeight*0.62+noise(x,seed)*45;
h=Math.floor(base-Math.exp(-(d1*d1)/(r1*r1))*160-Math.exp(-(d2*d2)/(r2*r2))*130);
terrainAbove[x]=Math.max(60,h);
}
}else{
const cx=gameWidth*0.5,amp=280;
for(let x=0;x<gameWidth;x++){
let h=gameHeight;
const d=Math.abs(x-cx);
h=Math.floor(gameHeight*0.48-Math.exp(-(d*d)/(amp*amp))*320+noise(x,seed)*40);
terrainAbove[x]=Math.max(40,h);
}
}
}
// ============================================================
// CLOUDS
// ============================================================
function initClouds(){
cloudPositions=[];
for(let i=0;i<15;i++){
cloudPositions.push({
x:Math.random()*gameWidth,y:30+Math.random()*150,
w:80+Math.random()*120,h:25+Math.random()*30,
speed:0.1+Math.random()*0.2,opacity:0.08+Math.random()*0.12
});
}
}
function drawClouds(cam){
for(const c of cloudPositions){
c.x+=c.speed;
if(c.x>gameWidth+200)c.x=-200;
const sx=(c.x-cam.x)*cam.zoom+canvas.width/2;
const sy=(c.y-cam.y)*cam.zoom+canvas.height/2;
const sw=c.w*cam.zoom,sh=c.h*cam.zoom;
if(sx+sw<-50||sx>canvas.width+50)continue;
ctx.globalAlpha=c.opacity;
ctx.fillStyle='#fff';
ctx.beginPath();
ctx.ellipse(sx,sy,sw/2,sh/2,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();
ctx.ellipse(sx-sw*0.25,sy+sh*0.1,sw*0.35,sh*0.4,0,0,Math.PI*2);ctx.fill();
ctx.beginPath();
ctx.ellipse(sx+sw*0.25,sy+sh*0.05,sw*0.3,sh*0.35,0,0,Math.PI*2);ctx.fill();
}
ctx.globalAlpha=1;
}
// ============================================================
// PARTICLE SYSTEM
// ============================================================
function spawnExplosion(x,y,radius,color){
const count=Math.floor(radius*2.5);
for(let i=0;i<count;i++){
const angle=Math.random()*Math.PI*2,speed=Math.random()*radius*0.12+1;
particles.push({x,y,vx:Math.cos(angle)*speed,vy:Math.sin(angle)*speed-1.5,
life:1,decay:0.012+Math.random()*0.018,size:Math.random()*4+2,
color:color||`hsl(${Math.random()*40+10},100%,${50+Math.random()*30}%)`,type:'fire'});
}
for(let i=0;i<count/2;i++){
const angle=Math.random()*Math.PI*2,speed=Math.random()*radius*0.06+0.5;
particles.push({x:x+(Math.random()-0.5)*radius*0.4,y:y+(Math.random()-0.5)*radius*0.4,
vx:Math.cos(angle)*speed,vy:-Math.random()*1.8-0.8,life:1,decay:0.004+Math.random()*0.008,
size:Math.random()*8+4,color:'rgba(60,60,60,',type:'smoke'});
}
const dc=Math.floor(radius*1.2);
for(let i=0;i<dc;i++){
const angle=-Math.PI/2+(Math.random()-0.5)*Math.PI*0.7,speed=Math.random()*radius*0.18+2;
const tx=Math.floor(x);
if(tx>=0&&tx<gameWidth&&terrainAbove[tx]!==undefined){
debris.push({x,y:y-4,vx:Math.cos(angle)*speed,vy:Math.sin(angle)*speed-2.5,
size:Math.random()*5+3,rot:Math.random()*Math.PI*2,rs:(Math.random()-0.5)*0.18,
life:1,color:`hsl(${30+Math.random()*15},${20+Math.random()*25}%,${25+Math.random()*20}%)`});
}
}
}
function spawnDust(x,y){
for(let i=0;i<6;i++){
particles.push({x:x+(Math.random()-0.5)*8,y:y+(Math.random()-0.5)*4,
vx:(Math.random()-0.5)*1.5,vy:-Math.random()*1.2-0.4,life:1,decay:0.025+Math.random()*0.015,
size:Math.random()*3+1,color:'rgba(140,120,90,',type:'dust'});
}
}
function spawnSpark(x,y,n){
for(let i=0;i<n;i++){
const a=Math.random()*Math.PI*2,s=Math.random()*3+1;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,decay:0.035+Math.random()*0.02,
size:Math.random()*2+1,color:'#fff',type:'spark'});
}
}
function spawnLightning(x1,y1,x2,y2){
const pts=[];
let px=x1,py=y1;
const steps=12;
for(let i=0;i<=steps;i++){
pts.push({x:px,y:py});
if(i<steps){
px+=(x2-x1)/steps+(Math.random()-0.5)*30;
py+=(y2-y1)/steps;
}
}
lightningBolts.push({pts,life:1,decay:0.06});
for(let i=0;i<15;i++){
const bp=pts[Math.floor(Math.random()*pts.length)];
particles.push({x:bp.x,y:bp.y,vx:(Math.random()-0.5)*3,vy:(Math.random()-0.5)*3,
life:1,decay:0.04+Math.random()*0.02,size:Math.random()*3+1,
color:'#aaddff',type:'spark'});
}
}
function addCombatText(x,y,text,color){
const el=document.createElement('div');el.className='combat-text';
el.textContent=text;el.style.color=color;
const sx=(x-camera.x)*camera.zoom+canvas.width/2;
const sy=(y-camera.y)*camera.zoom+canvas.height/2;
el.style.left=sx+'px';el.style.top=sy+'px';
document.getElementById('ui-layer').appendChild(el);
setTimeout(()=>el.remove(),1100);
}
function updateParticles(){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];p.x+=p.vx;p.y+=p.vy;p.vy+=0.04;p.life-=p.decay;
if(p.life<=0){particles.splice(i,1);continue;}
if(p.type!=='smoke'&&p.type!=='dust'){
const tx=Math.floor(p.x);
if(tx>=0&&tx<gameWidth&&p.y>=terrainAbove[tx]){p.life-=0.03;p.vy*=-0.3;p.vx*=0.7;}
}
}
for(let i=debris.length-1;i>=0;i--){
const d=debris[i];d.x+=d.vx;d.y+=d.vy;d.vy+=GRAVITY;d.rot+=d.rs;d.life-=0.004;
const tx=Math.floor(d.x);
if(tx>=0&&tx<gameWidth&&d.y>=terrainAbove[tx]){
d.vy*=-0.25;d.vx*=0.75;d.y=terrainAbove[tx]-1;d.rs*=0.75;
}
if(d.life<=0||d.y>gameHeight)debris.splice(i,1);
}
for(let i=lightningBolts.length-1;i>=0;i--){
lightningBolts[i].life-=lightningBolts[i].decay;
if(lightningBolts[i].life<=0)lightningBolts.splice(i,1);
}
}
function drawParticles(cam){
for(const p of particles){
const sx=(p.x-cam.x)*cam.zoom+canvas.width/2,sy=(p.y-cam.y)*cam.zoom+canvas.height/2;
const ss=p.size*cam.zoom;
ctx.globalAlpha=p.life;
if(p.type==='smoke'){
ctx.fillStyle=p.color+p.life*0.35+')';
ctx.beginPath();ctx.arc(sx,sy,ss*2.5,0,Math.PI*2);ctx.fill();
}else if(p.type==='dust'){
ctx.fillStyle=p.color+p.life*0.5+')';
ctx.beginPath();ctx.arc(sx,sy,ss,0,Math.PI*2);ctx.fill();
}else{
ctx.fillStyle=p.color;
ctx.beginPath();ctx.arc(sx,sy,ss,0,Math.PI*2);ctx.fill();
}
}
ctx.globalAlpha=1;
for(const d of debris){
const sx=(d.x-cam.x)*cam.zoom+canvas.width/2,sy=(d.y-cam.y)*cam.zoom+canvas.height/2;
ctx.save();ctx.translate(sx,sy);ctx.rotate(d.rot);ctx.globalAlpha=d.life;
ctx.fillStyle=d.color;ctx.fillRect(-d.size/2,-d.size/2,d.size,d.size);
ctx.restore();
}
ctx.globalAlpha=1;
for(const lb of lightningBolts){
ctx.globalAlpha=lb.life;
ctx.strokeStyle='#aaddff';ctx.lineWidth=3*cam.zoom;
ctx.shadowColor='#66aaff';ctx.shadowBlur=15;
ctx.beginPath();
for(let i=0;i<lb.pts.length;i++){
const sx=(lb.pts[i].x-cam.x)*cam.zoom+canvas.width/2;
const sy=(lb.pts[i].y-cam.y)*cam.zoom+canvas.height/2;
i===0?ctx.moveTo(sx,sy):ctx.lineTo(sx,sy);
}
ctx.stroke();
ctx.strokeStyle='#fff';ctx.lineWidth=1.5*cam.zoom;ctx.stroke();
ctx.shadowBlur=0;ctx.globalAlpha=1;
}
}
// ============================================================
// UNITS
// ============================================================
function createUnits(){
units=[];teams=[];score={};
for(let t=0;t<teamCount;t++){
teams.push({index:t,name:TEAM_NAMES[t],color:TEAM_COLORS[t],alive:true,score:0});
score[t]=0;
for(let u=0;u<4;u++){
let x;
if(teamCount===2)x=t===0?gameWidth*0.2+u*40:gameWidth*0.8-u*40;
else x=gameWidth*(0.15+t*0.25)+(u-1.5)*35;
const tx=Math.floor(x);
if(tx<0||tx>=gameWidth)continue;
const ty=terrainAbove[tx]-6;
units.push({team:t,index:u,x,y:ty,vx:0,vy:0,hp:UNIT_HP,maxHp:UNIT_HP,alive:true,
color:TEAM_COLORS[t],name:`${TEAM_NAMES[t]} ${u+1}`,inWater:false,
hasJumped:false,facing:1});
}
}
}
// ============================================================
// CAMERA
// ============================================================
function updateCamera(){
const au=getActiveUnit();
if(au){camera.tx=au.x;camera.ty=au.y-100;}
if(gameState==='flying'&&projectile){camera.tx=projectile.x;camera.ty=projectile.y-60;}
if(gameState==='aiming'||gameState==='charging'){
const dx=mouse.x-canvas.width/2;
camera.tx=au?au.x+dx*0.12:camera.tx;
}
camera.tz=gameState==='flying'?0.72:1.0;
camera.x+=(camera.tx-camera.x)*0.055;
camera.y+=(camera.ty-camera.y)*0.055;
camera.zoom+=(camera.tz-camera.zoom)*0.035;
}
// ============================================================
// SCREEN SHAKE
// ============================================================
function addShake(i){shake.intensity=Math.max(shake.intensity,i)}
function updateShake(){
if(shake.intensity>0.5){shake.x=(Math.random()-0.5)*shake.intensity;shake.y=(Math.random()-0.5)*shake.intensity;shake.intensity*=0.88;}
else{shake.x=0;shake.y=0;shake.intensity=0;}
}
// ============================================================
// WIND
// ============================================================
function changeWind(){
wind.x=(Math.random()-0.5)*0.035;
wind.y=-0.002+Math.random()*0.001;
sfxWindChange();updateWindUI();
}
function updateWindUI(){
const a=document.getElementById('wind-arrow'),s=document.getElementById('wind-speed');
const dir=wind.x>=0?'→':'←';
a.textContent=dir;a.style.transform=`rotate(${wind.x>=0?0:180}deg)`;
s.textContent=Math.abs(Math.round(wind.x*1000));
}
// ============================================================
// TERRAIN DESTRUCTION
// ============================================================
function destroyTerrain(cx,cy,radius){
const r2=radius*radius;
const minX=Math.max(0,Math.floor(cx-radius)),maxX=Math.min(gameWidth-1,Math.ceil(cx+radius));
const minY=Math.max(0,Math.floor(cy-radius)),maxY=Math.min(gameHeight-1,Math.ceil(cy+radius));
for(let y=minY;y<=maxY;y++)for(let x=minX;x<=maxX;x++){
const dx=x-cx,dy=y-cy;
if(dx*dx+dy*dy<r2&&terrain[y][x]===1){
terrain[y][x]=0;if(y<terrainAbove[x])terrainAbove[x]=y;
}
}
}
// ============================================================
// WEAPON FIRE
// ============================================================
function fireWeapon(){
const unit=getActiveUnit();if(!unit)return;
const w=WEAPONS[selectedWeapon];
if(w.type==='utility'){
unit.hp=Math.min(unit.maxHp,unit.hp+w.heal);
addCombatText(unit.x,unit.y-30,`+${w.heal}`,'#4f4');sfxMedkit();spawnDust(unit.x,unit.y);endTurn();return;
}
if(w.type==='direct'){
const angle=getAimAngle();
const dx=Math.cos(angle)*w.speed,dy=Math.sin(angle)*w.speed;
let tx=unit.x,ty=unit.y,hit=false;
for(let s=0;s<60;s++){
tx+=dx*3;ty+=dy*3;
if(tx<-10||tx>gameWidth+10||ty<-10||ty>gameHeight+10)break;
const ttx=Math.floor(tx),tty=Math.floor(ty);
if(ttx>=0&&ttx<gameWidth&&tty>=terrainAbove[ttx]){spawnDust(tx,ty);hit=true;break;}
for(const u of units){
if(!u.alive||u.team===unit.team)continue;
if((tx-u.x)**2+(ty-u.y)**2<400){dealDamage(u,w.damage,tx,ty);hit=true;break;}
}
if(hit)break;
}
if(!hit){destroyTerrain(tx,ty,6);spawnDust(tx,ty);}
sfxShoot();endTurn();return;
}
if(w.type==='aoe'){
// Nuke - launch with big explosion
const angle=getAimAngle();const pwr=power/100;const spd=w.speed*(0.5+pwr*0.5);
projectile={x:unit.x,y:unit.y-10,vx:Math.cos(angle)*spd*unit.facing,vy:Math.sin(angle)*spd,
weapon:selectedWeapon,radius:w.aoe,damage:w.damage,trail:[]};
gameState='flying';power=0;sfxShoot();return;
}
// Indirect
const angle=getAimAngle();const pwr=power/100;const spd=w.speed*(0.5+pwr*0.5);
projectile={x:unit.x,y:unit.y-10,vx:Math.cos(angle)*spd*unit.facing,vy:Math.sin(angle)*spd,
weapon:selectedWeapon,radius:w.aoe||40,damage:w.damage,trail:[]};
gameState='flying';power=0;sfxShoot();
}
function getAimAngle(){
const unit=getActiveUnit();
return Math.atan2(mouse.y-canvas.height/2+100,mouse.x-canvas.width/2);
}
// ============================================================
// PROJECTILE
// ============================================================
function updateProjectile(){
if(!projectile)return;const p=projectile;
p.trail.push({x:p.x,y:p.y,life:1});if(p.trail.length>50)p.trail.shift();
p.vy+=GRAVITY;p.vx+=wind.x;p.vy+=wind.y;p.x+=p.vx;p.y+=p.vy;
for(const t of p.trail)t.life-=0.02;
if(p.x<-50||p.x>gameWidth+50||p.y>gameHeight+50){
if(p.y>gameHeight)onImpact(p.x,gameHeight-10,p);
projectile=null;endTurn();return;
}
if(p.y>waterLevel){spawnDust(p.x,p.y);onImpact(p.x,p.y,p);projectile=null;endTurn();return;}
const tx=Math.floor(p.x);
if(tx>=0&&tx<gameWidth&&terrainAbove[tx]!==undefined&&p.y>=terrainAbove[tx]){
onImpact(p.x,p.y,p);projectile=null;endTurn();return;
}
for(const u of units){
if(!u.alive||u.team===getActiveUnit().team)continue;
if((p.x-u.x)**2+(p.y-u.y)**2<400){dealDamage(u,p.damage,p.x,p.y);spawnExplosion(p.x,p.y,p.radius);sfxExplosion(p.damage>35);addShake(p.damage>35?14:7);
if(p.weapon===4){destroyTerrain(p.x,p.y,p.radius);impactFreeze=10;}
projectile=null;endTurn();return;}
}
}
function onImpact(x,y,p){
destroyTerrain(x,y,p.radius*0.55);spawnExplosion(x,y,p.radius);
sfxExplosion(p.damage>35);addShake(p.damage>35?14:7);
if(p.weapon===4)impactFreeze=10;
for(const u of units){
if(!u.alive||u.team===getActiveUnit().team)continue;
const dx=u.x-x,dy=u.y-y,dist=Math.sqrt(dx*dx+dy*dy);
if(dist<p.radius){const f=1-dist/p.radius;dmg=Math.floor(p.damage*f);dealDamage(u,dmg,x,y);}
}
}
function dealDamage(unit,dmg,hitX,hitY){
unit.hp-=dmg;addCombatText(hitX,hitY-20,`-${dmg}`,'#ff4444');sfxHit();spawnSpark(hitX,hitY,5);
if(unit.hp<=0){unit.hp=0;unit.alive=false;sfxDeath();spawnExplosion(unit.x,unit.y,35,unit.color);addShake(10);checkTeamStatus();}
}
function checkTeamStatus(){
for(const t of teams){const a=units.filter(u=>u.team===t.index&&u.alive).length;if(a===0)t.alive=false;}
const at=teams.filter(t=>t.alive);
if(at.length<=1){gameState='game_over';const w=at[0]||teams[0];
document.getElementById('go-title').textContent='GAME OVER';
document.getElementById('go-winner').textContent=`${w.name} team wins!`;
document.getElementById('go-winner').style.color=w.color;
document.getElementById('game-over-overlay').classList.add('show');sfxGameOver();}
}
// ============================================================
// TURN MANAGEMENT
// ============================================================
function getActiveUnit(){return units.find(u=>u.alive&&u.team===teams[activeTeamIndex].index)}
function getNextUnit(){
const st=activeTeamIndex;let ti=activeTeamIndex,ui=currentUnitIdx;
do{ui++;if(ui>=4){ui=0;ti++;if(ti>=teamCount)ti=0;}
if(ti===st&&ui<=currentUnitIdx)break;
const u=units.find(u=>u.alive&&u.team===teams[ti].index);
if(u)return{unit:u,teamIdx:ti,unitIdx:ui};
}while(ti!==st);return null;
}
function startTurn(){
const next=getNextUnit();if(!next){gameState='game_over';return;}
activeTeamIndex=next.teamIdx;currentUnitIdx=next.unitIdx;
const unit=next.unit;unit.hasJumped=false;unit.vx=0;unit.vy=0;
changeWind();turnTimer=TURN_TIME;gameState='aiming';selectedWeapon=0;updateHUD();
}
function endTurn(){
projectile=null;power=0;document.getElementById('power-bar').style.display='none';updateHUD();
setTimeout(()=>startTurn(),600);
}
// ============================================================
// UNIT PHYSICS
// ============================================================
function updateUnit(unit){
if(!unit.alive)return;
const tx=Math.floor(unit.x);
if(tx>=0&&tx<gameWidth){
if(unit.y>waterLevel&&unit.y>terrainAbove[tx]){unit.hp-=0.15;if(unit.hp<=0){unit.hp=0;unit.alive=false;sfxDeath();checkTeamStatus();return;}}
else{unit.inWater=unit.y>waterLevel-unit.vy;if(unit.inWater){unit.vx*=0.88;unit.vy*=0.88;unit.vy+=GRAVITY*0.25;if(unit.y<waterLevel-8)unit.vy+=0.12;else unit.vy-=0.12;}}
}else{unit.inWater=false;}
if(!unit.inWater)unit.vy+=GRAVITY;
if(!unit.inWater)unit.vx+=wind.x*0.015;
unit.x+=unit.vx;unit.y+=unit.vy;
const ttx=Math.floor(unit.x);
if(ttx>=0&&ttx<gameWidth&&terrainAbove[ttx]!==undefined){
if(unit.y>=terrainAbove[ttx]){
const fd=unit.vy;
if(fd>5){const fdmg=Math.floor((fd-5)*2);if(fdmg>0){unit.hp-=fdmg;addCombatText(unit.x,unit.y-30,`-${fdmg} fall`,'#ff8844');if(unit.hp<=0){unit.hp=0;unit.alive=false;sfxDeath();checkTeamStatus();return;}}}
unit.y=terrainAbove[ttx];unit.vy=0;unit.vx*=0.82;
}
}
if(unit.x<10){unit.x=10;unit.vx=0;}
if(unit.x>gameWidth-10){unit.x=gameWidth-10;unit.vx=0;}
if(unit.y>gameHeight){unit.hp-=1;if(unit.hp<=0){unit.hp=0;unit.alive=false;sfxDeath();checkTeamStatus();return;}unit.y=gameHeight-1;unit.vy=0;}
if(Math.abs(unit.vx)>0.5)unit.facing=unit.vx>0?1:-1;
}
// ============================================================
// RENDERING
// ============================================================
function render(){
const cam=camera;
ctx.save();ctx.translate(canvas.width/2,canvas.height/2);ctx.scale(cam.zoom,cam.zoom);ctx.translate(-cam.x+shake.x,-cam.y+shake.y);
// Sky
const sg=ctx.createLinearGradient(0,-400,0,gameHeight*0.7);
sg.addColorStop(0,'#08081e');sg.addColorStop(0.25,'#0f1038');sg.addColorStop(0.5,'#1a2055');sg.addColorStop(0.75,'#2a3568');sg.addColorStop(1,'#4a6a9e');
ctx.fillStyle=sg;ctx.fillRect(cam.x-canvas.width,cam.y-canvas.height*2,canvas.width*3,canvas.height*3);
// Stars
ctx.fillStyle='rgba(255,255,255,0.35)';
for(let i=0;i<60;i++){
const sx=((i*137.5+seed*0.3)%gameWidth),sy=((i*97.3+seed*0.7)%(gameHeight*0.45));
const ss=0.4+(i%3)*0.4;
ctx.beginPath();ctx.arc(sx,sy,ss,0,Math.PI*2);ctx.fill();
}
// Clouds
drawClouds(cam);
// Water
const wg=ctx.createLinearGradient(0,waterLevel,0,gameHeight);
wg.addColorStop(0,'rgba(15,50,100,0.55)');wg.addColorStop(0.4,'rgba(10,35,75,0.75)');wg.addColorStop(1,'rgba(5,15,40,0.9)');
ctx.fillStyle=wg;ctx.fillRect(0,waterLevel,gameWidth,gameHeight-waterLevel);
// Water surface waves
ctx.strokeStyle='rgba(80,160,240,0.35)';ctx.lineWidth=1.5;ctx.beginPath();
for(let x=0;x<gameWidth;x+=2){const wy=waterLevel+Math.sin(x*0.015+Date.now()*0.001)*3+Math.sin(x*0.03+Date.now()*0.002)*1.5;x===0?ctx.moveTo(x,wy):ctx.lineTo(x,wy);}
ctx.stroke();
// Terrain
drawTerrain(cam);
// Lightning
drawParticles(cam);
// Units
for(const u of units)if(u.alive)drawUnit(u);
// Projectile
if(projectile)drawProjectile(cam);
// Aim line
if((gameState==='aiming'||gameState==='charging')&&!isCPUPlayer())drawAimLine();
ctx.restore();
// Impact freeze
if(impactFreeze>0){ctx.fillStyle=`rgba(255,255,255,${impactFreeze*0.025})`;ctx.fillRect(0,0,canvas.width,canvas.height);impactFreeze--;}
}
function drawTerrain(cam){
const sc=Math.max(0,Math.floor(cam.x-canvas.width/cam.zoom/2)),ec=Math.min(gameWidth,Math.ceil(cam.x+canvas.width/cam.zoom/2));
for(let x=sc;x<ec;x++){
if(terrainAbove[x]===undefined)continue;
const top=terrainAbove[x];
// Fill terrain column
const grad=ctx.createLinearGradient(0,top,0,gameHeight);
grad.addColorStop(0,'#4a8a3a');grad.addColorStop(0.03,'#3d7530');grad.addColorStop(0.15,'#5a4a2a');
grad.addColorStop(0.5,'#4a3a20');grad.addColorStop(1,'#2a1a0a');
ctx.fillStyle=grad;ctx.fillRect(x,top,1,gameHeight-top);
// Grass
if(top<waterLevel){
ctx.fillStyle='#5aaa40';
for(let i=0;i<4;i++){
const gx=x+(i-1.5)*1.5,gy=top-2-i;
ctx.fillRect(gx,gy,1.2,3+i);
}
ctx.fillStyle='#6aba50';
for(let i=0;i<3;i++){
ctx.fillRect(x+i*1.5,top-1,1,2);
}
}
// Dirt texture dots
ctx.fillStyle='rgba(0,0,0,0.08)';
for(let y=top+10;y<Math.min(top+60,gameHeight);y+=15){
ctx.fillRect(x,y,1,1);
}
}
}
function drawUnit(u){
const x=u.x,y=u.y;
// Shadow
ctx.fillStyle='rgba(0,0,0,0.15)';
ctx.beginPath();ctx.ellipse(x,y+2,UNIT_RADIUS*0.9,4,0,0,Math.PI*2);ctx.fill();
// Body
ctx.fillStyle=u.color;
ctx.beginPath();ctx.ellipse(x,y-8,UNIT_RADIUS,UNIT_RADIUS*0.82,0,0,Math.PI*2);ctx.fill();
ctx.strokeStyle='rgba(0,0,0,0.25)';ctx.lineWidth=1.5;ctx.stroke();
// Belly highlight
ctx.fillStyle=shadeColor(u.color,35);
ctx.beginPath();ctx.ellipse(x,y-4,UNIT_RADIUS*0.68,UNIT_RADIUS*0.45,0,0,Math.PI);ctx.fill();
// Eyes
const eyeX=x+u.facing*5;
ctx.fillStyle='#fff';
ctx.beginPath();ctx.arc(eyeX-3,y-10,3.8,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(eyeX+4,y-10,3.8,0,Math.PI*2);ctx.fill();
// Pupils
ctx.fillStyle='#111';
ctx.beginPath();ctx.arc(eyeX-2+u.facing*1.5,y-10,2,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(eyeX+5+u.facing*1.5,y-10,2,0,Math.PI*2);ctx.fill();
// Eye shine
ctx.fillStyle='rgba(255,255,255,0.7)';
ctx.beginPath();ctx.arc(eyeX-1,y-11.5,0.8,0,Math.PI*2);ctx.fill();
ctx.beginPath();ctx.arc(eyeX+6,y-11.5,0.8,0,Math.PI*2);ctx.fill();
// Mouth
ctx.strokeStyle='#333';ctx.lineWidth=1;ctx.beginPath();
ctx.arc(eyeX+1,y-4,3.5,0.1,Math.PI-0.1);ctx.stroke();
// Arms
ctx.strokeStyle=u.color;ctx.lineWidth=3.5;ctx.lineCap='round';
const armAnim=Math.sin(Date.now()*0.004)*2;
ctx.beginPath();ctx.moveTo(x-UNIT_RADIUS+2,y-8);
ctx.lineTo(x-UNIT_RADIUS-6,y-6+armAnim);ctx.stroke();
ctx.beginPath();ctx.moveTo(x+UNIT_RADIUS-2,y-8);
const armEndX=x+UNIT_RADIUS+6+Math.cos(Date.now()*0.003)*3;
const armEndY=y-8+Math.sin(Date.now()*0.003)*2;
ctx.lineTo(armEndX,armEndY);ctx.stroke();
// Weapon in hand
if(gameState==='aiming'||gameState==='charging'){
ctx.font='15px serif';ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(WEAPONS[selectedWeapon].icon,armEndX,armEndY);
}
// HP bar
const bw=28,bh=4,hpPct=u.hp/u.maxHp;
ctx.fillStyle='rgba(0,0,0,0.4)';ctx.fillRect(x-bw/2,y-UNIT_RADIUS-13,bw,bh);
ctx.fillStyle=hpPct>0.5?'#4f4':hpPct>0.25?'#ff4':'#f44';
ctx.fillRect(x-bw/2,y-UNIT_RADIUS-13,bw*hpPct,bh);
// Name
ctx.fillStyle='#fff';ctx.font='bold 9px sans-serif';ctx.textAlign='center';
ctx.fillText(u.name,x,y-UNIT_RADIUS-17);
// Active indicator
if(u.team===teams[activeTeamIndex].index){
ctx.strokeStyle='#ff0';ctx.lineWidth=2;ctx.setLineDash([4,4]);
ctx.beginPath();ctx.arc(x,y-8,UNIT_RADIUS+7,0,Math.PI*2);ctx.stroke();
ctx.setLineDash([]);
// Pulsing glow
const pulse=Math.sin(Date.now()*0.005)*0.3+0.3;
ctx.strokeStyle=`rgba(255,255,0,${pulse})`;ctx.lineWidth=1;
ctx.beginPath();ctx.arc(x,y-8,UNIT_RADIUS+10,0,Math.PI*2);ctx.stroke();
}
// Water splash
if(u.inWater){
ctx.fillStyle='rgba(80,160,240,0.2)';
ctx.beginPath();ctx.arc(x,y-5,UNIT_RADIUS+4,0,Math.PI*2);ctx.fill();
}
}
function drawProjectile(cam){
const p=projectile;
for(const t of p.trail){if(t.life<=0)continue;ctx.globalAlpha=t.life*0.4;ctx.fillStyle='#ff8844';ctx.beginPath();ctx.arc(t.x,t.y,1.5,0,Math.PI*2);ctx.fill();}
ctx.globalAlpha=1;
ctx.font='16px serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(WEAPONS[p.weapon].icon,p.x,p.y);
ctx.shadowColor='#ff4400';ctx.shadowBlur=12;ctx.fillStyle='#ff4400';ctx.beginPath();ctx.arc(p.x,p.y,3.5,0,Math.PI*2);ctx.fill();ctx.shadowBlur=0;
}
function drawAimLine(){
const unit=getActiveUnit();if(!unit)return;
const angle=getAimAngle();const pwr=power/100;const speed=WEAPONS[selectedWeapon].speed*(0.5+pwr*0.5);
let vx=Math.cos(angle)*speed*unit.facing,vy=Math.sin(angle)*speed;
let px=unit.x,py=unit.y-10;
ctx.strokeStyle='rgba(255,255,100,0.35)';ctx.lineWidth=1.5;ctx.setLineDash([5,5]);ctx.beginPath();ctx.moveTo(px,py);
for(let i=0;i<70;i++){vy+=GRAVITY;vx+=wind.x;vy+=wind.y;px+=vx;py+=vy;
const ttx=Math.floor(px);if(px<-10||px>gameWidth+10||py>gameHeight||py>terrainAbove[ttx]||ttx<0||ttx>=gameWidth)break;
ctx.lineTo(px,py);}
ctx.stroke();ctx.setLineDash([]);
// Impact point
ctx.strokeStyle=`rgba(255,${Math.max(0,255-power*2.5)},${Math.max(0,255-power*2.5)},0.5)`;ctx.lineWidth=2;
ctx.beginPath();ctx.arc(px,py,4,0,Math.PI*2);ctx.stroke();
ctx.beginPath();ctx.arc(px,py,8,0,Math.PI*2);ctx.stroke();
}
function shadeColor(c,p){const n=parseInt(c.replace('#',''),16);const r=Math.min(255,(n>>16)+p);const g=Math.min(255,((n>>8)&0xff)+p);const b=Math.min(255,(n&0xff)+p);return`rgb(${r},${g},${b})`}
// ============================================================
// HUD
// ============================================================
function updateHUD(){
const pe=document.getElementById('team-panels');pe.innerHTML='';
for(const t of teams){
const tu=units.filter(u=>u.team===t.index&&u.alive);
const th=tu.reduce((s,u)=>s+u.hp,0),mh=tu.length*UNIT_HP;
const p=document.createElement('div');p.className='team-panel'+(t.index===activeTeamIndex?' active-team':'');
p.innerHTML=`<h3 style="color:${t.color}">${t.name}</h3><div style="font-size:10px;color:rgba(255,255,255,0.5)">${tu.length} alive</div><div class="hp-bar"><div class="hp-fill" style="width:${mh?th/mh*100:0}%;background:${t.color}"></div></div>`;
pe.appendChild(p);
}
const sr=document.getElementById('score-rows');sr.innerHTML='';
for(const t of teams){
const r=document.createElement('div');r.className='score-row';
r.innerHTML=`<span><span class="score-dot" style="background:${t.color}"></span>${t.name}</span><span>${score[t.index]||0}</span>`;sr.appendChild(r);
}
const au=getActiveUnit(),ti=document.getElementById('turn-info');
if(au){
ti.querySelector('.unit-name').textContent=au.name;ti.querySelector('.unit-name').style.color=au.color;
const at2=ti.querySelector('.action-text');
if(gameState==='aiming')at2.textContent='← → Move • W Jump • ENTER Fire';
else if(gameState==='charging')at2.textContent='Hold ENTER to charge power';
else if(gameState==='flying')at2.textContent='Projectile in flight...';
else if(gameState==='ai_turn')at2.textContent=`${TEAM_NAMES[activeTeamIndex]} is thinking...`;
}
const wp=document.getElementById('weapon-panel');wp.innerHTML='';
for(let i=0;i<WEAPONS.length;i++){
const b=document.createElement('button');b.className='weapon-btn'+(i===selectedWeapon?' selected':'');
b.innerHTML=`<span class="key-hint">${i+1}</span><span>${WEAPONS[i].icon}</span><span class="label">${WEAPONS[i].name}</span>`;
b.title=WEAPONS[i].desc;b.onclick=()=>{selectedWeapon=i;sfxSelect();updateHUD()};wp.appendChild(b);
}
const tf=document.getElementById('timer-fill'),tt=document.getElementById('timer-text');
tf.style.width=(turnTimer/TURN_TIME*100)+'%';tt.textContent=Math.ceil(turnTimer)+'s';
if(turnTimer<10){tf.style.background='#f44';tt.style.color='#f44';}
else if(turnTimer<20){tf.style.background='#ff4';tt.style.color='#ff4';}
else{tf.style.background='#4f8';tt.style.color='#4f8';}
if(turnTimer<5){const p=Math.sin(Date.now()*0.012)*0.12+1.08;tt.style.transform=`scale(${p})`;tt.style.opacity=0.7+Math.sin(Date.now()*0.012)*0.3;}
else{tt.style.transform='scale(1)';tt.style.opacity=1;}
}
// ============================================================
// AI
// ============================================================
function isCPUPlayer(){return mode==='cpu'&&teams[activeTeamIndex].index%2===1}
function aiTurn(){
if(gameState!=='ai_turn')return;
const unit=getActiveUnit();if(!unit){startTurn();return;}
const targets=units.filter(u=>u.alive&&u.team!==unit.team);
if(targets.length===0){startTurn();return;}
let best=null,bd=Infinity;
for(const t of targets){const d=Math.abs(t.x-unit.x);if(d<bd){bd=d;best=t;}}
if(!best){startTurn();return;}
// Risk assessment
const tx2=Math.floor(unit.x+(best.x-unit.x)*0.35);
// Choose weapon
if(bd<80)selectedWeapon=2;
else if(bd>350)selectedWeapon=4;
else selectedWeapon=0;
// Calculate power
const dx=best.x-unit.x,dy=best.y-unit.y;
const dist=Math.sqrt(dx*dx+dy*dy);
const angle=Math.atan2(dy,dx);
let powerNeeded=30+dist*0.045;
// Adjust for wind
powerNeeded+=wind.x*dist*0.5;
powerNeeded=Math.max(10,Math.min(95,powerNeeded));
// Adjust for elevation
if(best.y<unit.y)powerNeeded+=5;
// Risk: check if landing point is near water
const landX=unit.x+Math.cos(angle)*dist*1.1;
if(landX>0&&landX<gameWidth&&terrainAbove[Math.floor(landX)]!==undefined&&terrainAbove[Math.floor(landX)]>waterLevel-30){
powerNeeded+=3; // Avoid water
}
gameState='charging';power=0;powerDir=1;
const ct=400+powerNeeded*18;
setTimeout(()=>{if(gameState==='charging'){power=powerNeeded;fireWeapon();gameState='ai_turn';setTimeout(()=>startTurn(),700);}},ct);
}
// ============================================================
// GAME LOOP
// ============================================================
function gameLoop(ts){
const dt=Math.min(32,ts-lastTime);lastTime=ts;
if(gameState==='menu'||gameState==='game_over'){render();requestAnimationFrame(gameLoop);return;}
if(gameState==='aiming'){
const u=getActiveUnit();if(u)updateUnit(u);
turnTimer-=dt/1000;if(turnTimer<=0){turnTimer=0;endTurn();}
if(isCPUPlayer())aiTurn();
}
if(gameState==='charging'){turnTimer-=dt/1000;if(turnTimer<=0){turnTimer=0;endTurn();}}
if(gameState==='flying'){updateProjectile();updateCamera();}
if(gameState==='flying'||gameState==='aiming'){const u=getActiveUnit();if(u)updateUnit(u);}
updateParticles();updateShake();updateCamera();updateHUD();render();
requestAnimationFrame(gameLoop);
}
// ============================================================
// INIT
// ============================================================
function initGame(){
canvas=document.getElementById('game');ctx=canvas.getContext('2d');
resizeCanvas();window.addEventListener('resize',resizeCanvas);
generateTerrain();createUnits();changeWind();initClouds();
activeTeamIndex=0;currentUnitIdx=0;gameState='aiming';startTurn();
lastTime=performance.now();requestAnimationFrame(gameLoop);
}
function resizeCanvas(){canvas=document.getElementById('game');canvas.width=window.innerWidth;canvas.height=window.innerHeight;ctx=canvas.getContext('2d');}
function startNewGame(){
initAudio();sfxMenuOpen();seed=Math.random()*10000;
teamCount=menuSel.teams;mapType=menuSel.map;mode=menuSel.mode;
document.getElementById('menu-overlay').classList.add('hidden');
document.getElementById('game-over-overlay').classList.remove('show');
initGame();
}
// ============================================================
// EVENTS
// ============================================================
document.addEventListener('keydown',e=>{keys[e.code]=true;resumeAudio();
if(e.code==='Escape'&&gameState==='charging'){power=0;gameState='aiming';document.getElementById('power-bar').style.display='none';}
});
document.addEventListener('keyup',e=>{keys[e.code]=false;});
document.addEventListener('mousemove',e=>{mouse.x=e.clientX;mouse.y=e.clientY;});
document.addEventListener('contextmenu',e=>e.preventDefault());
document.addEventListener('click',()=>resumeAudio());
document.querySelectorAll('.menu-btn[data-cat]').forEach(btn=>{
btn.addEventListener('click',()=>{sfxClick();const cat=btn.dataset.cat,val=btn.dataset.val;
btn.parentElement.querySelectorAll('.menu-btn').forEach(b=>b.classList.remove('active'));btn.classList.add('active');
menuSel[cat]=cat==='teams'?parseInt(val):val;});
});
document.getElementById('start-btn').addEventListener('click',startNewGame);
document.getElementById('go-restart').addEventListener('click',()=>{sfxClick();document.getElementById('game-over-overlay').classList.remove('show');document.getElementById('menu-overlay').classList.remove('hidden');});
window.addEventListener('load',()=>{initAudio();});
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"010f6393-3f63-4adf-9288-4e961543f001","tokensIn":881515,"tokensOut":41528,"tokensTotal":923043,"turns":20,"toolCalls":19,"failedToolCalls":0,"timestamp":"2026-07-21T03:00:02.019Z"} -->