Worms
thinkingcap qwen3.6 27b · middle

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 39,378 bytes · SHA-256 dbf7d3a886d8
<!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',system-ui,sans-serif;color:#fff;user-select:none}
canvas{display:block}
#ui-overlay{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:10}
#ui-overlay>*{pointer-events:auto}
.screen{position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;background:rgba(5,5,20,0.97);transition:opacity 0.4s;z-index:100}
.screen.hidden{opacity:0;pointer-events:none}
h1{font-size:3.5em;text-shadow:0 0 30px #ff6b35;margin-bottom:10px;background:linear-gradient(135deg,#ff6b35,#ffd700);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
h2{font-size:1.4em;color:#aaa;margin-bottom:12px;font-weight:normal}
.btn{padding:12px 32px;font-size:1.1em;border:2px solid rgba(255,255,255,0.2);border-radius:10px;background:rgba(255,255,255,0.06);color:#fff;cursor:pointer;margin:5px;transition:all 0.2s;font-family:inherit}
.btn:hover{background:rgba(255,255,255,0.18);border-color:rgba(255,255,255,0.5);transform:scale(1.04)}
.btn.selected{background:rgba(255,107,53,0.35);border-color:#ff6b35}
.team-select{display:flex;gap:8px;margin:8px 0;flex-wrap:wrap;justify-content:center}
#hud-top{position:absolute;top:0;left:0;right:0;display:none;justify-content:center;gap:10px;padding:8px 16px;background:linear-gradient(180deg,rgba(0,0,0,0.75),transparent);pointer-events:none}
.team-hud{display:flex;align-items:center;gap:5px;padding:5px 12px;border-radius:8px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08)}
.team-hud.active{border-color:#ff6b35;box-shadow:0 0 12px rgba(255,107,53,0.25)}
.unit-bar{width:32px;height:6px;border-radius:3px;background:rgba(255,255,255,0.1);overflow:hidden}
.unit-fill{height:100%;border-radius:3px;transition:width 0.3s}
#hud-bottom{position:absolute;bottom:0;left:0;right:0;display:none;align-items:center;justify-content:center;padding:10px;background:linear-gradient(0deg,rgba(0,0,0,0.85),transparent);gap:6px}
.weapon-btn{padding:6px 12px;border-radius:7px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.04);color:#fff;cursor:pointer;font-size:0.85em;display:flex;flex-direction:column;align-items:center;gap:1px;transition:all 0.15s}
.weapon-btn:hover{background:rgba(255,255,255,0.12)}
.weapon-btn.active{border-color:#ff6b35;background:rgba(255,107,53,0.18);box-shadow:0 0 8px rgba(255,107,53,0.25)}
.weapon-icon{font-size:1.3em}
#turn-timer-bar{position:absolute;top:56px;left:50%;transform:translateX(-50%);width:180px;height:5px;border-radius:3px;background:rgba(255,255,255,0.08);overflow:hidden;display:none}
#turn-timer-fill{height:100%;border-radius:3px;transition:background 0.3s}
#wind-indicator{position:absolute;top:70px;left:50%;transform:translateX(-50%);font-size:0.75em;color:#888;display:none;align-items:center;gap:5px}
#action-btns{display:flex;gap:5px;margin-left:10px}
.action-btn{padding:6px 12px;border-radius:7px;border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.04);color:#fff;cursor:pointer;font-size:0.8em;transition:all 0.15s}
.action-btn:hover{background:rgba(255,255,255,0.12)}
#power-meter{position:absolute;bottom:65px;left:50%;transform:translateX(-50%);width:280px;height:14px;border-radius:7px;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.15);overflow:hidden;display:none}
#power-fill{height:100%;border-radius:7px;transition:width 0.03s}
#turn-label{position:absolute;top:84px;left:50%;transform:translateX(-50%);font-size:0.9em;color:#ccc;display:none;text-align:center}
#game-over-screen h1{font-size:2.8em}
.score-row{display:flex;align-items:center;gap:8px;padding:6px 16px;margin:3px;border-radius:7px;background:rgba(255,255,255,0.04);min-width:220px}
.team-config{display:flex;flex-direction:column;align-items:center;gap:8px;margin:10px 0;width:90%;max-width:500px}
.config-row{display:flex;align-items:center;justify-content:center;gap:10px;padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.04);width:100%}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui-overlay">
<div id="menu-screen" class="screen">
<h1>🪱 WORM WARFARE</h1>
<h2>Terrain Shape</h2>
<div class="team-select" id="terrain-shape"></div>
<h2 style="margin-top:15px">Teams (click to toggle CPU)</h2>
<div class="team-config" id="team-config"></div>
<button class="btn" onclick="startGame()" style="margin-top:20px;font-size:1.3em;padding:14px 50px">⚔️ START BATTLE</button>
</div>
<div id="hud-top"></div>
<div id="turn-timer-bar"><div id="turn-timer-fill"></div></div>
<div id="wind-indicator">💨 Wind: <span id="wind-val">0</span></div>
<div id="turn-label"></div>
<div id="hud-bottom">
<div id="weapon-list"></div>
<div id="action-btns">
<button class="action-btn" onclick="doMove()">🏃 Move</button>
<button class="action-btn" onclick="skipTurn()">⏭ Skip</button>
</div>
</div>
<div id="power-meter"><div id="power-fill"></div></div>
<div id="game-over-screen" class="screen hidden">
<h1>BATTLE OVER!</h1>
<div id="score-list"></div>
<button class="btn" onclick="location.reload()" style="margin-top:18px;font-size:1.2em;padding:12px 40px">🔄 PLAY AGAIN</button>
</div>
</div>
<script>
// ==================== AUDIO ENGINE ====================
const AudioCtx=window.AudioContext||window.webkitAudioContext;
let audioCtx;
function initAudio(){if(!audioCtx)try{audioCtx=new AudioCtx()}catch(e){}}
function playSound(type){
if(!audioCtx)return;
try{
const t=audioCtx.currentTime;const g=audioCtx.createGain();g.connect(audioCtx.destination);
switch(type){
case'click':{const o=audioCtx.createOscillator();o.type='sine';o.frequency.setValueAtTime(800,t);o.frequency.exponentialRampToValueAtTime(400,t+0.1);g.gain.setValueAtTime(0.12,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.connect(g);o.start(t);o.stop(t+0.1);break}
case'explosion':{const buf=audioCtx.createBuffer(1,audioCtx.sampleRate*0.5,audioCtx.sampleRate);const d=buf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2);const n=audioCtx.createBufferSource();n.buffer=buf;g.gain.setValueAtTime(0.35,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.5);n.connect(g);n.start(t);break}
case'smallExpl':{const buf=audioCtx.createBuffer(1,audioCtx.sampleRate*0.2,audioCtx.sampleRate);const d=buf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,3);const n=audioCtx.createBufferSource();n.buffer=buf;g.gain.setValueAtTime(0.18,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);n.connect(g);n.start(t);break}
case'hit':{const o=audioCtx.createOscillator();o.type='square';o.frequency.setValueAtTime(300,t);o.frequency.exponentialRampToValueAtTime(80,t+0.15);g.gain.setValueAtTime(0.15,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.15);o.connect(g);o.start(t);o.stop(t+0.15);break}
case'jump':{const o=audioCtx.createOscillator();o.type='sine';o.frequency.setValueAtTime(200,t);o.frequency.exponentialRampToValueAtTime(600,t+0.12);g.gain.setValueAtTime(0.08,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.12);o.connect(g);o.start(t);o.stop(t+0.12);break}
case'laser':{const o=audioCtx.createOscillator();o.type='sawtooth';o.frequency.setValueAtTime(2000,t);o.frequency.exponentialRampToValueAtTime(200,t+0.3);g.gain.setValueAtTime(0.12,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.3);o.connect(g);o.start(t);o.stop(t+0.3);break}
case'water':{const buf=audioCtx.createBuffer(1,audioCtx.sampleRate*0.3,audioCtx.sampleRate);const d=buf.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,1.5)*0.3;const n=audioCtx.createBufferSource();n.buffer=buf;g.gain.setValueAtTime(0.1,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.3);n.connect(g);n.start(t);break}
case'countdown':{const o=audioCtx.createOscillator();o.type='triangle';o.frequency.setValueAtTime(600,t);g.gain.setValueAtTime(0.1,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.08);o.connect(g);o.start(t);o.stop(t+0.08);break}
case'death':{const o=audioCtx.createOscillator();o.type='sawtooth';o.frequency.setValueAtTime(400,t);o.frequency.exponentialRampToValueAtTime(50,t+0.6);g.gain.setValueAtTime(0.2,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.6);o.connect(g);o.start(t);o.stop(t+0.6);break}
case'whoosh':{const o=audioCtx.createOscillator();o.type='sine';o.frequency.setValueAtTime(300,t);o.frequency.exponentialRampToValueAtTime(1200,t+0.08);g.gain.setValueAtTime(0.06,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.08);o.connect(g);o.start(t);o.stop(t+0.08);break}
}
}catch(e){}
}
// ==================== CONSTANTS ====================
const GRAVITY=0.35,WORM_RADIUS=7,MAX_HP=100,TURN_TIME=25;
const TEAM_COLORS=['#ff4444','#44aaff','#44dd44','#ffaa00'];
const TEAM_NAMES=['Red','Blue','Green','Gold'];
// ==================== WEAPONS ====================
const WEAPONS=[
{id:'grenade',name:'Grenade',icon:'💣',cat:'indirect',power:35,aoe:40,speed:8,damage:25},
{id:'rocket',name:'Rocket',icon:'🚀',cat:'indirect',power:50,aoe:60,speed:12,damage:40},
{id:'missile',name:'Missile',icon:'☄️',cat:'indirect',power:60,aoe:80,speed:10,damage:55},
{id:'shotgun',name:'Shotgun',icon:'🔫',cat:'direct',power:20,aoe:30,speed:25,damage:15,count:7,spread:0.4},
{id:'laser',name:'Laser',icon:'⚡',cat:'direct',power:80,aoe:15,speed:999,damage:35},
{id:'cluster',name:'Cluster',icon:'💥',cat:'indirect',power:45,aoe:70,speed:9,damage:20,subBombs:6},
{id:'nuke',name:'Nuke',icon:'☢️',cat:'aoe',power:100,aoe:150,speed:8,damage:80}
];
// ==================== GAME STATE ====================
let canvas,ctx,W,H;
let terrainCanvas,terrainCtx,terrainData;
let gameState='menu';
let teams=[],activeTeam=0,activeUnit=0;
let projectiles=[],particles=[],floatingTexts=[];
let wind=0,camera={x:0,y:0,zoom:1,targetZoom:1};
let turnTimer=TURN_TIME,turnTimerMax=TURN_TIME;
let selectedWeapon=0,powerLevel=50,aimAngle=-45;
let isCharging=false,chargeDir=1;
let screenShake={x:0,y:0,intensity:0};
let impactFreeze=0;
let terrainShape='island',teamCount=2,isCPU=[];
let waterLevel=0,debris=[];
let countdownTimer=0;
// ==================== INIT ====================
function init(){
canvas=document.getElementById('gameCanvas');ctx=canvas.getContext('2d');
resize();window.addEventListener('resize',resize);setupMenu();requestAnimationFrame(gameLoop);
}
function resize(){W=window.innerWidth;H=window.innerHeight;canvas.width=W;canvas.height=H;}
// ==================== MENU ====================
function setupMenu(){
const ts=document.getElementById('terrain-shape');ts.innerHTML='';
['island','twoIslands','cave'].forEach(s=>{
const b=document.createElement('button');b.className='btn'+(s===terrainShape?' selected':'');
b.textContent=s==='island'?'🏝️ Island':s==='twoIslands'?'🌊 Two Islands':'🕳️ Cave';
b.onclick=()=>{terrainShape=s;setupMenu()};ts.appendChild(b);
});
const tc=document.getElementById('team-config');tc.innerHTML='';
for(let i=0;i<4;i++){
const row=document.createElement('div');row.className='config-row';
const dot=document.createElement('span');dot.style.cssText=`display:inline-block;width:18px;height:18px;border-radius:50%;background:${TEAM_COLORS[i]};border:2px solid rgba(255,255,255,0.3)`;
const name=document.createElement('b');name.textContent=TEAM_NAMES[i];
const btn=document.createElement('button');btn.className='btn'+((i<teamCount&&isCPU[i])?' selected':'');
btn.textContent=i<teamCount?(isCPU[i]?'🤖 CPU':'👤 Human'):'—';
btn.style.fontSize='0.85em';btn.style.padding='6px 14px';
if(i>=teamCount){btn.disabled=true;btn.style.opacity='0.3'}
else{btn.onclick=()=>{isCPU[i]=!isCPU[i];setupMenu()}}
row.appendChild(dot);row.appendChild(name);row.appendChild(btn);tc.appendChild(row);
}
}
// ==================== TERRAIN GENERATION ====================
function generateTerrain(){
const tw=Math.floor(W*2.5),th=Math.floor(H*1.8);
terrainCanvas=document.createElement('canvas');terrainCanvas.width=tw;terrainCanvas.height=th;
terrainCtx=terrainCanvas.getContext('2d');
// Sky gradient
const skyGrad=terrainCtx.createLinearGradient(0,0,0,th);
skyGrad.addColorStop(0,'#0a0520');skyGrad.addColorStop(0.3,'#1a0a3e');
skyGrad.addColorStop(0.6,'#4a1942');skyGrad.addColorStop(0.85,'#ff6b35');skyGrad.addColorStop(1,'#ffd700');
terrainCtx.fillStyle=skyGrad;terrainCtx.fillRect(0,0,tw,th);
// Stars
for(let i=0;i<250;i++){const sx=Math.random()*tw,sy=Math.random()*th*0.45,r=Math.random()*1.5+0.3;
terrainCtx.fillStyle=`rgba(255,255,255,${Math.random()*0.7+0.3})`;terrainCtx.beginPath();terrainCtx.arc(sx,sy,r,0,Math.PI*2);terrainCtx.fill()}
// Moon
const moonX=tw*0.8,moonY=th*0.1;
const moonGrad=terrainCtx.createRadialGradient(moonX,moonY,0,moonX,moonY,40);
moonGrad.addColorStop(0,'rgba(255,255,230,0.9)');moonGrad.addColorStop(0.5,'rgba(255,255,200,0.3)');moonGrad.addColorStop(1,'rgba(255,255,200,0)');
terrainCtx.fillStyle=moonGrad;terrainCtx.beginPath();terrainCtx.arc(moonX,moonY,40,0,Math.PI*2);terrainCtx.fill();
waterLevel=th*0.78;
if(terrainShape==='island')generateIsland(tw,th,false);
else if(terrainShape==='twoIslands')generateIsland(tw,th,true);
else generateCave(tw,th);
terrainData=terrainCtx.getImageData(0,0,tw,th);
}
function noise(x,s){return Math.sin(x*0.01+s)*0.5+Math.sin(x*0.023+s*2)*0.3+Math.sin(x*0.047+s*3)*0.2}
function generateIsland(tw,th,double){
const seed=Math.random()*100;baseH=th*0.5;
for(let x=0;x<tw;x++){
let h=noise(x,seed)*90+noise(x*2,seed+50)*35+noise(x*4,seed+100)*12;
if(double){const mid=tw/2,dist=Math.abs(x-mid);h-=Math.max(0,(mid-dist)/mid)*70}
const topY=baseH-h;
// Multi-layer terrain with gradient
for(let y=topY;y<th;y++){
const depth=y-topY;let r,g,b;
if(depth<8){r=90+Math.random()*20;g=140+Math.random()*30;b=50} // grass
else if(depth<30){r=139+Math.random()*15;g=105+Math.random()*10;b=20} // dirt
else{r=80+Math.random()*20;g=60+Math.random()*15;b=30+Math.random()*10} // rock
terrainCtx.fillStyle=`rgb(${r|0},${g|0},${b|0})`;terrainCtx.fillRect(x,y,1,1);
}
}
drawWater(tw,th);
}
function generateCave(tw,th){
const seed=Math.random()*100;
// Fill with rock gradient
for(let y=0;y<th;y++){for(let x=0;x<tw;x++){
const d=y/th;r=50+d*30+Math.random()*10|0;g=40+d*25+Math.random()*8|0;b=30+d*20+Math.random()*6|0;
terrainCtx.fillStyle=`rgb(${r},${g},${b})`;terrainCtx.fillRect(x,y,1,1);
}}
// Carve cave
for(let y=th*0.15;y<th*0.85;y++){
const leftEdge=noise(y,seed)*tw*0.12+tw*0.25;
const rightEdge=noise(y,seed+50)*tw*0.12+tw*0.75;
for(let x=leftEdge;x<rightEdge;x++){terrainCtx.clearRect(x,y,1,1)}
}
drawWater(tw,th);
}
function drawWater(tw,th){
const wg=terrainCtx.createLinearGradient(0,waterLevel-30,0,th);
wg.addColorStop(0,'rgba(25,70,140,0.6)');wg.addColorStop(0.5,'rgba(15,50,110,0.85)');wg.addColorStop(1,'rgba(8,30,80,0.95)');
terrainCtx.fillStyle=wg;terrainCtx.fillRect(0,waterLevel,tw,th-waterLevel);
}
function isTerrain(px,py){
if(px<0||px>=terrainCanvas.width||py<0||py>=terrainCanvas.height)return true;
const d=terrainData.data,idx=(Math.floor(py)*terrainCanvas.width+Math.floor(px))*4;
return d[idx+3]>128;
}
function destroyTerrain(cx,cy,aoe){
const r=Math.ceil(aoe);const x0=Math.max(0,cx-r),x1=Math.min(terrainCanvas.width-1,cx+r);
const y0=Math.max(0,cy-r),y1=Math.min(terrainCanvas.height-1,cy+r);
for(let y=y0;y<=y1;y++){for(let x=x0;x<=x1;x++){
if((x-cx)*(x-cx)+(y-cy)*(y-cy)<r*r)terrainCtx.clearRect(x,y,1,1)}}
terrainData=terrainCtx.getImageData(0,0,terrainCanvas.width,terrainCanvas.height);
}
// ==================== TEAMS & UNITS ====================
function createTeams(){
teams=[];
for(let i=0;i<teamCount;i++){
const team={color:TEAM_COLORS[i],name:TEAM_NAMES[i],units:[],alive:true,isCPU:isCPU[i]||false};
const spacing=terrainCanvas.width/(teamCount+1);const sx=spacing*(i+1);
let sy=0;for(let y=0;y<terrainCanvas.height;y++){if(isTerrain(sx,y)){sy=y-2;break}}
for(let j=0;j<4;j++){
const ux=sx+(j-1.5)*35;let uy=sy;
for(let y=sy;y>0;y--){if(!isTerrain(ux,y))uy=y}
team.units.push({x:ux,y:uy,hp:MAX_HP,alive:true,vy:0,vx:0,onGround:false,fallStartY:uy,inWater:false,drowning:0});
}
teams.push(team);
}
}
function getTeamOfUnit(unit){for(let i=0;i<teams.length;i++)if(teams[i].units.includes(unit))return i;return -1;}
// ==================== PHYSICS ====================
function updateUnit(unit){
unit.vy+=GRAVITY;unit.x+=unit.vx;unit.y+=unit.vy;unit.vx*=0.92;
const feetY=Math.floor(unit.y+WORM_RADIUS);const lx=Math.floor(unit.x-WORM_RADIUS+1),rx=Math.floor(unit.x+WORM_RADIUS-1);
if(isTerrain(lx,feetY)||isTerrain(rx,feetY)){
let landY=unit.y;for(let y=Math.floor(unit.y-WORM_RADIUS);y<Math.floor(unit.y+WORM_RADIUS+5);y++){if(!isTerrain(Math.floor(unit.x),y))landY=y;else break}
const fallDist=unit.y-unit.fallStartY;
if(fallDist>100&&unit.onGround){const dmg=Math.min(40,Math.floor((fallDist-100)/5));if(dmg>0){unit.hp-=dmg;addFloatingText(unit.x,unit.y-20,'-'+dmg,'#ff6644');playSound('hit')}}
unit.y=landY-WORM_RADIUS;unit.vy=0;if(!unit.onGround){unit.fallStartY=unit.y;spawnParticles(unit.x,unit.y+WORM_RADIUS,6,'#aa8855',2)}
unit.onGround=true;
}else{unit.onGround=false}
if(unit.y+WORM_RADIUS>waterLevel){
unit.inWater=true;unit.vy*=0.7;unit.vx*=0.85;
if(gameState==='weaponSelect'||gameState==='turnSelect'){unit.drowning+=0.001;if(unit.drowning>2){unit.hp-=0.3}}else{unit.drowning=0}
}else{unit.inWater=false;unit.drowning=0}
if(unit.x<20){unit.x=20;unit.vx*=-0.5}if(unit.x>terrainCanvas.width-20){unit.x=terrainCanvas.width-20;unit.vx*=-0.5}
if(unit.y>terrainCanvas.height+100)killUnit(unit);
if(unit.hp<=0&&unit.alive)killUnit(unit);
}
function killUnit(unit){
unit.alive=false;playSound('death');spawnParticles(unit.x,unit.y,35,'#ff6b35',6);addFloatingText(unit.x,unit.y-20,'💀','#fff');
const ti=getTeamOfUnit(unit);if(ti>=0)teams[ti].alive=teams[ti].units.some(u=>u.alive);
}
// ==================== PROJECTILES ====================
function fireProjectile(teamIdx){
const unit=teams[teamIdx].units[activeUnit];const w=WEAPONS[selectedWeapon];
const power=w.power*(powerLevel||50)/50;const rad=(aimAngle||(-45))*Math.PI/180;
if(w.cat==='direct'){
if(w.id==='laser')fireLaser(unit,rad);
else if(w.id==='shotgun')fireShotgun(unit,rad,w.count||5,w.spread||0.3,power);
}else{
const px=unit.x+Math.cos(rad)*15;const py=unit.y-12+Math.sin(rad)*15;
projectiles.push({x:px,y:py,vx:Math.cos(rad)*power*0.3,vy:Math.sin(rad)*power*0.3,aoe:w.aoe,damage:w.damage,weaponId:w.id,subBombs:w.subBombs||0,trail:[],ownerTeam:teamIdx});
}
playSound(w.cat==='direct'?'laser':'whoosh');gameState='firing';
}
function fireLaser(unit,angle){
const w=WEAPONS[selectedWeapon];let dx=Math.cos(angle),dy=-Math.abs(Math.sin(angle));
let lx=unit.x,ly=unit.y-15;const steps=800,stepLen=3;
for(let i=0;i<steps;i++){lx+=dx*stepLen;ly+=dy*stepLen;
if(isTerrain(Math.floor(lx),Math.floor(ly))){destroyTerrain(lx,ly,w.aoe);damageUnitsInArea(lx,ly,w.aoe,w.damage,getTeamOfUnit(unit));spawnParticles(lx,ly,20,'#ff4444',4);screenShake.intensity=8;impactFreeze=5;playSound('explosion');break}}
for(let i=0;i<steps;i+=3)particles.push({x:lx-dx*stepLen*i/3,y:ly-dy*stepLen*i/3,vx:0,vy:0,life:12,maxLife:12,size:2,color:'#ff4444'});
}
function fireShotgun(unit,angle,count,spread,power){
const w=WEAPONS[selectedWeapon];const ti=getTeamOfUnit(unit);
for(let i=0;i<count;i++){const a=angle+(Math.random()-0.5)*spread;
projectiles.push({x:unit.x,y:unit.y-15,vx:Math.cos(a)*power*0.25,vy:Math.sin(a)*power*0.25,aoe:w.aoe,damage:w.damage/count,weaponId:'shotgun',trail:[],subBombs:0,ownerTeam:ti});
}
}
function updateProjectiles(){
for(let i=projectiles.length-1;i>=0;i--){const p=projectiles[i];
p.vx+=wind*0.002;p.vy+=GRAVITY*0.5;p.x+=p.vx;p.y+=p.vy;
p.trail.push({x:p.x,y:p.y,life:10});if(p.trail.length>30)p.trail.shift();p.trail.forEach(t=>t.life--);p.trail=p.trail.filter(t=>t.life>0);
if(isTerrain(Math.floor(p.x),Math.floor(p.y))){explodeProjectile(p,i);continue}
if(p.x<-100||p.x>terrainCanvas.width+100||p.y>terrainCanvas.height+100){projectiles.splice(i,1);continue}
}
}
function explodeProjectile(p,idx){
destroyTerrain(p.x,p.y,p.aoe);damageUnitsInArea(p.x,p.y,p.aoe,p.damage,p.ownerTeam!==undefined?p.ownerTeam:activeTeam);
spawnParticles(p.x,p.y,25+Math.floor(p.aoe/3),'#ff8844',6);screenShake.intensity=Math.min(15,p.aoe*0.2);impactFreeze=Math.min(10,p.aoe*0.1);
playSound(p.aoe>60?'explosion':'smallExpl');spawnDebris(p.x,p.y,p.aoe);
if(p.subBombs){for(let i=0;i<p.subBombs;i++){const a=Math.random()*Math.PI*2,sp=Math.random()*4+2;
projectiles.push({x:p.x,y:p.y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp-3,aoe:p.aoe*0.4,damage:p.damage*0.5,weaponId:'cluster',trail:[],subBombs:0,ownerTeam:p.ownerTeam})}}
projectiles.splice(idx,1);
}
function damageUnitsInArea(cx,cy,aoe,damage,excludeTeam){
for(let i=0;i<teams.length;i++){if(i===excludeTeam)continue;
teams[i].units.forEach(u=>{if(!u.alive)return;const dx=u.x-cx,dy=u.y-cy,dist=Math.sqrt(dx*dx+dy*dy);
if(dist<aoe){const f=1-dist/aoe;u.hp-=Math.floor(damage*f);addFloatingText(u.x,u.y-20,'-'+Math.floor(damage*f),'#ff4444');if(dist>0){u.vx+=dx/dist*5;u.vy+=dy/dist*-3}}});
}
}
// ==================== DEBRIS & PARTICLES ====================
function spawnDebris(cx,cy,aoe){const n=Math.floor(aoe/8);for(let i=0;i<n;i++){
debris.push({x:cx+(Math.random()-0.5)*aoe,y:cy+(Math.random()-0.5)*aoe*0.5,vx:(Math.random()-0.5)*3,vy:-Math.random()*4-1,size:Math.random()*4+2,life:60+Math.random()*60,color:`hsl(${30+Math.random()*20},${40+Math.random()*30}%,${20+Math.random()*30}%)`});
}}
function updateDebris(){for(let i=debris.length-1;i>=0;i--){const d=debris[i];d.vy+=GRAVITY*0.5;d.x+=d.vx;d.y+=d.vy;
if(isTerrain(Math.floor(d.x),Math.floor(d.y))){d.vy*=-0.3;d.vx*=0.5}d.life--;if(d.life<=0)debris.splice(i,1)}}
function spawnParticles(x,y,count,color,speed){for(let i=0;i<count;i++){const a=Math.random()*Math.PI*2,s=Math.random()*speed+1;
particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-1,life:30+Math.random()*30,maxLife:60,size:Math.random()*4+1,color})}}
function updateParticles(){for(let i=particles.length-1;i>=0;i--){const p=particles[i];p.vy+=GRAVITY*0.3;p.x+=p.vx;p.y+=p.vy;p.life--;if(p.life<=0)particles.splice(i,1)}}
function addFloatingText(x,y,text,color){floatingTexts.push({x,y,text,color,life:60,vy:-2})}
function updateFloatingTexts(){for(let i=floatingTexts.length-1;i>=0;i--){const f=floatingTexts[i];f.y+=f.vy;f.life--;if(f.life<=0)floatingTexts.splice(i,1)}}
// ==================== CAMERA ====================
function updateCamera(){
const unit=teams[activeTeam]?.units[activeUnit];
if(unit&&unit.alive){camera.x+=(unit.x-camera.x)*0.05;camera.y+=(unit.y-H*0.6-camera.y)*0.05}
camera.x=Math.max(0,Math.min(camera.x,terrainCanvas.width-W/camera.zoom));
camera.y=Math.max(-100,Math.min(camera.y,terrainCanvas.height-H/camera.zoom));
if(projectiles.length>0)camera.targetZoom=0.82;else camera.targetZoom=1;
camera.zoom+=(camera.targetZoom-camera.zoom)*0.03;
if(screenShake.intensity>0){screenShake.x=(Math.random()-0.5)*screenShake.intensity;screenShake.y=(Math.random()-0.5)*screenShake.intensity;screenShake.intensity*=0.85;if(screenShake.intensity<0.3)screenShake.intensity=0}
else{screenShake.x=0;screenShake.y=0}
}
// ==================== RENDERING ====================
function render(){
ctx.clearRect(0,0,W,H);ctx.save();ctx.translate(screenShake.x,screenShake.y);ctx.scale(camera.zoom,camera.zoom);ctx.translate(-camera.x,-camera.y);
ctx.drawImage(terrainCanvas,0,0);
// Water surface animation
const t=Date.now()*0.002;ctx.fillStyle='rgba(30,80,160,0.35)';
for(let x=camera.x-10;x<camera.x+W/camera.zoom+10;x+=4){const wy=waterLevel+Math.sin(x*0.05+t)*3;if(!isTerrain(Math.floor(x),Math.floor(wy)))ctx.fillRect(x,wy,4,2)}
// Debris
debris.forEach(d=>{ctx.globalAlpha=Math.min(1,d.life/20);ctx.fillStyle=d.color;ctx.fillRect(d.x-d.size/2,d.y-d.size/2,d.size,d.size)});ctx.globalAlpha=1;
// Projectiles & trails
projectiles.forEach(p=>{p.trail.forEach(t=>{ctx.fillStyle=`rgba(255,150,50,${t.life/10})`;ctx.beginPath();ctx.arc(t.x,t.y,2,0,Math.PI*2);ctx.fill()});
const glow=ctx.createRadialGradient(p.x,p.y,0,p.x,p.y,8);glow.addColorStop(0,'#fff');glow.addColorStop(0.5,'#ffaa44');glow.addColorStop(1,'rgba(255,100,0,0)');
ctx.fillStyle=glow;ctx.beginPath();ctx.arc(p.x,p.y,8,0,Math.PI*2);ctx.fill()});
// Particles
particles.forEach(p=>{const a=p.life/p.maxLife;ctx.globalAlpha=a;ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(p.x,p.y,p.size*a,0,Math.PI*2);ctx.fill()});ctx.globalAlpha=1;
// Units
teams.forEach((team,ti)=>team.units.forEach(u=>{if(!u.alive)return;drawWorm(u,team.color,ti===activeTeam&&teams[ti].units.indexOf(u)===activeUnit)}));
// Floating texts
floatingTexts.forEach(f=>{ctx.globalAlpha=Math.min(1,f.life/20);ctx.fillStyle=f.color;ctx.font='bold 14px sans-serif';ctx.textAlign='center';ctx.fillText(f.text,f.x,f.y)});ctx.globalAlpha=1;
// Aiming line
if(gameState==='aiming'){const unit=teams[activeTeam].units[activeUnit];if(unit)drawAimLine(unit)}
ctx.restore();
}
function drawWorm(u,color,isActive){
const t=Date.now()*0.003;
// Shadow
ctx.fillStyle='rgba(0,0,0,0.25)';ctx.beginPath();ctx.ellipse(u.x,u.y+WORM_RADIUS+2,WORM_RADIUS*1.4,2.5,0,0,Math.PI*2);ctx.fill();
// Body
ctx.strokeStyle=color;ctx.lineWidth=WORM_RADIUS*1.6;ctx.lineCap='round';ctx.beginPath();
for(let i=-4;i<=4;i++){const wx=u.x+i*1.5,wy=u.y+Math.sin(i*0.8+t)*2;if(i===-4)ctx.moveTo(wx,wy);else ctx.lineTo(wx,wy)}ctx.stroke();
ctx.strokeStyle='rgba(0,0,0,0.4)';ctx.lineWidth=1.5;ctx.beginPath();
for(let i=-4;i<=4;i++){const wx=u.x+i*1.5,wy=u.y+Math.sin(i*0.8+t)*2;if(i===-4)ctx.moveTo(wx,wy);else ctx.lineTo(wx,wy)}ctx.stroke();
// Eyes
const eo=Math.sin(t)*0.3;ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(u.x-3,u.y-4+eo,3,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.arc(u.x+3,u.y-4+eo,3,0,Math.PI*2);ctx.fill();
ctx.fillStyle='#111';ctx.beginPath();ctx.arc(u.x-2.5,u.y-3.5+eo,1.5,0,Math.PI*2);ctx.fill();ctx.beginPath();ctx.arc(u.x+3.5,u.y-3.5+eo,1.5,0,Math.PI*2);ctx.fill();
// HP bar
const hpPct=u.hp/MAX_HP;ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(u.x-14,u.y-WORM_RADIUS-10,28,5);
ctx.fillStyle=hpPct>0.5?'#44dd44':hpPct>0.25?'#ffaa00':'#ff4444';ctx.fillRect(u.x-14,u.y-WORM_RADIUS-10,Math.max(0,28*hpPct),5);
// Active indicator
if(isActive){const pulse=Math.sin(Date.now()*0.005)*0.3+0.7;ctx.strokeStyle=`rgba(255,255,255,${pulse*0.4})`;ctx.lineWidth=3;ctx.beginPath();ctx.arc(u.x,u.y,WORM_RADIUS+8,0,Math.PI*2);ctx.stroke()}
if(u.inWater){ctx.fillStyle='rgba(100,150,255,0.25)';ctx.beginPath();ctx.arc(u.x,u.y,WORM_RADIUS+6,0,Math.PI*2);ctx.fill()}
}
function drawAimLine(unit){
const w=WEAPONS[selectedWeapon];const power=w.power*(powerLevel||50)/50;const rad=(aimAngle||(-45))*Math.PI/180;
let px=unit.x+Math.cos(rad)*15,py=unit.y-12+Math.sin(rad)*15,vx=Math.cos(rad)*power*0.3,vy=Math.sin(rad)*power*0.3;
ctx.setLineDash([4,6]);ctx.strokeStyle='rgba(255,255,255,0.35)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(px,py);
for(let i=0;i<80;i++){vx+=wind*0.002;vy+=GRAVITY*0.5;px+=vx;py+=vy;if(isTerrain(Math.floor(px),Math.floor(py)))break;ctx.lineTo(px,py)}
ctx.stroke();ctx.setLineDash([]);
ctx.strokeStyle='rgba(255,100,50,0.25)';ctx.lineWidth=2;ctx.beginPath();ctx.arc(px,py,w.aoe,0,Math.PI*2);ctx.stroke();
}
// ==================== HUD ====================
function updateHUD(){
const hudTop=document.getElementById('hud-top');hudTop.innerHTML='';
teams.forEach((team,i)=>{if(!team.alive)return;
const div=document.createElement('div');div.className='team-hud'+(i===activeTeam?' active':'');
let html=`<span style="color:${team.color}">●</span><b>${team.name}</b>`+
(team.isCPU?' <span style="font-size:0.7em;color:#888">🤖</span>':'') ;
team.units.forEach(u=>{if(!u.alive)return;html+=`<div class="unit-bar"><div class="unit-fill" style="width:${Math.max(0,u.hp/MAX_HP*100)}%;background:${team.color}"></div></div>`});
div.innerHTML=html;hudTop.appendChild(div);
});
const wl=document.getElementById('weapon-list');wl.innerHTML='';
WEAPONS.forEach((w,i)=>{const btn=document.createElement('button');btn.className='weapon-btn'+(i===selectedWeapon?' active':'');
btn.innerHTML=`<span class="weapon-icon">${w.icon}</span><span>${w.name}</span>`;
btn.onclick=()=>{initAudio();playSound('click');selectedWeapon=i;updateHUD()};wl.appendChild(btn);
});
const tf=document.getElementById('turn-timer-fill');const pct=Math.max(0,turnTimer/turnTimerMax*100);tf.style.width=pct+'%';
if(pct>50)tf.style.background='#44dd44';else if(pct>25)tf.style.background='#ffaa00';else tf.style.background='#ff4444';
document.getElementById('wind-val').textContent=wind.toFixed(1);
const pm=document.getElementById('power-meter'),pf=document.getElementById('power-fill');
if(gameState==='aiming'){pm.style.display='block';pf.style.width=Math.max(0,Math.min(100,powerLevel))+'%';pf.style.background=`hsl(${120-powerLevel*1.2},80%,50%)`}else{pm.style.display='none'}
// Turn label
const tl=document.getElementById('turn-label');
if(gameState==='weaponSelect'||gameState==='aiming'){
const team=teams[activeTeam];tl.textContent=`${team.name}'s turn — Unit ${activeUnit+1}`;tl.style.color=team.color;tl.style.display='block';
}else{tl.style.display='none'}
}
// ==================== AI SYSTEM ====================
let aiTimeout=null;
function runAI(){
if(gameState!=='weaponSelect'||!teams[activeTeam]?.isCPU)return;
const team=teams[activeTeam];const unit=team.units[activeUnit];if(!unit||!unit.alive){startTurn();return}
// AI decision making with risk assessment
aiTimeout=setTimeout(()=>{aiDecide(team,unit)},800+Math.random()*600);
}
function aiDecide(team,unit){
if(gameState!=='weaponSelect')return;
// Risk assessment: check environmental hazards
const inDanger=unit.inWater||(!unit.onGround&&unit.y>waterLevel-50);
const nearCliff=!isTerrain(Math.floor(unit.x+20),Math.floor(unit.y+WORM_RADIUS))&&!isTerrain(Math.floor(unit.x-20),Math.floor(unit.y+WORM_RADIUS));
// If in danger, try to move to safety first
if(inDanger&&unit.onGround){aiMoveToSafety(unit);return}
// Find targets (enemy units)
const enemies=[];
teams.forEach((t,i)=>{if(i===activeTeam)return;t.units.forEach(u=>{if(!u.alive)return;enemies.push({team:i,unit:u,dist:Math.sqrt((u.x-unit.x)**2+(u.y-unit.y)**2)})})});
if(enemies.length===0){skipTurn();return}
// Sort enemies by distance (prioritize closest)
enemies.sort((a,b)=>a.dist-b.dist);
// Choose weapon based on situation
let bestWeapon=0;
const avgDist=enemies.reduce((s,e)=>s+e.dist,0)/enemies.length;
if(avgDist<150){bestWeapon=3} // shotgun for close range
else if(avgDist>400){bestWeapon=Math.random()>0.5?2:1} // missile or rocket for far
else{bestWeapon=Math.floor(Math.random()*6)} // random otherwise
selectedWeapon=bestWeapon;updateHUD();
// Calculate aim angle toward target
const target=enemies[0].unit;
const dx=target.x-unit.x,dy=target.y-(unit.y-12);
const dist=Math.sqrt(dx*dx+dy*dy);
// Simple trajectory calculation for indirect weapons
const w=WEAPONS[selectedWeapon];
if(w.cat==='indirect'||w.cat==='aoe'){
// Calculate angle to hit target with parabolic arc
const power=w.power*0.6; // AI uses moderate power
// Simplified: aim at a point above the target
const aimX=target.x+(Math.random()-0.5)*40;
const aimY=target.y-30-Math.random()*40;
aimAngle=Math.atan2(aimY-(unit.y-12),aimX-unit.x)*180/Math.PI;
powerLevel=40+Math.random()*40;
// Risk assessment: avoid shooting near water or own units
const shotLanding={x:aimX,y:aimY};
if(shotLanding.y>waterLevel-20){powerLevel=Math.max(30,powerLevel-15)} // reduce power to avoid water splash
setTimeout(()=>{fireProjectile(activeTeam)},400);
}else{
// Direct weapons - aim directly at target
aimAngle=Math.atan2(dy,dx)*180/Math.PI;
if(aimAngle>0)aimAngle=-aimAngle*0.5; // Adjust for upward aiming
powerLevel=60+Math.random()*30;
setTimeout(()=>{fireProjectile(activeTeam)},400);
}
}
function aiMoveToSafety(unit){
// Find safe ground nearby (not near water)
const searchRange=150;const dir=Math.random()>0.5?1:-1;
for(let d=30;d<searchRange;d+=20){
const tx=unit.x+dir*d;
let landY=-1;for(let y=Math.floor(unit.y);y<terrainCanvas.height;y++){if(isTerrain(Math.floor(tx),y)){landY=y;break}}
if(landY>waterLevel-30&&landY>=0){
// Move to safe position
playSound('jump');unit.vx=dir*5;unit.vy=-7;unit.onGround=false;unit.fallStartY=unit.y;gameState='moving';
setTimeout(()=>{if(gameState==='moving')startTurn()},1500);return;
}
}
// If no safe ground found, skip turn
startTurn();
}
// ==================== GAME FLOW ====================
function startGame(){
initAudio();playSound('click');
document.getElementById('menu-screen').classList.add('hidden');
['hud-top','turn-timer-bar','wind-indicator','hud-bottom'].forEach(id=>document.getElementById(id).style.display='flex');
generateTerrain();createTeams();
wind=(Math.random()-0.5)*10;activeTeam=0;activeUnit=0;startTurn();
}
function startTurn(){
if(aiTimeout){clearTimeout(aiTimeout);aiTimeout=null}
let found=false,attempts=0;
while(!found&&attempts<teams.length*4){
if(teams[activeTeam]?.alive&&teams[activeTeam].units[activeUnit]?.alive){found=true;break}
activeUnit++;if(activeUnit>=teams[activeTeam]?.units?.length){activeUnit=0;activeTeam=(activeTeam+1)%teams.length}
attempts++;
}
if(!found||!teams.some(t=>t.alive)){endGame();return}
turnTimer=TURN_TIME;gameState='weaponSelect';selectedWeapon=0;powerLevel=50;aimAngle=-45;
wind=(Math.random()-0.5)*12;updateHUD();
// If CPU team, run AI after delay
if(teams[activeTeam].isCPU){setTimeout(()=>runAI(),300)}
}
function skipTurn(){playSound('click');startTurn()}
function doMove(){if(gameState!=='weaponSelect')return;initAudio();playSound('click');gameState='moving'}
// ==================== INPUT ====================
let mouseX=0,mouseY=0,keys={};
canvas.addEventListener('mousemove',e=>{mouseX=e.clientX;mouseY=e.clientY});
canvas.addEventListener('mousedown',e=>handleClick(e));
document.addEventListener('keydown',e=>{keys[e.key]=true;handleKey(e)});
document.addEventListener('keyup',e=>{keys[e.key]=false});
function handleClick(e){
initAudio();
if(gameState==='weaponSelect'){
const worldX=(e.clientX-screenShake.x)/camera.zoom+camera.x;const worldY=(e.clientY-screenShake.y)/camera.zoom+camera.y;
const unit=teams[activeTeam].units[activeUnit];
// Click on friendly unit to switch
for(let i=0;i<teams[activeTeam].units.length;i++){const u=teams[activeTeam].units[i];if(!u.alive)continue;
if((u.x-worldX)**2+(u.y-worldY)**2<400){activeUnit=i;playSound('click');updateHUD();return}}
gameState='aiming';powerLevel=50;isCharging=false;chargeDir=1;
}else if(gameState==='aiming'){fireProjectile(activeTeam)}
else if(gameState==='moving'){const worldX=(e.clientX-screenShake.x)/camera.zoom+camera.x;const worldY=(e.clientY-screenShake.y)/camera.zoom+camera.y;moveUnit(worldX,worldY)}
}
function handleKey(e){
if(gameState==='weaponSelect'){
const num=parseInt(e.key);if(num>=1&&num<=WEAPONS.length){selectedWeapon=num-1;playSound('click');updateHUD()}
if(e.key==='Enter'||e.key===' '){gameState='aiming';powerLevel=50;isCharging=false;chargeDir=1}
}else if(gameState==='aiming'){if(e.key==='Escape'){gameState='weaponSelect';playSound('click')}}
}
function moveUnit(tx,ty){
const unit=teams[activeTeam].units[activeUnit];if(!unit.alive)return;
let landY=-1;for(let y=Math.floor(ty);y<terrainCanvas.height;y++){if(isTerrain(Math.floor(tx),y)){landY=y;break}}
if(landY===-1){gameState='weaponSelect';return}
const targetY=landY-WORM_RADIUS,dx=tx-unit.x,dy=targetY-unit.y,dist=Math.sqrt(dx*dx+dy*dy);
if(dist<5){gameState='weaponSelect';return}
playSound('jump');unit.vx=(dx/dist)*6;unit.vy=-8-Math.min(Math.abs(dy)*0.05,5);unit.onGround=false;unit.fallStartY=unit.y;
setTimeout(()=>{if(gameState==='moving'){gameState='weaponSelect';updateHUD()}},1500);
}
// ==================== GAME LOOP ====================
let lastTime=0;
function gameLoop(time){
const dt=Math.min(32,time-lastTime);lastTime=time;
if(gameState!=='menu'&&gameState!=='gameOver'){
if(impactFreeze>0){impactFreeze--;render();requestAnimationFrame(gameLoop);return}
teams.forEach(t=>t.units.forEach(u=>{if(u.alive)updateUnit(u)}));
updateProjectiles();updateParticles();updateDebris();updateFloatingTexts();updateCamera();
if(gameState==='weaponSelect'||gameState==='turnSelect'){
turnTimer-=dt/1000;
if(turnTimer<=3&&turnTimer>0){countdownTimer+=dt;if(countdownTimer>500){playSound('countdown');countdownTimer=0}}
if(turnTimer<=0)startTurn();
}
if(gameState==='firing'&&projectiles.length===0){gameState='weaponSelect';updateHUD()}
if(gameState==='aiming'){
const unit=teams[activeTeam].units[activeUnit];if(!unit)return;
const worldMX=(mouseX-screenShake.x)/camera.zoom+camera.x,worldMY=(mouseY-screenShake.y)/camera.zoom+camera.y;
aimAngle=Math.atan2(worldMY-(unit.y-12),worldMX-unit.x)*180/Math.PI;
if(keys[' ']||keys['Shift']){powerLevel+=chargeDir*2.5;if(powerLevel>=100){powerLevel=100;chargeDir=-1}if(powerLevel<=10){powerLevel=10;chargeDir=1}}
}
updateHUD();
}
render();requestAnimationFrame(gameLoop);
}
// ==================== END GAME ====================
function endGame(){
gameState='gameOver';const gs=document.getElementById('game-over-screen');gs.classList.remove('hidden');
const sl=document.getElementById('score-list');sl.innerHTML='';
const sorted=[...teams].sort((a,b)=>{const aS=a.units.filter(u=>u.alive).length*100+a.units.reduce((s,u)=>s+Math.max(0,u.hp),0);const bS=b.units.filter(u=>u.alive).length*100+b.units.reduce((s,u)=>s+Math.max(0,u.hp),0);return bS-aS});
sorted.forEach((team,i)=>{const alive=team.units.filter(u=>u.alive).length,totalHP=team.units.reduce((s,u)=>s+Math.max(0,u.hp),0);
const row=document.createElement('div');row.className='score-row';
row.innerHTML=`<span style="color:${team.color};font-size:1.3em">${i===0?'🏆':'#'}</span><b>${team.name}</b><span style="margin-left:auto">${alive} alive, ${totalHP} HP</span>`;sl.appendChild(row)});
}
// ==================== START ====================
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"d31a6d30-de7d-4ef8-bfe7-c7caea871ef2","tokensIn":66124,"tokensOut":28625,"tokensTotal":94749,"turns":4,"toolCalls":3,"failedToolCalls":0,"timestamp":"2026-07-18T23:21:04.044Z"} -->