← RTS results

RTS

Claude Sonnet 5.5 · full

Initial view of RTS

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 66,017 bytes · SHA-256 735b13b977c1
<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><title>FRONTIER WAR</title>
<style>
*{box-sizing:border-box;user-select:none}
html,body{margin:0;height:100%;overflow:hidden;background:#000;font-family:Segoe UI,Roboto,Arial,sans-serif;color:#cfe8f5}
#c{position:absolute;left:0;top:0;width:100%;height:100%;cursor:default}
#top{position:absolute;left:0;top:0;right:0;height:34px;background:linear-gradient(#0a1520f0,#0a1520b0);border-bottom:1px solid #2a5a78;display:flex;align-items:center;gap:18px;padding:0 12px;font-size:15px;z-index:5}
#top .sp{flex:1}
.res b{color:#5ff;font-weight:600}
button{font:inherit;background:#123044;color:#cfe8f5;border:1px solid #3a7ea0;border-radius:3px;padding:3px 10px;cursor:pointer}
button:hover{background:#1c4a68}
#bot{position:absolute;left:0;right:0;bottom:0;height:196px;background:linear-gradient(#0a1520e8,#050b12f5);border-top:2px solid #2a6a90;display:flex;gap:8px;padding:6px;z-index:5}
#mm{width:184px;height:184px;border:2px solid #2a6a90;background:#000;cursor:crosshair;flex:none}
#info{flex:1;min-width:0;background:#08121c;border:1px solid #1d4560;padding:6px 10px;overflow:auto;font-size:13px}
#card{width:300px;flex:none;display:grid;grid-template-columns:repeat(4,1fr);grid-auto-rows:56px;gap:4px}
.cb{position:relative;background:#10293b;border:1px solid #3a7ea0;border-radius:4px;font-size:11px;text-align:center;padding:2px;cursor:pointer;display:flex;flex-direction:column;justify-content:center;line-height:1.15}
.cb:hover{background:#1e4c6c}.cb .k{position:absolute;left:3px;top:1px;color:#ff9;font-size:10px}.cb .cst{color:#5ff;font-size:10px}
.cb.dis{opacity:.45;border-color:#444}
#msgs{position:absolute;left:12px;top:44px;z-index:4;pointer-events:none;font-size:14px;text-shadow:0 1px 3px #000}
#msgs div{margin-bottom:3px;padding:2px 8px;background:#000a;border-left:3px solid #5cf}
#msgs .bad{border-color:#f55;color:#fbb}#msgs .good{border-color:#5f8;color:#bfc}
.bar{height:7px;background:#300;border:1px solid #000;margin:2px 0;position:relative}.bar i{position:absolute;left:0;top:0;bottom:0;background:#3d5}
.bar.pr i{background:#4bf}
.chip{display:inline-block;margin:2px;padding:3px 7px;background:#123;border:1px solid #3a7ea0;border-radius:3px;cursor:pointer}
.chip:hover{background:#245}
.q{display:inline-block;width:64px;margin:2px;padding:2px;background:#123;border:1px solid #58a;font-size:11px;cursor:pointer;text-align:center;vertical-align:top}
.scr{position:absolute;inset:0;z-index:20;background:#000c;display:none;align-items:center;justify-content:center;flex-direction:column;text-align:center}
.scr.on{display:flex}
.scr .box{max-width:760px;background:#0a1a28;border:2px solid #3a8cb8;padding:22px 30px;border-radius:6px;box-shadow:0 0 40px #0af4;user-select:text;text-align:left;font-size:14px;line-height:1.45;max-height:92vh;overflow:auto}
.scr h1{font-size:46px;letter-spacing:8px;margin:0 0 4px;color:#7df;text-align:center;text-shadow:0 0 18px #0af}
.scr h2{color:#7df;margin:10px 0 4px;font-size:16px}
.scr button{font-size:20px;padding:8px 30px;margin:10px 6px 0}
#tip{position:absolute;z-index:30;background:#000e;border:1px solid #5cf;padding:5px 9px;font-size:12px;max-width:260px;pointer-events:none;display:none}
kbd{background:#234;border:1px solid #58a;border-radius:3px;padding:0 4px;font-size:12px}
</style></head><body>
<canvas id="c"></canvas>
<div id="top"><span class="res">◆ Ore <b id="rMin">0</b></span><span class="res">Supply <b id="rSup">0/0</b></span><span class="res">Army <b id="rArmy">0</b></span><span class="res">Time <b id="rTime">0:00</b></span><span class="sp"></span>
<button id="bRotL" title="Rotate ( , )">⟲</button><button id="bRotR" title="Rotate ( . )">⟳</button><button id="bPause">Pause</button><button id="bMute">Sound: On</button><button id="bHelp">Help</button></div>
<div id="msgs"></div>
<div id="bot"><canvas id="mm" width="184" height="184"></canvas><div id="info"></div><div id="card"></div></div>
<div id="tip"></div>

<div class="scr on" id="title"><div class="box">
<h1>FRONTIER WAR</h1>
<div style="text-align:center;color:#8bd;margin-bottom:8px">The <b style="color:#4af">Vanguard Expedition</b> vs the <b style="color:#f66">Kharon Syndicate</b> — one ore-rich frontier world.</div>
<h2>Objective</h2>Destroy <b>all enemy Headquarters</b> to win. Lose all of yours and you are defeated. Build extra HQs near distant ore to expand. The enemy base location is <b>unknown</b> — scout to find it.
<h2>Quick controls</h2>
<b>Left-click / drag</b> select · <b>Right-click</b> move / attack / mine / set rally · <kbd>Shift</kbd> add/queue · <kbd>A</kbd>+click attack-move · <kbd>S</kbd> stop · <kbd>H</kbd> hold · <kbd>Ctrl+1-9</kbd> set group, <kbd>1-9</kbd> recall · double-click = select same type · <b>Arrows / screen edge</b> pan · <b>Wheel</b> zoom · <kbd>,</kbd> <kbd>.</kbd> or ⟲⟳ rotate · minimap click = jump camera, right-click = order · <kbd>P</kbd> pause · <kbd>F1</kbd> help.
<div style="text-align:center"><button id="bStart">START MATCH</button></div></div></div>

<div class="scr" id="pause"><div class="box"><h1 style="font-size:32px">PAUSED</h1><div style="text-align:center"><button id="bResume">Resume</button><button id="bRestart1">Restart</button></div></div></div>
<div class="scr" id="help"><div class="box">
<h2>Economy</h2>Drones mine finite ore crystals and return it to an HQ (drop-off). Ore is the only resource; expand with new HQs beside distant deposits for faster returns. Crystals deplete.
<h2>Supply</h2>Units use supply (used / available). HQ gives 12, Depot gives 10. There is no hard cap — keep building Depots. Queued units reserve supply; cancelling refunds 100% cost. Destroyed Depots block new production but never delete units.
<h2>Buildings</h2>Select a Drone, choose a building, click a valid (green) spot. Drone builds it. Cancelling a construction refunds 75%. Barracks → Lab, Airfield, Turret. Tanks need a Lab.
<h2>Units &amp; counters</h2>Damage: <b>Kinetic</b> (strong vs light, weak vs heavy) and <b>Explosive</b> (strong vs heavy &amp; buildings, weak vs light).<br>
<b>Rifleman</b> (light, ranged, hits air) &gt; <b>Wasp</b> (light air, explosive, ground only) &gt; <b>Bulwark tank</b> (heavy, explosive splash, ground only) &gt; <b>Lancer</b> (heavy melee, kinetic, ground only) &gt; Riflemen. Turrets hit ground+air.
<h2>Fog of war</h2>Black = unexplored, dim = explored but not visible (enemy units vanish; seen enemy buildings stay as last-known silhouettes). Camera movement reveals nothing. Units only auto-target what you can see.
<h2>Upgrades</h2>Lab: Weapons (+15%/level), Plating (−12% damage/level), Drone Logistics (carry 8 ore).
<div style="text-align:center"><button id="bHelpClose">Close</button></div></div></div>
<div class="scr" id="end"><div class="box"><h1 id="endT">VICTORY</h1><div id="endS" style="text-align:center"></div><div style="text-align:center"><button id="bRestart2">Play Again</button></div></div></div>

<script>
'use strict';
const $=id=>document.getElementById(id);
const cv=$('c'),g=cv.getContext('2d');
let VW=0,VH=0;
function resize(){VW=cv.width=innerWidth;VH=cv.height=innerHeight}
addEventListener('resize',resize);resize();
const CS=2,N=96,WD=N*CS;
const cl=(v,a,b)=>v<a?a:v>b?b:v;
function mul(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
/* ---------- data ---------- */
const UT={
 drone:{n:'Drone',hp:40,sp:10,r:.7,sup:1,cost:50,time:11,sight:13,arm:'L',air:0,w:null,desc:'Mines ore, builds structures.'},
 rifle:{n:'Rifleman',hp:60,sp:9,r:.7,sup:1,cost:60,time:13,sight:14,arm:'L',air:0,w:{d:8,t:'K',rng:9,cd:.8,g:1,a:1,proj:60},desc:'Light ranged. Kinetic. Hits ground & air. Counters Wasps and Lancers... at range.'},
 lancer:{n:'Lancer',hp:140,sp:12,r:.9,sup:2,cost:80,time:15,sight:12,arm:'H',air:0,w:{d:18,t:'K',rng:.6,cd:.8,g:1,a:0},desc:'Heavy melee. Fast. Kinetic, ground only. Shreds Riflemen; weak to Tanks.'},
 tank:{n:'Bulwark Tank',hp:340,sp:6.5,r:1.4,sup:3,cost:200,time:26,sight:15,arm:'H',air:0,req:'lab',w:{d:34,t:'E',rng:14,cd:1.5,g:1,a:0,proj:30,spl:2.6},desc:'Heavy, slow. Explosive splash, ground only. Needs Lab. Weak to Wasps.'},
 wasp:{n:'Wasp Gunship',hp:95,sp:14,r:.9,sup:2,cost:150,time:22,sight:15,arm:'L',air:1,w:{d:10,t:'E',rng:8,cd:.65,g:1,a:0,proj:50},desc:'Flying. Explosive bombs, ground only. Crushes Tanks; die to Riflemen.'}
};
const BT={
 hq:{n:'Headquarters',sz:4,hp:1500,cost:400,time:55,sup:12,sight:17,prod:['drone'],desc:'Ore drop-off. Trains Drones. +12 supply.'},
 depot:{n:'Supply Depot',sz:2,hp:350,cost:100,time:18,sup:10,sight:9,desc:'+10 supply.'},
 barracks:{n:'Barracks',sz:3,hp:700,cost:150,time:32,sup:0,sight:11,prod:['rifle','lancer','tank'],desc:'Trains ground units.'},
 airfield:{n:'Airfield',sz:3,hp:650,cost:200,time:38,sup:0,sight:11,req:'barracks',prod:['wasp'],desc:'Trains flying units. Needs Barracks.'},
 lab:{n:'Tech Lab',sz:3,hp:600,cost:120,time:30,sup:0,sight:11,req:'barracks',res:1,desc:'Research upgrades; unlocks Tanks. Needs Barracks.'},
 turret:{n:'Flak Turret',sz:2,hp:450,cost:100,time:22,sup:0,sight:16,req:'barracks',w:{d:12,t:'K',rng:15,cd:.6,g:1,a:1,proj:70},desc:'Defense. Hits ground & air. Needs Barracks.'}
};
const UP={w:{n:'Weapon Systems',cost:[150,275],time:[35,50],max:2,desc:'+15% damage per level.'},a:{n:'Composite Plating',cost:[150,275],time:[35,50],max:2,desc:'-12% damage taken per level.'},l:{n:'Drone Logistics',cost:[100],time:[25],max:1,desc:'Drones carry 8 ore per trip.'}};
const MOD={K:{L:1.25,H:.6,B:.75},E:{L:.6,H:1.4,B:1.35}};
/* ---------- state ---------- */
let rock,bldA,minA,walkA,ver=0,units,blds,deps,projs,parts,fts,beams,nid,T,S,sel,groups,mem,gameOn,paused,stats,sites,ai,kills,lastAlert=-99,alertPos=null,nextSpecial=0;
const vis=[new Uint8Array(N*N),new Uint8Array(N*N)],expl=[new Uint8Array(N*N),new Uint8Array(N*N)];
const cam={x:34,z:150,yaw:0,dist:70};
const SITES=[[17,79,-1,1],[79,17,1,-1],[32,62,-1,.4],[64,34,1,-.4],[12,50,-1,0],[84,46,1,0],[48,85,0,1],[48,11,0,-1],[48,48,-1,-1]];
const upd=i=>{walkA[i]=(rock[i]||bldA[i]||minA[i])?0:1};
function genMap(){
 for(let seed=3;;seed++){
  const r=mul(seed*7919);rock=new Uint8Array(N*N);
  for(let i=0;i<N;i++){for(const k of[0,1,N-2,N-1]){rock[k*N+i]=1;rock[i*N+k]=1}}
  const dirs=[[1,0],[0,1],[1,1],[1,-1]];
  for(let k=0;k<6;k++){const d=dirs[k%4],x0=8+r()*80|0,y0=8+r()*80|0,len=18+r()*14|0,gp=4+r()*(len-10)|0;
   for(let s=0;s<len;s++){if(s>=gp&&s<gp+6)continue;const x=x0+d[0]*s,y=y0+d[1]*s;for(let t=0;t<2;t++){const xx=x+(d[1]?t:0),yy=y+(d[0]&&!d[1]?t:d[1]&&d[0]?0:t);if(xx>0&&yy>0&&xx<N&&yy<N)rock[yy*N+xx]=1}}}
  for(let k=0;k<22;k++){const cx=r()*N,cy=r()*N,rad=2+r()*4;for(let y=-8;y<=8;y++)for(let x=-8;x<=8;x++){const xx=cx+x|0,yy=cy+y|0;if(x*x+y*y<rad*rad&&xx>0&&yy>0&&xx<N-1&&yy<N-1)rock[yy*N+xx]=1}}
  for(const s of SITES){for(let y=-9;y<=9;y++)for(let x=-9;x<=9;x++){const xx=s[0]+x,yy=s[1]+y;if(x*x+y*y<=81&&xx>1&&yy>1&&xx<N-2&&yy<N-2)rock[yy*N+xx]=0}}
  // connectivity
  const seen=new Uint8Array(N*N),q=[SITES[0][1]*N+SITES[0][0]];seen[q[0]]=1;
  for(let h=0;h<q.length;h++){const c=q[h],cx=c%N,cy=c/N|0;for(const[dx,dy]of[[1,0],[-1,0],[0,1],[0,-1]]){const n=(cy+dy)*N+cx+dx;if(!seen[n]&&!rock[n]){seen[n]=1;q.push(n)}}}
  if(SITES.every(s=>seen[s[1]*N+s[0]])&&q.length>N*N*.55)return;
 }
}
function newGame(){
 genMap();
 bldA=new Uint8Array(N*N);minA=new Uint8Array(N*N);walkA=new Uint8Array(N*N);
 for(let i=0;i<N*N;i++)upd(i);
 units=[];blds=[];deps=[];projs=[];parts=[];fts=[];beams=[];nid=0;T=0;ver++;sel=[];groups={};kills=[0,0];
 mem=[new Map(),new Map()];vis[0].fill(0);vis[1].fill(0);expl[0].fill(0);expl[1].fill(0);
 S=[0,1].map(()=>({min:500,up:{w:0,a:0,l:0},cap:0,used:0}));
 sites=SITES.map((s,i)=>({i,x:s[0]*CS,z:s[1]*CS,cx:s[0],cy:s[1],dx:s[2],dz:s[3]}));
 sites.forEach((s,i)=>{const a0=Math.atan2(s.dz,s.dx),amt=i<2?1600:1300;
  for(let k=0;k<8;k++){const a=a0+(k-3.5)*.3,cx=Math.round(s.cx+Math.cos(a)*5.8),cy=Math.round(s.cy+Math.sin(a)*5.8),ci=cy*N+cx;
   if(minA[ci]||cx<2||cy<2||cx>N-3||cy>N-3)continue;rock[ci]=0;minA[ci]=1;upd(ci);
   deps.push({id:++nid,cx,cy,x:(cx+.5)*CS,z:(cy+.5)*CS,amt,max:amt,wk:0,site:i,rot:Math.random()*6})}});
 for(const [side,si] of [[0,0],[1,1]]){const s=sites[si];const h=mkBld(side,'hq',s.cx-2,s.cy-2,true);
  for(let k=0;k<4;k++){const u=mkUnit(side,'drone',h.x+(k-1.5)*2,h.z+h.rad+2);const d=nearestDep(u.x,u.z,60,side);if(d)u.orders=[{t:'g',dep:d}]}}
 cam.x=sites[0].x+6;cam.z=sites[0].z-6;cam.yaw=0;cam.dist=60;
 ai={wave:0,attacking:false,launch:0,scout:null,scouted:{},lastExp:0,thr:8,home:sites[1],hist:[]};
 gameOn=true;paused=false;stats={};sel=[];lastAlert=-99;
 updVision();calcSupply();
}
function mkUnit(side,type,x,z){const d=UT[type];const u={id:++nid,isU:1,side,type,d,x,z,y:0,hp:d.hp,mhp:d.hp,orders:[],cd:0,scan:Math.random()*.4,carry:0,air:d.air,rad:d.r,arm:d.arm,w:d.w,face:Math.random()*6.28,tgt:null,hurt:0,mv:0,dead:false};units.push(u);return u}
function mkBld(side,type,cx,cy,built){const d=BT[type];const b={id:++nid,isB:1,side,type,d,cx,cy,sz:d.sz,x:(cx+d.sz/2)*CS,z:(cy+d.sz/2)*CS,rad:d.sz*CS/2*.95,hp:built?d.hp:d.hp*.1,mhp:d.hp,built,prog:built?1:0,queue:[],rally:null,cd:0,hurt:0,arm:'B',w:d.w||null,tgt:null,scan:0,face:0,dead:false,air:0};
 for(let j=0;j<d.sz;j++)for(let i=0;i<d.sz;i++){const k=(cy+j)*N+cx+i;bldA[k]=1;upd(k)}ver++;blds.push(b);return b}
function nearestWalk(x,z){const ci=cl(x/CS|0,0,N-1),cj=cl(z/CS|0,0,N-1);if(walkA[cj*N+ci])return[x,z];
 for(let r=1;r<14;r++)for(let j=-r;j<=r;j++)for(let i=-r;i<=r;i++){if(Math.max(Math.abs(i),Math.abs(j))!==r)continue;const a=ci+i,b=cj+j;if(a>0&&b>0&&a<N-1&&b<N-1&&walkA[b*N+a])return[(a+.5)*CS,(b+.5)*CS]}return[x,z]}
const walkAt=(x,z)=>{const i=x/CS|0,j=z/CS|0;return i>=0&&j>=0&&i<N&&j<N&&walkA[j*N+i]===1};
function lineClear(x0,z0,x1,z1){const d=Math.hypot(x1-x0,z1-z0),n=Math.ceil(d/1.2);for(let k=1;k<n;k++){const t=k/n;if(!walkAt(x0+(x1-x0)*t,z0+(z1-z0)*t))return false}return true}
/* ---------- flow fields ---------- */
const fcache=new Map();let fver=-1;const DX=[1,-1,0,0,1,1,-1,-1],DY=[0,0,1,-1,1,-1,1,-1];const FQ=new Int32Array(N*N);
function getField(wx,wz,r){
 if(fver!==ver){fcache.clear();fver=ver}
 const gi=cl(wx/CS|0,1,N-2),gj=cl(wz/CS|0,1,N-2),key=(gj*N+gi)*8+r;let f=fcache.get(key);if(f)return f;
 if(fcache.size>90)fcache.clear();
 const d=new Uint16Array(N*N).fill(65535);
 const open=(i,j)=>i>=0&&j>=0&&i<N&&j<N&&(walkA[j*N+i]||(Math.abs(i-gi)<=r&&Math.abs(j-gj)<=r&&i>0&&j>0&&i<N-1&&j<N-1));
 let qh=0,qt=0;FQ[qt++]=gj*N+gi;d[gj*N+gi]=0;
 while(qh<qt){const c=FQ[qh++],cx=c%N,cy=c/N|0,dc=d[c]+1;
  for(let k=0;k<8;k++){const nx=cx+DX[k],ny=cy+DY[k];if(!open(nx,ny))continue;const n=ny*N+nx;if(d[n]<=dc)continue;
   if(k>3&&!(open(cx+DX[k],cy)&&open(cx,cy+DY[k])))continue;d[n]=dc;FQ[qt++]=n}}
 fcache.set(key,d);return d}
let DOX=0,DOZ=0;
function descend(f,x,z,tx,tz){const ci=cl(x/CS|0,0,N-1),cj=cl(z/CS|0,0,N-1),c=cj*N+ci;let best=f[c],bx=-1,by=-1,bt=1e9;if(best===65535)return false;
 for(let k=0;k<8;k++){const nx=ci+DX[k],ny=cj+DY[k];if(nx<0||ny<0||nx>=N||ny>=N)continue;const dn=f[ny*N+nx];if(dn>best)continue;
  if(k>3&&(f[cj*N+nx]===65535||f[ny*N+ci]===65535||(!walkA[cj*N+nx]&&!walkA[ny*N+ci])))continue;
  const px=(nx+.5)*CS-tx,pz=(ny+.5)*CS-tz,tb=px*px+pz*pz;if(dn<best||tb<bt){best=dn;bx=nx;by=ny;bt=tb}}
 if(bx<0)return false;DOX=(bx+.5)*CS;DOZ=(by+.5)*CS;return true}
/* ---------- vision / helpers ---------- */
const cidx=(x,z)=>cl(z/CS|0,0,N-1)*N+cl(x/CS|0,0,N-1);
function seen(s,t){if(t.side===s)return true;const v=vis[s];if(!t.isB)return v[cidx(t.x,t.z)]===2;const h=t.rad*.8;return v[cidx(t.x,t.z)]===2||v[cidx(t.x-h,t.z-h)]===2||v[cidx(t.x+h,t.z-h)]===2||v[cidx(t.x-h,t.z+h)]===2||v[cidx(t.x+h,t.z+h)]===2}
function markDisc(s,x,z,r){const v=vis[s],e=expl[s],ci=x/CS|0,cj=z/CS|0,rc=r/CS,R=Math.ceil(rc);
 for(let j=-R;j<=R;j++){const yy=cj+j;if(yy<0||yy>=N)continue;const w=Math.sqrt(Math.max(0,rc*rc-j*j))|0;const a=Math.max(0,ci-w),b=Math.min(N-1,ci+w);for(let i=a;i<=b;i++){v[yy*N+i]=2;e[yy*N+i]=1}}}
function updVision(){
 vis[0].fill(0);vis[1].fill(0);
 for(const u of units)if(!u.dead)markDisc(u.side,u.x,u.z,u.d.sight);
 for(const b of blds)if(!b.dead)markDisc(b.side,b.x,b.z,b.built?b.d.sight:8);
 for(const s of[0,1]){const m=mem[s];
  for(const b of blds){if(b.side===s||b.dead)continue;if(seen(s,b))m.set(b.id,{b,x:b.x,z:b.z,type:b.type,sz:b.sz,side:b.side,cx:b.cx,cy:b.cy,prog:b.built?1:b.prog})}
  for(const[id,e]of m){if(e.b.dead&&vis[s][cidx(e.x,e.z)]===2)m.delete(id)}}
}
function calcSupply(){for(const s of[0,1]){let cap=0,used=0;for(const b of blds)if(b.side===s&&!b.dead){if(b.built)cap+=b.d.sup;for(const q of b.queue)if(q.k==='u')used+=UT[q.type].sup}
 for(const u of units)if(u.side===s&&!u.dead)used+=u.d.sup;S[s].cap=cap;S[s].used=used}}
function hasB(s,t){return blds.some(b=>b.side===s&&b.type===t&&b.built&&!b.dead)}
function nearestDep(x,z,maxd,side){let best=null,bs=1e9;for(const d of deps){if(d.amt<=0||(side!==undefined&&!expl[side][d.cy*N+d.cx]))continue;const dd=Math.hypot(d.x-x,d.z-z);if(dd>maxd)continue;const s=dd+d.wk*6;if(s<bs){bs=s;best=d}}return best}
function nearestHQ(u){let best=null,bs=1e9;for(const b of blds)if(b.side===u.side&&b.type==='hq'&&b.built&&!b.dead){const d=Math.hypot(b.x-u.x,b.z-u.z);if(d<bs){bs=d;best=b}}return best}
/* ---------- hash ---------- */
const HN=48;const hHead=new Int32Array(HN*HN);let hNext=new Int32Array(2048);
function rebuildHash(){hHead.fill(-1);if(hNext.length<units.length)hNext=new Int32Array(units.length*2);
 for(let i=0;i<units.length;i++){const u=units[i];if(u.dead)continue;const c=cl(u.z/4|0,0,HN-1)*HN+cl(u.x/4|0,0,HN-1);hNext[i]=hHead[c];hHead[c]=i}}
/* ---------- combat ---------- */
const canHit=(w,t)=>t.air?w.a:w.g;
function validT(a,t){return !t.dead&&canHit(a.w,t)&&seen(a.side,t)}
function acquire(a,range){
 let best=null,bs=1e9;const w=a.w,R=Math.ceil((range+2)/4);const cx=a.x/4|0,cz=a.z/4|0;
 for(let j=Math.max(0,cz-R);j<=Math.min(HN-1,cz+R);j++)for(let i=Math.max(0,cx-R);i<=Math.min(HN-1,cx+R);i++){
  for(let k=hHead[j*HN+i];k>=0;k=hNext[k]){const u=units[k];if(u.dead||u.side===a.side||!canHit(w,u))continue;const d=Math.hypot(u.x-a.x,u.z-a.z)-u.rad;if(d>range)continue;if(!seen(a.side,u))continue;const s=d+(u.type==='drone'?5:0);if(s<bs){bs=s;best=u}}}
 for(const b of blds){if(b.dead||b.side===a.side||!canHit(w,b))continue;const d=Math.hypot(b.x-a.x,b.z-a.z)-b.rad;if(d>range)continue;if(!seen(a.side,b))continue;const s=d+(b.w?12:35);if(s<bs){bs=s;best=b}}
 return best}
function engage(a,dt,chase,lim){
 const t=a.tgt;if(!validT(a,t)){a.tgt=null;return false}
 const dx=t.x-a.x,dz=t.z-a.z,dc=Math.hypot(dx,dz),d=dc-t.rad-a.rad;
 if(a.isB||d<=a.w.rng){a.face=Math.atan2(dx,dz);if(a.cd<=0&&d<=a.w.rng+(a.isB?a.rad:0)){fire(a,t)}return true}
 if(!chase||dc>lim){a.tgt=null;return false}
 stepTo(a,t.x,t.z,dt,t.isB?Math.ceil(t.sz/2)+1:0);return true}
function fire(a,t){
 const w=a.w;a.cd=w.cd*(0.9+Math.random()*.2);let dmg=w.d*(1+.15*S[a.side].up.w);
 const ay=a.air?5:(a.isB?3:1.1);
 if(w.proj){projs.push({x:a.x,y:ay,z:a.z,t,dmg,ty:w.t,sp:w.proj,spl:w.spl||0,side:a.side,big:w.spl?1:0,tx:t.x,tz:t.z,dx:0,dz:0,dy:0})}
 else{hit(t,dmg,w.t,0,a.side);spark(t.x,1,t.z,'#ff8',4)}
 if(vis[0][cidx(a.x,a.z)]===2||vis[0][cidx(t.x,t.z)]===2)snd(w.spl?90:w.t==='E'?160:w.proj?420:200,.06,'square',.04)}
function hit(t,dmg,ty,spl,side){
 if(t.dead)return;const apply=(e,m)=>{let d=dmg*m*MOD[ty][e.arm];if(e.isU||e.isB)d*=1-.12*S[e.side].up.a;e.hp-=d;e.hurt=.25;
  if(e.hp<=0)kill(e,side);else if(e.side===0&&T-lastAlert>10&&(e.isB||e.type!=='drone'||true)){lastAlert=T;alertPos=[e.x,e.z];toast('⚠ Your forces are under attack!','bad');snd(220,.25,'sawtooth',.08)}
  if(e.side===1)ai.alert={x:e.x,z:e.z,t:T}};
 apply(t,1);
 if(spl){const R=Math.ceil(spl/4)+1,cx=t.x/4|0,cz=t.z/4|0;for(let j=Math.max(0,cz-R);j<=Math.min(HN-1,cz+R);j++)for(let i=Math.max(0,cx-R);i<=Math.min(HN-1,cx+R);i++)for(let k=hHead[j*HN+i];k>=0;k=hNext[k]){const u=units[k];if(u.dead||u===t||u.side===side||u.air!==t.air)continue;if(Math.hypot(u.x-t.x,u.z-t.z)<spl)apply(u,.5)}}}
function kill(e,by){if(e.dead)return;e.dead=true;if(by!==undefined)kills[by]++;
 const big=e.isB;spark(e.x,e.air?5:1,e.z,e.side?'#f84':'#6cf',big?40:12);spark(e.x,e.air?5:1,e.z,'#fd6',big?30:8);
 if(vis[0][cidx(e.x,e.z)]===2)snd(big?60:110,big?.6:.25,'sawtooth',big?.16:.07);
 if(big){for(let j=0;j<e.sz;j++)for(let i=0;i<e.sz;i++){const k=(e.cy+j)*N+e.cx+i;bldA[k]=0;upd(k)}ver++}
 sel=sel.filter(x=>x!==e)}
function spark(x,y,z,col,n){for(let i=0;i<n&&parts.length<1400;i++){const a=Math.random()*6.28,s=2+Math.random()*6;parts.push({x,y:y+Math.random(),z,vx:Math.cos(a)*s,vy:2+Math.random()*6,vz:Math.sin(a)*s,life:.4+Math.random()*.6,max:1,col,sz:1+Math.random()*1.5})}}
/* ---------- movement ---------- */
function stepTo(u,tx,tz,dt,r){
 let dx=tx-u.x,dz=tz-u.z;const d=Math.hypot(dx,dz)||1e-6;let mx=dx/d,mz=dz/d;
 if(!u.air&&!(d<3||(d<26&&lineClear(u.x,u.z,tx,tz)))){const f=getField(tx,tz,r|0);if(descend(f,u.x,u.z,tx,tz)){const ex=DOX-u.x,ez=DOZ-u.z,e=Math.hypot(ex,ez)||1;mx=ex/e;mz=ez/e}}
 const sp=u.d.sp*dt,nx=u.x+mx*sp,nz=u.z+mz*sp;u.mv=1;u.face=Math.atan2(mx,mz);
 if(u.air){u.x=cl(nx,2,WD-2);u.z=cl(nz,2,WD-2)}
 else if(walkAt(nx,nz)){u.x=nx;u.z=nz}else if(walkAt(nx,u.z))u.x=nx;else if(walkAt(u.x,nz))u.z=nz}
function separate(){
 for(let i=0;i<units.length;i++){const u=units[i];if(u.dead)continue;const cx=u.x/4|0,cz=u.z/4|0;
  for(let j=Math.max(0,cz-1);j<=Math.min(HN-1,cz+1);j++)for(let ii=Math.max(0,cx-1);ii<=Math.min(HN-1,cx+1);ii++)for(let k=hHead[j*HN+ii];k>=0;k=hNext[k]){
   if(k<=i)continue;const v=units[k];if(v.dead||v.air!==u.air)continue;let dx=v.x-u.x,dz=v.z-u.z;const m=u.rad+v.rad+.1;const d2=dx*dx+dz*dz;if(d2>=m*m)continue;
   let d=Math.sqrt(d2);if(d<.01){dx=Math.random()-.5;dz=Math.random()-.5;d=Math.hypot(dx,dz)+.001}
   const p=(m-d),ux=dx/d,uz=dz/d;let wu=.5,wv=.5;if(u.mv&&!v.mv){wu=.15;wv=.85}else if(v.mv&&!u.mv){wu=.85;wv=.15}
   if(u.air){u.x-=ux*p*wu;u.z-=uz*p*wu;v.x+=ux*p*wv;v.z+=uz*p*wv}
   else{let a=u.x-ux*p*wu,b=u.z-uz*p*wu;if(walkAt(a,b)){u.x=a;u.z=b}a=v.x+ux*p*wv;b=v.z+uz*p*wv;if(walkAt(a,b)){v.x=a;v.z=b}}}}}
function combat(u,dt,range,chase,lim){
 if(!u.w)return false;if(u.tgt&&!validT(u,u.tgt))u.tgt=null;
 if(!u.tgt){u.scan-=dt;if(u.scan<=0){u.scan=.25+Math.random()*.2;u.tgt=acquire(u,range)}}
 return u.tgt?engage(u,dt,chase,lim):false}
function updUnit(u,dt){
 u.cd-=dt;u.hurt-=dt;u.mv=0;
 if(!u.air&&!walkAt(u.x,u.z)){const p=nearestWalk(u.x,u.z);u.x=p[0];u.z=p[1]}
 const o=u.orders[0];
 if(!o){if(u.w)combat(u,dt,Math.max(u.w.rng+2,9),true,18);return}
 switch(o.t){
  case 'm':{const d=Math.hypot(o.x-u.x,o.z-u.z);if(d<(o.arr||1))u.orders.shift();else stepTo(u,o.x,o.z,dt,0);break}
  case 'a':{if(combat(u,dt,Math.max(u.w?u.w.rng+3:0,11),true,26))break;const d=Math.hypot(o.x-u.x,o.z-u.z);if(d<(o.arr||1.5))u.orders.shift();else stepTo(u,o.x,o.z,dt,0);break}
  case 'h':combat(u,dt,u.w?u.w.rng:0,false,0);break;
  case 'k':{const t=o.tg;if(u.w&&!t.dead&&seen(u.side,t)&&canHit(u.w,t)){o.lx=t.x;o.lz=t.z;u.tgt=t;engage(u,dt,true,999)}
   else if(o.lx!==undefined&&Math.hypot(o.lx-u.x,o.lz-u.z)>2&&!t.dead){stepTo(u,o.lx,o.lz,dt,0)}else u.orders.shift();break}
  case 'g':{const dp=o.dep;
   if(dp.amt<=0){const nd=nearestDep(u.x,u.z,35,u.side);if(nd)o.dep=nd;else{u.orders.shift();break}}
   const cap=S[u.side].up.l?8:5;if(u.carry>=cap){u.orders.unshift({t:'r'});break}
   const d=Math.hypot(dp.x-u.x,dp.z-u.z);
   if(d<2.4){o.m=(o.m||0)+dt;u.face=Math.atan2(dp.x-u.x,dp.z-u.z);u.mining=1;if(o.m>=1.6){o.m=0;const take=Math.min(cap,dp.amt);dp.amt-=take;u.carry=take;if(dp.amt<=0){minA[dp.cy*N+dp.cx]=0;upd(dp.cy*N+dp.cx);ver++}u.orders.unshift({t:'r'})}}
   else{u.mining=0;stepTo(u,dp.x,dp.z,dt,1)}break}
  case 'r':{const hq=nearestHQ(u);if(!hq||u.carry<=0){u.orders.shift();break}
   const d=Math.hypot(hq.x-u.x,hq.z-u.z)-hq.rad;
   if(d<2){S[u.side].min+=u.carry;if(u.side===0&&vis[0][cidx(u.x,u.z)]===2)fts.push({x:u.x,y:2,z:u.z,txt:'+'+u.carry,life:1.2});u.carry=0;u.orders.shift();
    if(!u.orders.length){const nd=nearestDep(u.x,u.z,60,u.side);if(nd)u.orders=[{t:'g',dep:nd}]}}
   else stepTo(u,hq.x,hq.z,dt,Math.ceil(hq.sz/2)+1);break}
  case 'b':{const b=o.b;if(b.dead||b.built){u.orders.shift();if(!u.orders.length){const nd=nearestDep(u.x,u.z,60,u.side);if(nd)u.orders=[{t:'g',dep:nd}]}break}
   const d=Math.hypot(b.x-u.x,b.z-u.z)-b.rad;if(d<2.2){b.bt=T;u.face=Math.atan2(b.x-u.x,b.z-u.z);u.mining=1}else{u.mining=0;stepTo(u,b.x,b.z,dt,Math.ceil(b.sz/2)+1)}break}
 }
}
function updBld(b,dt){
 b.cd-=dt;b.hurt-=dt;
 if(!b.built){if(b.bt!==undefined&&T-b.bt<.3){b.prog+=dt/b.d.time;b.hp=Math.min(b.mhp,b.hp+b.mhp*.9*dt/b.d.time);if(b.prog>=1){b.built=true;b.hp=Math.max(b.hp,b.mhp*.98);if(b.side===0){toast(b.d.n+' complete','good');snd(660,.2,'sine',.08)}}}return}
 if(b.queue.length){const q=b.queue[0];
  if(q.k==='u'&&S[b.side].used>S[b.side].cap){b.blocked=1;if(b.side===0&&T-(b.bmsg||-99)>12){b.bmsg=T;toast('Supply blocked — build more Supply Depots!','bad');snd(150,.25,'square',.08)}}
  else{b.blocked=0;q.t+=dt;if(q.t>=q.tt){b.queue.shift();
   if(q.k==='u'){spawn(b,q.type)}else{S[b.side].up[q.type]++;if(b.side===0){toast('Research complete: '+UP[q.type].n,'good');snd(880,.3,'sine',.08)}}}}}
 if(b.w){if(b.tgt&&!validT(b,b.tgt))b.tgt=null;if(!b.tgt){b.scan-=dt;if(b.scan<=0){b.scan=.35;b.tgt=acquire(b,b.w.rng+b.rad)}}if(b.tgt)engage(b,dt,false,0)}
}
function spawn(b,type){
 const a=Math.random()*3+.2;let x=b.x+Math.sin(a)*(b.rad+1.5),z=b.z+Math.cos(a)*(b.rad+1.5);const p=nearestWalk(x,z);
 const u=mkUnit(b.side,type,p[0],p[1]);u.y=u.air?5:0;
 if(b.side===0){toast(u.d.n+' ready','good');if(T-(nextSpecial)>.5){nextSpecial=T;snd(520,.1,'sine',.05)}}
 if(b.rally){const r=b.rally;if(r.dep&&type==='drone'&&r.dep.amt>0)u.orders=[{t:'g',dep:r.dep}];else u.orders=[{t:'m',x:r.x,z:r.z,arr:1.2}]}
 else if(type==='drone'){const d=nearestDep(b.x,b.z,50,b.side);if(d)u.orders=[{t:'g',dep:d}]}
}
function enqueueUnit(b,type){
 const d=UT[type],s=S[b.side];
 if(!b.built||b.dead)return'Not finished';if(d.req&&!hasB(b.side,d.req))return'Requires '+BT[d.req].n;if(b.queue.length>=6)return'Queue full';
 if(s.min<d.cost)return'Not enough ore';calcSupply();if(s.used+d.sup>s.cap)return'Supply capped — build Supply Depots';
 s.min-=d.cost;b.queue.push({k:'u',type,t:0,tt:d.time});s.used+=d.sup;return null}
function enqueueRes(b,k){
 const s=S[b.side],lv=s.up[k],u=UP[k];if(!b.built)return'Not finished';if(lv>=u.max)return'Fully researched';
 if(blds.some(x=>x.side===b.side&&!x.dead&&x.queue.some(q=>q.k==='r'&&q.type===k)))return'Already researching';
 if(b.queue.some(q=>q.k==='r'))return'Lab busy';
 if(s.min<u.cost[lv])return'Not enough ore';s.min-=u.cost[lv];b.queue.push({k:'r',type:k,t:0,tt:u.time[lv],cost:u.cost[lv]});return null}
function cancelQ(b,i){const q=b.queue[i];if(!q)return;const s=S[b.side];s.min+=q.k==='u'?UT[q.type].cost:q.cost;b.queue.splice(i,1);calcSupply();if(b.side===0){toast('Cancelled — 100% refund','')}}
function canPlace(side,type,cx,cy,ai){
 const sz=BT[type].sz;if(cx<2||cy<2||cx+sz>N-2||cy+sz>N-2)return false;
 for(let j=-1;j<=sz;j++)for(let i=-1;i<=sz;i++){const k=(cy+j)*N+cx+i,inside=i>=0&&j>=0&&i<sz&&j<sz;
  if(inside){if(!walkA[k]||!expl[side][k])return false}else if(bldA[k]||minA[k])return false}
 if(ai||type!=='hq'){/* keep clear of ore lines */for(const d of deps){if(d.amt>0&&Math.abs(d.cx-(cx+sz/2))<sz/2+(ai?4:1.5)&&Math.abs(d.cy-(cy+sz/2))<sz/2+(ai?4:1.5)&&type!=='hq')return false}}
 return true}
function placeBld(side,type,cx,cy,drones,queue){
 const d=BT[type],s=S[side];if(s.min<d.cost)return'Not enough ore';if(d.req&&!hasB(side,d.req))return'Requires '+BT[d.req].n;if(!canPlace(side,type,cx,cy,false))return'Invalid location';
 s.min-=d.cost;const b=mkBld(side,type,cx,cy,false);
 drones.forEach((u,i)=>{if(i==0){if(queue)u.orders.push({t:'b',b});else u.orders=[{t:'b',b}]}});return b}
function updProjs(dt){
 for(const p of projs){const t=p.t;let tx=p.tx,tz=p.tz,ty=t.air?5:(t.isB?2:1);if(!t.dead){tx=t.x;tz=t.z;p.tx=tx;p.tz=tz}
  const dx=tx-p.x,dy=ty-p.y,dz=tz-p.z,d=Math.hypot(dx,dy,dz),st=p.sp*dt;
  if(d<=st+.3){p.done=1;if(!t.dead)hit(t,p.dmg,p.ty,p.spl,p.side);else if(p.spl)spark(tx,1,tz,'#fa4',6);if(p.spl){spark(tx,1,tz,'#fb5',14);beams.push({x:tx,z:tz,r:p.spl,life:.3,max:.3})}else spark(tx,ty,tz,'#ffd',3)}
  else{p.dx=dx/d;p.dy=dy/d;p.dz=dz/d;p.x+=p.dx*st;p.y+=p.dy*st;p.z+=p.dz*st}}
 projs=projs.filter(p=>!p.done);
 for(const q of parts){q.life-=dt;q.x+=q.vx*dt;q.y+=q.vy*dt;q.z+=q.vz*dt;q.vy-=14*dt;if(q.y<0){q.y=0;q.vy*=-.3}}
 parts=parts.filter(q=>q.life>0);
 for(const f of fts){f.life-=dt;f.y+=dt*2}fts=fts.filter(f=>f.life>0);
 for(const b of beams)b.life-=dt;beams=beams.filter(b=>b.life>0)}
let tVis=0,tSup=0,tAI=0;
function update(dt){
 T+=dt;rebuildHash();
 for(const d of deps)d.wk=0;for(const u of units)if(u.orders[0]&&u.orders[0].t==='g')u.orders[0].dep.wk++;
 for(const u of units)if(!u.dead)updUnit(u,dt);
 separate();
 for(const u of units)if(!u.dead&&u.air){u.y=5+Math.sin(T*3+u.id)*.3}
 for(const b of blds)if(!b.dead)updBld(b,dt);
 updProjs(dt);
 units=units.filter(u=>!u.dead);blds=blds.filter(b=>!b.dead);
 if((tVis-=dt)<=0){tVis=.2;updVision()}
 if((tSup-=dt)<=0){tSup=.25;calcSupply()}
 if((tAI-=dt)<=0){tAI=1;aiTick()}
 if(T>4){const a0=blds.some(b=>b.side===0&&b.type==='hq'),a1=blds.some(b=>b.side===1&&b.type==='hq');if(!a1)endGame(true);else if(!a0)endGame(false)}
}
/* ---------- AI ---------- */
function aiBuild(type,near){
 const hqs=blds.filter(b=>b.side===1&&b.type==='hq'&&!b.dead);if(!hqs.length)return false;
 const d=BT[type];if(S[1].min<d.cost||(d.req&&!hasB(1,d.req)))return false;
 const ds=units.filter(u=>u.side===1&&u.type==='drone'&&u.orders[0]&&u.orders[0].t!=='b'&&!u.carry);if(!ds.length)return false;
 const hq=near||hqs[Math.random()*hqs.length|0];
 for(let a=0;a<50;a++){const ang=Math.random()*6.28,r=(hq.sz/2+3+Math.random()*9)*1;const cx=Math.round(hq.cx+hq.sz/2+Math.cos(ang)*r-d.sz/2),cy=Math.round(hq.cy+hq.sz/2+Math.sin(ang)*r-d.sz/2);
  if(canPlace(1,type,cx,cy,true)){let bu=ds[0],bd=1e9;for(const u of ds){const dd=Math.hypot(u.x-hq.x,u.z-hq.z);if(dd<bd){bd=dd;bu=u}}
   const b=placeBld(1,type,cx,cy,[bu],false);return typeof b==='object'}}
 return false}
function aiTick(){
 const s=S[1];const my=units.filter(u=>u.side===1),drones=my.filter(u=>u.type==='drone'),army=my.filter(u=>u.type!=='drone');
 const bl=blds.filter(b=>b.side===1),hqs=bl.filter(b=>b.type==='hq'),cnt=t=>bl.filter(b=>b.type===t).length;
 const busy=bl.filter(b=>!b.built).length;
 for(const h of hqs){if(h.built&&h.queue.length<1&&drones.length+h.queue.length<Math.min(45,11*hqs.length+2))enqueueUnit(h,'drone')}
 // idle drones mine
 for(const d of drones)if(!d.orders.length){const nd=nearestDep(d.x,d.z,60,1);if(nd)d.orders=[{t:'g',dep:nd}]}
 calcSupply();
 const dep=bl.filter(b=>b.type==='depot'&&!b.built).length;
 if(s.cap-s.used<4+army.length*.15+hqs.length*2&&dep<(T>500?2:1)&&s.min>=100)aiBuild('depot');
 else{
  const nb=cnt('barracks'),want=Math.min(2+hqs.length*2,1+(T/200|0));
  if(nb<1&&T>25)aiBuild('barracks');
  else if(!cnt('lab')&&T>120&&nb)aiBuild('lab');
  else if(!cnt('airfield')&&T>200&&nb&&s.min>250)aiBuild('airfield');
  else if(nb<want&&s.min>250)aiBuild('barracks');
  else if(cnt('airfield')<1+(hqs.length>1)+(T>900)&&T>400&&s.min>350)aiBuild('airfield');
  else if(cnt('turret')<hqs.length*2&&T>260&&s.min>300){const h=hqs[Math.random()*hqs.length|0];aiBuild('turret',h)}
  else if(s.min>650&&busy<2&&cnt('depot')<60)aiBuild('depot');
 }
 // expansion
 if(hqs.length<4&&s.min>=430&&T-ai.lastExp>50&&drones.length>=9&&hqs.every(h=>h.built)&&T>150){
  let best=null,bs=1e9;for(const st of sites){if(!expl[1][st.cy*N+st.cx])continue;if(blds.some(b=>b.type==='hq'&&!b.dead&&Math.hypot(b.x-st.x,b.z-st.z)<30))continue;
   if(!deps.some(d=>d.site===st.i&&d.amt>0))continue;
   if(mem[1]&&[...mem[1].values()].some(m=>Math.hypot(m.x-st.x,m.z-st.z)<25))continue;
   const dd=Math.hypot(st.x-sites[1].x,st.z-sites[1].z);if(dd<bs){bs=dd;best=st}}
  if(best){const ds=drones.filter(u=>u.orders[0]&&u.orders[0].t!=='b');if(ds.length){ai.lastExp=T;const u=ds[0];
    const r=placeBld(1,'hq',best.cx-2,best.cy-2,[u],false);if(typeof r==='string')ai.lastExp=T-35}}}
 // production
 for(const b of bl){if(!b.built||!b.d.prod||b.type==='hq')continue;
  if(!b.rally){const h=hqs[0];if(h){const dx=WD/2-h.x,dz=WD/2-h.z,l=Math.hypot(dx,dz);const p=nearestWalk(h.x+dx/l*18,h.z+dz/l*18);b.rally={x:p[0],z:p[1]}}}
  if(b.queue.length>=2)continue;
  if(b.type==='barracks'){const c=ai.hist,n=c.length;let t;const air=[...mem[1]].length&&units.some(u=>u.side===0&&u.air&&seen(1,u));
   const r=Math.random();t=air&&r<.6?'rifle':(hasB(1,'lab')&&r<.35)?'tank':r<.7?'rifle':'lancer';
   if(enqueueUnit(b,t)===null){c.push(t);if(c.length>20)c.shift()}}
  else if(b.type==='airfield')enqueueUnit(b,'wasp')}
 for(const b of bl){if(b.type==='lab'&&b.built&&!b.queue.length){for(const k of['w','a','l'])if(S[1].up[k]<UP[k].max&&s.min>UP[k].cost[S[1].up[k]]+150){if(enqueueRes(b,k)===null)break}}}
 // scouting
 if(T>30&&(!ai.scout||ai.scout.dead||!ai.scout.orders.length)){
  let sc=ai.scout&&!ai.scout.dead?ai.scout:null;if(!sc){sc=army.find(u=>u.type==='lancer')||army[0]||(T>40&&drones.length>7?drones[drones.length-1]:null)}
  if(sc){ai.scout=sc;const un=sites.filter(st=>!expl[1][st.cy*N+st.cx]).sort((a,b)=>Math.hypot(a.x-sc.x,a.z-sc.z)-Math.hypot(b.x-sc.x,b.z-sc.z));
   const list=un.length?un:sites.filter(st=>Math.random()<.5);sc.orders=list.slice(0,3).map(st=>({t:'m',x:st.x,z:st.z,arr:4}))}}
 // defense
 const fighters=army.filter(u=>u!==ai.scout);
 if(ai.alert&&T-ai.alert.t<10&&!ai.attacking){for(const u of fighters)if(!u.orders.length||u.orders[0].t==='m')u.orders=[{t:'a',x:ai.alert.x,z:ai.alert.z}]}
 // attack
 const asup=fighters.reduce((a,u)=>a+u.d.sup,0);
 if(!ai.attacking&&T>200+ai.wave*20&&asup>=ai.thr){ai.attacking=true;ai.wave++;ai.thr=Math.min(400,Math.round(ai.thr*1.45+4));ai.target=null;for(const u of fighters)u.orders=[]}
 if(ai.attacking){
  if(!ai.target||T-ai.target.t>40||ai.target.done){ai.target=aiTarget(fighters)}
  let act=0;for(const u of fighters){if(!u.orders.length){if(Math.hypot(u.x-ai.target.x,u.z-ai.target.z)<10){ai.target.done=1}u.orders=[{t:'a',x:ai.target.x,z:ai.target.z}]}else act++}
  if(fighters.length<Math.max(2,ai.thr*.08)){ai.attacking=false;for(const u of fighters)if(u.orders.length)u.orders=[{t:'m',x:hqs[0]?hqs[0].x:u.x,z:hqs[0]?hqs[0].z+6:u.z,arr:3}]}
 }else{ // gather idle at rally
  for(const u of fighters)if(!u.orders.length&&hqs[0]){const b=bl.find(q=>q.rally);if(b&&Math.hypot(u.x-b.rally.x,u.z-b.rally.z)>12)u.orders=[{t:'m',x:b.rally.x+(Math.random()-.5)*6,z:b.rally.z+(Math.random()-.5)*6,arr:3}]}}
}
function aiTarget(f){
 let cx=0,cz=0;for(const u of f){cx+=u.x;cz+=u.z}cx/=f.length||1;cz/=f.length||1;
 let best=null,bs=1e9;for(const m of mem[1].values()){if(m.b.dead&&vis[1][cidx(m.x,m.z)]===2)continue;const w=(m.type==='turret'?60:0)+Math.hypot(m.x-cx,m.z-cz)-(m.type==='hq'?0:0);if(w<bs){bs=w;best=m}}
 if(best)return{x:best.x,z:best.z,t:T};
 const un=sites.filter(st=>!expl[1][st.cy*N+st.cx]).sort((a,b)=>Math.hypot(a.x-cx,a.z-cz)-Math.hypot(b.x-cx,b.z-cz));
 if(un.length)return{x:un[0].x,z:un[0].z,t:T};
 const st=sites[Math.random()*sites.length|0];return{x:st.x,z:st.z,t:T}}
/* ---------- audio ---------- */
let AC=null,muted=false;
function snd(f,d,type,v){if(muted)return;try{if(!AC)AC=new(window.AudioContext||window.webkitAudioContext)();const o=AC.createOscillator(),g2=AC.createGain();o.type=type||'sine';o.frequency.value=f;g2.gain.value=v||.05;g2.gain.exponentialRampToValueAtTime(.0001,AC.currentTime+d);o.connect(g2);g2.connect(AC.destination);o.start();o.stop(AC.currentTime+d)}catch(e){}}
function toast(t,c){const m=$('msgs'),d=document.createElement('div');d.textContent=t;if(c)d.className=c;m.appendChild(d);while(m.children.length>5)m.firstChild.remove();setTimeout(()=>d.remove(),4500)}
/* ---------- camera / projection ---------- */
const PITCH=.92;let ex,ey,ez,fx,fy,fz,rx,rz,ux,uy,uz,FOC,CX,CY;
function camSetup(){const sy=Math.sin(cam.yaw),cy=Math.cos(cam.yaw),sp=Math.sin(PITCH),cp=Math.cos(PITCH);
 ex=cam.x+sy*cp*cam.dist;ey=sp*cam.dist;ez=cam.z+cy*cp*cam.dist;fx=-sy*cp;fy=-sp;fz=-cy*cp;rx=cy;rz=-sy;ux=-sy*sp;uy=cp;uz=-cy*sp;
 FOC=VH*1.05;CX=VW/2;CY=(34+VH-196)/2}
let PX=0,PY=0,PD=0;
function P(x,y,z){const dx=x-ex,dy=y-ey,dz=z-ez;PD=dx*fx+dy*fy+dz*fz;const k=FOC/Math.max(PD,.5);PX=CX+(dx*rx+dz*rz)*k;PY=CY-(dx*ux+dy*uy+dz*uz)*k;return k}
function unproj(sx,sy){const a=(sx-CX)/FOC,b=(sy-CY)/FOC;const dx=fx+a*rx-b*ux,dy=fy-b*uy,dz=fz+a*rz-b*uz;if(dy>=-1e-4)return null;const t=-ey/dy;return[ex+dx*t,ez+dz*t]}
/* ---------- render ---------- */
const TONES=[];for(let i=0;i<8;i++){const v=i*4;TONES.push(['rgb('+(78+v)+','+(88+v)+','+(62+v/2)+')','rgb('+((78+v)*.42|0)+','+((88+v)*.45|0)+','+((62+v/2)*.5|0)+')'])}
let tone=null;function initTone(){tone=new Uint8Array(N*N);const r=mul(99);for(let i=0;i<N*N;i++)tone[i]=(r()*r()*8)|0}
const VXs=new Float32Array((N+1)*(N+1)),VYs=new Float32Array((N+1)*(N+1)),VDs=new Float32Array((N+1)*(N+1));
const TC=['#3af','#f54'];
function shade(hex,f){if(hex[0]!=='#')return hex;const n=parseInt(hex.slice(1),16);let r=n>>16,g=(n>>8)&255,b=n&255;if(hex.length===4){r=(n>>8&15)*17;g=(n>>4&15)*17;b=(n&15)*17}return'rgb('+(r*f|0)+','+(g*f|0)+','+(b*f|0)+')'}
const bx=[0,0,0,0,0,0,0,0],by=[0,0,0,0,0,0,0,0];
function box(cx,cz,w,d,h,y0,col,alpha){
 const hw=w/2,hd=d/2;const X=[cx-hw,cx+hw,cx+hw,cx-hw],Z=[cz-hd,cz-hd,cz+hd,cz+hd];
 for(let i=0;i<4;i++){P(X[i],y0,Z[i]);bx[i]=PX;by[i]=PY;P(X[i],y0+h,Z[i]);bx[i+4]=PX;by[i+4]=PY}
 if(alpha!==undefined)g.globalAlpha=alpha;
 for(let i=0;i<4;i++){const j=(i+1)&3;const nx=(i===0?0:i===1?1:i===2?0:-1),nz=(i===0?-1:i===1?0:i===2?1:0);
  if(nx*(ex-cx)+nz*(ez-cz)<=0)continue;g.fillStyle=shade(col,.55+.25*(nx*.6+nz*.8+1)/2*1.2);
  g.beginPath();g.moveTo(bx[i],by[i]);g.lineTo(bx[j],by[j]);g.lineTo(bx[j+4],by[j+4]);g.lineTo(bx[i+4],by[i+4]);g.fill()}
 g.fillStyle=shade(col,1.05);g.beginPath();g.moveTo(bx[4],by[4]);g.lineTo(bx[5],by[5]);g.lineTo(bx[6],by[6]);g.lineTo(bx[7],by[7]);g.fill();
 if(alpha!==undefined)g.globalAlpha=1}
function ell(x,z,r,style,fill){const k=P(x,0,z);g.beginPath();g.ellipse(PX,PY,r*k,r*k*.75,0,0,6.3);if(fill){g.fillStyle=style;g.fill()}else{g.strokeStyle=style;g.stroke()}}
const BCOL={hq:'#8aa0b4',depot:'#b8a060',barracks:'#7a8a70',airfield:'#7d7f9a',lab:'#7ab0a8',turret:'#9a8080'};
function drawBld(b,ghost,m){
 const type=ghost?m.type:b.type,sz=ghost?m.sz:b.sz,side=ghost?m.side:b.side,x=ghost?m.x:b.x,z=ghost?m.z:b.z;
 const w=sz*CS*.9,tc=ghost?'#844':TC[side];let col=ghost?'#556':BCOL[type];
 const prog=ghost?m.prog:(b.built?1:b.prog);const A=ghost?.7:1;
 if(ghost)g.globalAlpha=.75;
 const hh=(h)=>h*(ghost?1:(.25+.75*prog));
 ell(x,z,w*.62,'rgba(0,0,0,.35)',1);
 if(type==='hq'){box(x,z,w,w,hh(5),0,col);box(x,z,w*.6,w*.6,hh(2.5),hh(5),shade(col,1.15));box(x,z,w*.2,w*.2,hh(3),hh(7.5),tc);
  for(const[a,c]of[[-1,-1],[1,-1],[1,1],[-1,1]])box(x+a*w*.42,z+c*w*.42,1.4,1.4,hh(6),0,tc)}
 else if(type==='depot'){box(x,z,w,w,hh(2.2),0,col);box(x,z,w*.5,w*.5,hh(1),hh(2.2),tc)}
 else if(type==='barracks'){box(x,z,w,w*.8,hh(3.2),0,col);box(x,z-w*.15,w*.9,w*.15,hh(1),hh(3.2),tc);box(x+w*.3,z+w*.3,w*.2,w*.2,hh(4.5),0,shade(col,.8))}
 else if(type==='airfield'){box(x,z,w,w,hh(1),0,'#666');box(x-w*.2,z,w*.4,w*.8,hh(4),hh(1),col);box(x+w*.25,z,w*.3,w*.6,hh(2),hh(1),tc)}
 else if(type==='lab'){box(x,z,w*.9,w*.9,hh(3),0,col);box(x,z,w*.5,w*.5,hh(2.5),hh(3),shade(col,1.2));box(x,z,w*.15,w*.15,hh(2),hh(5.5),tc)}
 else if(type==='turret'){box(x,z,w,w,hh(1.2),0,'#665');box(x,z,w*.45,w*.45,hh(3),hh(1.2),tc);
  if(!ghost&&b.built){const a=b.face||0;P(x,4.2,z);const x0=PX,y0=PY;P(x+Math.sin(a)*3,4.2,z+Math.cos(a)*3);g.strokeStyle='#ddd';g.lineWidth=3;g.beginPath();g.moveTo(x0,y0);g.lineTo(PX,PY);g.stroke()}}
 if(ghost)g.globalAlpha=1;
 if(!ghost&&!b.built){P(x,hh(5)+1,z);g.fillStyle='#000a';g.fillRect(PX-20,PY-5,40,5);g.fillStyle='#fc4';g.fillRect(PX-19,PY-4,38*b.prog,3)}
 if(!ghost&&b.hurt>0){P(x,3,z);g.fillStyle='rgba(255,120,60,.5)';g.beginPath();g.arc(PX,PY,w*FOC/PD*.4,0,6.3);g.fill()}
}
function drawUnit(u){
 const k=P(u.x,u.y,u.z);const sx=PX,sy=PY,c=TC[u.side],r=u.rad*k;
 if(sx<-40||sx>VW+40||sy<-40||sy>VH+40)return;
 const sel_=sel.includes(u);
 if(u.air){P(u.x,0,u.z);g.fillStyle='rgba(0,0,0,.3)';g.beginPath();g.ellipse(PX,PY,r,r*.6,0,0,6.3);g.fill();g.strokeStyle='rgba(255,255,255,.18)';g.lineWidth=1;g.beginPath();g.moveTo(PX,PY);g.lineTo(sx,sy);g.stroke()}
 else{g.fillStyle='rgba(0,0,0,.3)';g.beginPath();g.ellipse(sx,sy,r*1.05,r*.7,0,0,6.3);g.fill()}
 const gy=u.air?sy:sy;const fx_=Math.sin(u.face-cam.yaw*0)*1,fz_=Math.cos(u.face);
 // screen-facing direction of unit
 const fdx=(fx_*rx+fz_*rz),fdy=-(fx_*ux+fz_*uz);const fl=Math.hypot(fdx,fdy)||1;const dxs=fdx/fl,dys=fdy/fl*.8;
 g.lineWidth=Math.max(1,r*.35);
 switch(u.type){
  case 'drone':g.fillStyle='#cdb';g.beginPath();g.arc(sx,sy-r*.8,r*.9,0,6.3);g.fill();g.fillStyle=c;g.fillRect(sx-r*.7,sy-r*.5,r*1.4,r*.8);if(u.carry){g.fillStyle='#5ff';g.fillRect(sx-r*.35,sy-r*1.9,r*.7,r*.7)}break;
  case 'rifle':g.fillStyle=c;g.beginPath();g.arc(sx,sy-r*.9,r*.95,0,6.3);g.fill();g.fillStyle='#ddd';g.beginPath();g.arc(sx,sy-r*1.9,r*.55,0,6.3);g.fill();g.strokeStyle='#222';g.beginPath();g.moveTo(sx,sy-r);g.lineTo(sx+dxs*r*1.8,sy-r+dys*r*1.8);g.stroke();break;
  case 'lancer':g.fillStyle=shade(c,.8);g.beginPath();g.moveTo(sx,sy-r*2.6);g.lineTo(sx+r*1.1,sy);g.lineTo(sx-r*1.1,sy);g.fill();g.fillStyle=c;g.beginPath();g.arc(sx,sy-r*1.5,r*.6,0,6.3);g.fill();g.strokeStyle='#eee';g.beginPath();g.moveTo(sx,sy-r);g.lineTo(sx+dxs*r*2.4,sy-r+dys*r*2.4);g.stroke();break;
  case 'tank':g.fillStyle='#333';g.fillRect(sx-r*1.1,sy-r*.5,r*2.2,r*.9);g.fillStyle=shade(c,.9);g.fillRect(sx-r*.9,sy-r*1.2,r*1.8,r*.9);g.fillStyle=c;g.beginPath();g.ellipse(sx,sy-r*1.3,r*.6,r*.45,0,0,6.3);g.fill();g.strokeStyle='#bbb';g.lineWidth=Math.max(2,r*.3);g.beginPath();g.moveTo(sx,sy-r*1.3);g.lineTo(sx+dxs*r*1.9,sy-r*1.3+dys*r*1.9);g.stroke();break;
  case 'wasp':g.fillStyle=c;g.beginPath();g.moveTo(sx+dxs*r*1.4,sy+dys*r*1.4);g.lineTo(sx-dxs*r+dys*r*1.7,sy-dys*r-dxs*r*.9*.5);g.lineTo(sx-dxs*r*.4,sy-dys*r*.4);g.lineTo(sx-dxs*r-dys*r*1.7,sy-dys*r+dxs*r*.9*.5);g.fill();g.fillStyle='#fff';g.beginPath();g.arc(sx,sy,r*.3,0,6.3);g.fill();break}
 if(u.hurt>0){g.fillStyle='rgba(255,255,255,.5)';g.beginPath();g.arc(sx,sy-r,r,0,6.3);g.fill()}
 if(sel_){g.strokeStyle='#5f8';g.lineWidth=1.5;ell(u.x,u.z,u.rad*1.3,'#5f8',0)}
 if(sel_||u.hp<u.mhp){const w=Math.max(14,r*2.4),yy=sy-r*3-(u.air?0:0);g.fillStyle='#000a';g.fillRect(sx-w/2,yy,w,3.5);g.fillStyle=u.hp/u.mhp>.5?'#4d6':u.hp/u.mhp>.25?'#fc4':'#f44';g.fillRect(sx-w/2,yy,w*u.hp/u.mhp,3.5)}
}
function render(){
 camSetup();
 g.fillStyle='#000';g.fillRect(0,0,VW,VH);
 if(!tone)initTone();
 // vertex grid
 for(let j=0;j<=N;j++)for(let i=0;i<=N;i++){P(i*CS,0,j*CS);const k=j*(N+1)+i;VXs[k]=PX;VYs[k]=PY;VDs[k]=PD}
 const v0=vis[0],e0=expl[0],items=[];
 for(let j=0;j<N;j++)for(let i=0;i<N;i++){const c=j*N+i;if(!e0[c])continue;
  const a=j*(N+1)+i,b=a+1,d=a+N+1,e=d+1;
  if(VDs[a]<1||VDs[e]<1||VDs[b]<1||VDs[d]<1)continue;
  const minx=Math.min(VXs[a],VXs[b],VXs[d],VXs[e]),maxx=Math.max(VXs[a],VXs[b],VXs[d],VXs[e]);if(maxx<0||minx>VW)continue;
  const miny=Math.min(VYs[a],VYs[b],VYs[d],VYs[e]),maxy=Math.max(VYs[a],VYs[b],VYs[d],VYs[e]);if(maxy<0||miny>VH)continue;
  g.fillStyle=TONES[tone[c]][v0[c]===2?0:1];g.strokeStyle=g.fillStyle;g.lineWidth=1;
  g.beginPath();g.moveTo(VXs[a],VYs[a]);g.lineTo(VXs[b],VYs[b]);g.lineTo(VXs[e],VYs[e]);g.lineTo(VXs[d],VYs[d]);g.closePath();g.fill();g.stroke();
  if(rock[c])items.push([VDs[a]+VDs[e],0,c])}
 // soft fog edge: unexplored is black already
 for(const d of deps){if(!e0[d.cy*N+d.cx]||d.amt<=0)continue;P(d.x,0,d.z);items.push([PD,1,d])}
 for(const b of blds){if(b.side===0||seen(0,b)){P(b.x,0,b.z);items.push([PD,2,b])}}
 for(const m of mem[0].values()){if(m.b.dead&&!e0[cidx(m.x,m.z)])continue;if(!seen(0,m.b)||m.b.dead){P(m.x,0,m.z);items.push([PD,3,m])}}
 for(const u of units){if(u.side===0||seen(0,u)){P(u.x,0,u.z);items.push([PD,4,u])}}
 items.sort((a,b)=>b[0]-a[0]);
 for(const it of items){const o=it[2];
  switch(it[1]){
   case 0:{const c=o,cx=(c%N+.5)*CS,cz=((c/N|0)+.5)*CS;const vv=v0[c]===2;box(cx,cz,CS,CS,3.2+(c*7%5)*.3,0,vv?'#7a7468':'#3e3b38');break}
   case 1:{const k=P(o.x,0,o.z),f=v0[o.cy*N+o.cx]===2;const s=.6+.5*o.amt/o.max;g.fillStyle=f?'#3ee':'#1a6a6e';g.beginPath();g.moveTo(PX,PY-k*3*s);g.lineTo(PX+k*.9*s,PY-k*.6);g.lineTo(PX,PY+k*.2);g.lineTo(PX-k*.9*s,PY-k*.6);g.fill();g.fillStyle=f?'#aff':'#3a9a9e';g.beginPath();g.moveTo(PX,PY-k*3*s);g.lineTo(PX+k*.9*s,PY-k*.6);g.lineTo(PX+k*.1,PY-k*.5);g.fill();break}
   case 2:drawBld(o,false);if(sel.includes(o)){g.lineWidth=2;const R=o.rad*1.1;ell(o.x,o.z,R*1.3,'#5f8',0);}
     if(sel.includes(o)||o.hp<o.mhp){P(o.x,0,o.z);const w=o.sz*10,yy=PY-o.sz*FOC/PD*1.9-8;g.fillStyle='#000a';g.fillRect(PX-w/2,yy,w,4);g.fillStyle='#4d6';g.fillRect(PX-w/2,yy,w*o.hp/o.mhp,4)}
     if(o.side===0&&o.rally&&sel.includes(o)){}break;
   case 3:drawBld(null,true,o);break;
   case 4:drawUnit(o);break}}
 // projectiles/particles
 for(const p of projs){const k=P(p.x,p.y,p.z);if(!(vis[0][cidx(p.x,p.z)]===2))continue;const x=PX,y=PY;P(p.x-p.dx*2,p.y-p.dy*2,p.z-p.dz*2);g.strokeStyle=p.side?'#fa5':'#8ef';g.lineWidth=p.big?4:2;g.beginPath();g.moveTo(PX,PY);g.lineTo(x,y);g.stroke()}
 for(const b of beams){P(b.x,0,b.z);const k=FOC/PD;g.strokeStyle='rgba(255,200,100,'+b.life/b.max+')';g.lineWidth=3;g.beginPath();g.ellipse(PX,PY,b.r*k*(1.2-b.life/b.max),b.r*k*.75*(1.2-b.life/b.max),0,0,6.3);g.stroke()}
 for(const q of parts){if(vis[0][cidx(q.x,q.z)]!==2)continue;P(q.x,q.y,q.z);g.globalAlpha=Math.min(1,q.life*2);g.fillStyle=q.col;const s=q.sz*FOC/PD*.25;g.fillRect(PX-s,PY-s,s*2,s*2)}
 g.globalAlpha=1;
 g.font='bold 14px sans-serif';g.textAlign='center';for(const f of fts){P(f.x,f.y,f.z);g.fillStyle='rgba(120,255,255,'+Math.min(1,f.life)+')';g.fillText(f.txt,PX,PY)}
 // orders indicator for selection
 if(sel.length&&sel[0].isU){g.lineWidth=1;let n=0;for(const u of sel){if(n++>40)break;if(u.side!==0)continue;let px=u.x,pz=u.z;for(const o of u.orders){let tx,tz;if(o.t==='m'||o.t==='a'){tx=o.x;tz=o.z}else if(o.t==='b'){tx=o.b.x;tz=o.b.z}else continue;P(px,.2,pz);const x0=PX,y0=PY;P(tx,.2,tz);g.strokeStyle=o.t==='a'?'rgba(255,90,90,.6)':o.t==='b'?'rgba(255,220,80,.6)':'rgba(90,255,140,.5)';g.beginPath();g.moveTo(x0,y0);g.lineTo(PX,PY);g.stroke();g.fillStyle=g.strokeStyle;g.fillRect(PX-2,PY-2,4,4);px=tx;pz=tz}}}
 if(sel.length===1&&sel[0].isB&&sel[0].rally&&sel[0].side===0){const b=sel[0];P(b.x,0,b.z);const x0=PX,y0=PY;P(b.rally.x,0,b.rally.z);g.strokeStyle='#ff8';g.setLineDash([4,4]);g.beginPath();g.moveTo(x0,y0);g.lineTo(PX,PY);g.stroke();g.setLineDash([]);g.fillStyle='#ff8';g.fillRect(PX-3,PY-8,2,10);g.fillRect(PX-1,PY-8,8,5)}
 if(cmdFx.length){for(const c of cmdFx){c.l-=1/60;P(c.x,0,c.z);const k=FOC/PD;g.strokeStyle=c.c;g.globalAlpha=Math.max(0,c.l*2);g.lineWidth=2;g.beginPath();g.ellipse(PX,PY,(1.5-c.l)*3*k,(1.5-c.l)*2.2*k,0,0,6.3);g.stroke();g.globalAlpha=1}cmdFx=cmdFx.filter(c=>c.l>0)}
 // placement preview
 if(placing){const p=unproj(mouse.x,mouse.y);if(p){const d=BT[placing],cx=Math.round(p[0]/CS-d.sz/2),cy=Math.round(p[1]/CS-d.sz/2);placing_c=[cx,cy];
   const ok=canPlace(0,placing,cx,cy,false);const x=(cx+d.sz/2)*CS,z=(cy+d.sz/2)*CS;
   for(let j=0;j<d.sz;j++)for(let i=0;i<d.sz;i++){const a=(cy+j)*(N+1)+cx+i;if(cx+i<N&&cy+j<N){g.fillStyle=ok?'rgba(60,255,100,.35)':'rgba(255,60,60,.4)';g.beginPath();g.moveTo(VXs[a],VYs[a]);g.lineTo(VXs[a+1],VYs[a+1]);g.lineTo(VXs[a+N+2],VYs[a+N+2]);g.lineTo(VXs[a+N+1],VYs[a+N+1]);g.fill()}}
   g.globalAlpha=.55;box(x,z,d.sz*CS*.9,d.sz*CS*.9,4,0,ok?'#6f8':'#f66');g.globalAlpha=1}}
 if(drag){g.strokeStyle='#5f8';g.lineWidth=1;g.fillStyle='rgba(80,255,130,.1)';const x=Math.min(drag.x0,mouse.x),y=Math.min(drag.y0,mouse.y),w=Math.abs(mouse.x-drag.x0),h=Math.abs(mouse.y-drag.y0);g.fillRect(x,y,w,h);g.strokeRect(x,y,w,h)}
 if(cmdMode){g.fillStyle='#ff8';g.font='14px sans-serif';g.textAlign='left';g.fillText(cmdMode==='a'?'ATTACK-MOVE: click target (Esc cancel)':'',mouse.x+14,mouse.y+18)}
 drawMinimap();
}
let cmdFx=[],placing=null,placing_c=null,cmdMode=null,drag=null;const mouse={x:0,y:0};
const mm=$('mm'),mg=mm.getContext('2d'),mimg=mg.createImageData(N,N),mcv=document.createElement('canvas');mcv.width=mcv.height=N;const mcg=mcv.getContext('2d');
function drawMinimap(){
 const d=mimg.data;
 for(let j=0;j<N;j++)for(let i=0;i<N;i++){const c=j*N+i;let r=0,gg=0,b=0;
  if(expl[0][c]){const v=vis[0][c]===2;if(rock[c]){r=95;gg=90;b=82}else{r=48;gg=64;b=44}if(!v){r*=.55;gg*=.55;b*=.55}}
  const o=c*4;d[o]=r;d[o+1]=gg;d[o+2]=b;d[o+3]=255}
 for(const dp of deps)if(dp.amt>0&&expl[0][dp.cy*N+dp.cx]){const o=(dp.cy*N+dp.cx)*4;d[o]=60;d[o+1]=240;d[o+2]=240}
 mcg.putImageData(mimg,0,0);mg.imageSmoothingEnabled=false;mg.drawImage(mcv,0,0,184,184);
 const s=184/WD;
 for(const m of mem[0].values()){if(m.b.dead&&!expl[0][cidx(m.x,m.z)])continue;if(seen(0,m.b)&&!m.b.dead)continue;mg.fillStyle='#844';const z=m.sz*CS*s;mg.fillRect(m.x*s-z/2,m.z*s-z/2,z,z)}
 for(const b of blds){if(b.side===0||seen(0,b)){mg.fillStyle=b.side?'#f44':'#4af';const z=Math.max(3,b.sz*CS*s);mg.fillRect(b.x*s-z/2,b.z*s-z/2,z,z)}}
 for(const u of units){if(u.side===0||seen(0,u)){mg.fillStyle=u.side?'#f66':'#7cf';mg.fillRect(u.x*s-1,u.z*s-1,2,2)}}
 mg.strokeStyle='#fff';mg.lineWidth=1;mg.beginPath();let first=1;for(const[a,b]of[[0,34],[VW,34],[VW,VH-196],[0,VH-196]]){const p=unproj(a,b);const q=p?p:[cam.x,cam.z];const X=cl(q[0]*s,0,184),Y=cl(q[1]*s,0,184);if(first){mg.moveTo(X,Y);first=0}else mg.lineTo(X,Y)}mg.closePath();mg.stroke();
}
/* ---------- input ---------- */
const keys={};let inCanvas=false,lastClick=0,lastClickType=null;
function pickUnit(sx,sy){let best=null,bd=1e9;for(const u of units){if(u.side!==0&&!seen(0,u))continue;const k=P(u.x,u.y+.6,u.z);const r=Math.max(9,u.rad*k*1.4);const d=Math.hypot(PX-sx,PY-sy);if(d<r&&d<bd){bd=d;best=u}}return best}
function pickBld(sx,sy){const p=unproj(sx,sy);if(!p)return null;for(const b of blds){if(b.side!==0&&!seen(0,b))continue;const h=b.sz*CS/2;if(Math.abs(p[0]-b.x)<h&&Math.abs(p[1]-b.z)<h+b.sz*.5)return b}
 // ghost
 for(const m of mem[0].values()){if(!m.b.dead&&seen(0,m.b))continue;const h=m.sz*CS/2;if(Math.abs(p[0]-m.x)<h&&Math.abs(p[1]-m.z)<h)return{ghost:1,x:m.x,z:m.z,isB:1,side:1}}return null}
function pickDep(sx,sy){const p=unproj(sx,sy);if(!p)return null;let best=null,bd=3;for(const d of deps){if(d.amt<=0||!expl[0][d.cy*N+d.cx])continue;const dd=Math.hypot(d.x-p[0],d.z-p[1]);if(dd<bd){bd=dd;best=d}}return best}
function setSel(a){sel=a.slice(0,2000);refreshUI(true)}
function myUnits(){return sel.filter(u=>u.isU&&u.side===0)}
function issue(o,shift){const us=myUnits();us.forEach(u=>{if(shift)u.orders.push(o);else u.orders=[o]})}
function spreadOrder(wx,wz,t,shift){
 const us=myUnits();if(!us.length)return;const n=us.length;const cols=Math.ceil(Math.sqrt(n)),sp=1.9;
 const p=nearestWalk(wx,wz);wx=p[0];wz=p[1];
 us.forEach((u,i)=>{const gx=(i%cols)-(cols-1)/2,gz=(i/cols|0)-((n-1)/cols/2|0)/1;let x=wx+gx*sp,z=wz+gz*sp;if(!u.air&&!walkAt(x,z)){x=wx;z=wz}
  const o={t,x:cl(x,2,WD-2),z:cl(z,2,WD-2),arr:n>1?1.5+Math.sqrt(n)*.3:1};if(shift)u.orders.push(o);else u.orders=[o]});
 cmdFx.push({x:wx,z:wz,l:.6,c:t==='a'?'#f66':'#6f8'})}
function rightClick(sx,sy,shift){
 if(placing){placing=null;return}
 if(cmdMode){cmdMode=null;return}
 if(!sel.length)return;
 const p=unproj(sx,sy);if(!p)return;
 if(sel.length===1&&sel[0].isB&&sel[0].side===0){const b=sel[0];if(!b.d.prod)return;const dp=pickDep(sx,sy);b.rally=dp?{x:dp.x,z:dp.z,dep:dp}:{x:p[0],z:p[1]};cmdFx.push({x:b.rally.x,z:b.rally.z,l:.6,c:'#ff8'});snd(400,.05,'sine',.04);return}
 const us=myUnits();if(!us.length)return;
 const u=pickUnit(sx,sy),b=u?null:pickBld(sx,sy);const tg=u||b;
 if(tg&&tg.side===1&&!tg.ghost){const armed=us.filter(x=>x.w&&canHit(x.w,tg));if(armed.length){us.forEach(x=>{if(x.w&&canHit(x.w,tg)){const o={t:'k',tg};if(shift)x.orders.push(o);else x.orders=[o]}else{const o={t:'m',x:tg.x,z:tg.z,arr:2};if(shift)x.orders.push(o);else x.orders=[o]}});cmdFx.push({x:tg.x,z:tg.z,l:.6,c:'#f66'});snd(300,.06,'square',.04);return}}
 if(tg&&tg.ghost){spreadOrder(tg.x,tg.z,'a',shift);snd(300,.06,'square',.04);return}
 const dp=pickDep(sx,sy);
 if(dp){const dr=us.filter(x=>x.type==='drone');if(dr.length){dr.forEach(x=>{const o={t:'g',dep:dp};if(shift)x.orders.push(o);else x.orders=[o]});us.filter(x=>x.type!=='drone').forEach(x=>{x.orders=[{t:'m',x:dp.x,z:dp.z,arr:3}]});cmdFx.push({x:dp.x,z:dp.z,l:.6,c:'#5ff'});snd(500,.05,'sine',.04);return}}
 if(b&&b.side===0){const dr=us.filter(x=>x.type==='drone');
  if(!b.built&&dr.length){dr.forEach(x=>{const o={t:'b',b};if(shift)x.orders.push(o);else x.orders=[o]});return}
  if(b.type==='hq'&&dr.some(x=>x.carry)){dr.forEach(x=>{if(x.carry)x.orders=[{t:'r'}]});return}}
 spreadOrder(p[0],p[1],'m',shift);snd(500,.05,'sine',.04)}
function leftDown(sx,sy){
 if(placing){if(placing_c){const dr=myUnits().filter(u=>u.type==='drone');const r=placeBld(0,placing,placing_c[0],placing_c[1],dr.length?[nearestOf(dr,placing_c)]:[],keys.Shift);
   if(typeof r==='string'){toast(r,'bad');snd(150,.15,'square',.06)}else{snd(600,.1,'sine',.06);if(!keys.Shift)placing=null}}return}
 if(cmdMode==='a'){const p=unproj(sx,sy);if(p){spreadOrder(p[0],p[1],'a',keys.Shift)}cmdMode=null;return}
 drag={x0:sx,y0:sy}}
function nearestOf(dr,c){let b=dr[0],bd=1e9;for(const u of dr){const d=Math.hypot(u.x-c[0]*CS,u.z-c[1]*CS);if(d<bd&&u.orders[0]?.t!=='b'||!b){bd=d;b=u}}return b}
function leftUp(sx,sy){
 if(!drag)return;const d=drag;drag=null;const dx=Math.abs(sx-d.x0),dy=Math.abs(sy-d.y0);
 if(dx<5&&dy<5){const u=pickUnit(sx,sy);let now=performance.now();
  if(u){if(now-lastClick<350&&lastClickType===u.type&&u.side===0){const a=units.filter(q=>q.side===0&&q.type===u.type&&Math.hypot(P(q.x,0,q.z)*0+PX-CX,PY-CY)>-1&&PX>0&&PX<VW&&PY>34&&PY<VH-196);setSel(a)}
   else if(keys.Shift&&u.side===0){sel.includes(u)?setSel(sel.filter(q=>q!==u)):setSel(sel.concat(u.side===0&&sel.every(q=>q.isU)?[u]:[]))}else setSel([u]);lastClick=now;lastClickType=u.type;return}
  const b=pickBld(sx,sy);if(b&&!b.ghost){if(keys.Shift&&b.side===0&&sel.every(q=>q.isB)){sel.includes(b)?0:setSel(sel.concat(b))}else setSel([b]);return}
  if(!keys.Shift)setSel([]);return}
 const x0=Math.min(d.x0,sx),x1=Math.max(d.x0,sx),y0=Math.min(d.y0,sy),y1=Math.max(d.y0,sy);
 let a=[];for(const u of units){if(u.side!==0)continue;P(u.x,u.y+.5,u.z);if(PX>=x0&&PX<=x1&&PY>=y0&&PY<=y1)a.push(u)}
 if(!a.length)for(const b of blds){if(b.side!==0)continue;P(b.x,0,b.z);if(PX>=x0&&PX<=x1&&PY>=y0&&PY<=y1){a=[b];break}}
 if(keys.Shift&&sel.every(q=>q.isU)&&a.every(q=>q.isU))a=[...new Set(sel.concat(a))];
 setSel(a);if(a.length)snd(700,.04,'sine',.03)}
cv.addEventListener('mousedown',e=>{if(!gameOn||paused)return;mouse.x=e.clientX;mouse.y=e.clientY;camSetup();if(e.button===0)leftDown(e.clientX,e.clientY);else if(e.button===2)rightClick(e.clientX,e.clientY,e.shiftKey)});
addEventListener('mouseup',e=>{if(e.button===0&&gameOn&&!paused){camSetup();leftUp(e.clientX,e.clientY)}mmDrag=false});
addEventListener('mousemove',e=>{mouse.x=e.clientX;mouse.y=e.clientY});
cv.addEventListener('mouseenter',()=>inCanvas=true);cv.addEventListener('mouseleave',()=>inCanvas=false);
cv.addEventListener('contextmenu',e=>e.preventDefault());mm.addEventListener('contextmenu',e=>e.preventDefault());
cv.addEventListener('wheel',e=>{e.preventDefault();cam.dist=cl(cam.dist*(e.deltaY>0?1.1:.91),22,130)},{passive:false});
let mmDrag=false;
function mmPos(e){const r=mm.getBoundingClientRect();return[(e.clientX-r.left)/r.width*WD,(e.clientY-r.top)/r.height*WD]}
mm.addEventListener('mousedown',e=>{if(!gameOn||paused)return;const p=mmPos(e);
 if(e.button===2&&myUnits().length){if(cmdMode==='a'||e.ctrlKey)spreadOrder(p[0],p[1],'a',e.shiftKey);else spreadOrder(p[0],p[1],'m',e.shiftKey)}
 else if(e.button===0){if(cmdMode==='a'&&myUnits().length){spreadOrder(p[0],p[1],'a',e.shiftKey);cmdMode=null}else{mmDrag=true;cam.x=p[0];cam.z=p[1]}}});
mm.addEventListener('mousemove',e=>{if(mmDrag){const p=mmPos(e);cam.x=p[0];cam.z=p[1]}});
addEventListener('keydown',e=>{
 if(e.repeat&&!'ArrowLeftArrowRightArrowUpArrowDown'.includes(e.key)){}
 keys[e.key]=true;const k=e.key.toLowerCase();
 if(k==='f1'){e.preventDefault();$('help').classList.toggle('on');return}
 if(!gameOn)return;
 if(k==='p'){togglePause();return}
 if(paused)return;
 if(k==='escape'){placing=null;cmdMode=null;return}
 if(k===','){cam.yaw-=.15}if(k==='.'){cam.yaw+=.15}
 if(/^[1-9]$/.test(k)){if(e.ctrlKey||e.metaKey){e.preventDefault();groups[k]=sel.slice();toast('Group '+k+' set ('+sel.length+')','')}else if(groups[k]){const a=groups[k].filter(x=>!x.dead);groups[k]=a;if(a.length){if(Date.now()-(groups.t||0)<350&&groups.l===k){cam.x=a[0].x;cam.z=a[0].z}groups.t=Date.now();groups.l=k;setSel(a)}}return}
 if(k==='a'&&myUnits().length){cmdMode='a';placing=null}
 else if(k==='s'&&!e.ctrlKey){myUnits().forEach(u=>{u.orders=[];u.tgt=null})}
 else if(k==='h'){myUnits().forEach(u=>u.orders=[{t:'h'}])}
 else if(k==='tab'){e.preventDefault()}
 else if(k===' '){const a=alertPos||[sites[0].x,sites[0].z];cam.x=a[0];cam.z=a[1];e.preventDefault()}
 else if(k==='b'||k==='q'){}
});
addEventListener('keyup',e=>keys[e.key]=false);
addEventListener('blur',()=>{for(const k in keys)keys[k]=false});
/* ---------- UI ---------- */
const card=$('card'),info=$('info'),tip=$('tip');let uiSig='';
function btn(label,cost,fn,dis,tt,key){const d=document.createElement('div');d.className='cb'+(dis?' dis':'');d.innerHTML=(key?'<span class=k>'+key+'</span>':'')+'<b>'+label+'</b>'+(cost?'<span class=cst>'+cost+'</span>':'');
 d.onclick=e=>{e.stopPropagation();fn()};d.onmouseenter=()=>{tip.style.display='block';tip.innerHTML=tt};d.onmousemove=e=>{tip.style.left=Math.min(e.clientX+12,innerWidth-280)+'px';tip.style.top=(e.clientY-tip.offsetHeight-14)+'px'};d.onmouseleave=()=>tip.style.display='none';card.appendChild(d)}
function unitCmds(){
 btn('Attack-Move','',()=>{cmdMode='a';placing=null},false,'Attack-move: engage enemies on the way. Click ground.','A');
 btn('Stop','',()=>myUnits().forEach(u=>{u.orders=[];u.tgt=null}),false,'Stop all orders.','S');
 btn('Hold','',()=>myUnits().forEach(u=>u.orders=[{t:'h'}]),false,'Hold position; only fire at targets in range.','H')}
function refreshUI(force){
 sel=sel.filter(x=>!x.dead);
 const us=myUnits(),b1=sel.length===1&&sel[0].isB?sel[0]:null;
 let sig=(b1?b1.id+':'+b1.built+':'+b1.queue.length+':'+S[0].min/20|0:'u'+us.length+':'+(us.some(u=>u.type==='drone')?1:0)+':'+Math.floor(S[0].min/25))+':'+S[0].used+'/'+S[0].cap+':'+sel.length+(placing||'')+(cmdMode||'');
 if(b1)sig+=':'+b1.hp/20|0+':'+(b1.queue[0]?b1.queue[0].t|0:0)+':'+S[0].up.w+S[0].up.a+S[0].up.l+hasB(0,'lab');
 else sig+=':'+(sel.length?sel[0].hp/10|0:0)+':'+(sel.length>1?sel.length:'')+':'+(hasB(0,'lab')?1:0)+(hasB(0,'barracks')?1:0);
 if(sig===uiSig&&!force)return;uiSig=sig;
 card.innerHTML='';
 if(!sel.length){info.innerHTML='<div style="color:#8ab">Nothing selected.<br><br><b>Goal:</b> destroy all enemy HQs. Enemy base is unknown — scout! Select Drones (mine ore) or HQ (train Drones). Keep Supply Depots coming. <kbd>F1</kbd> help.</div>'; return}
 let h='';
 if(b1){const b=b1;h='<b style="font-size:16px;color:'+TC[b.side]+'">'+b.d.n+'</b> '+(b.side?'(enemy)':'')+'<br>'+b.d.desc+'<div class=bar><i style="width:'+(100*b.hp/b.mhp)+'%"></i></div>HP '+Math.ceil(b.hp)+' / '+b.mhp+(b.built?'':' — under construction '+(b.prog*100|0)+'%');
  if(b.side===0&&b.built){h+='<br>';b.queue.forEach((q,i)=>{h+='<div class=q data-i='+i+' title="Click to cancel (100% refund)">'+(q.k==='u'?UT[q.type].n:UP[q.type].n)+'<div class="bar pr"><i style="width:'+(i?0:100*q.t/q.tt)+'%"></i></div>'+(i==0&&b.blocked?'<span style=color:#f77>SUPPLY</span>':'✕')+'</div>'});
   if(b.d.prod)h+='<br><span style=color:#8ab>Right-click ground to set rally point.</span>'}
  if(!b.built&&b.side===0){h+='<br><span style=color:#fc8>A drone must build it (right-click it with a Drone). </span>';btn('Cancel',' 75% refund',()=>{S[0].min+=b.d.cost*.75|0;kill(b);toast('Construction cancelled (75% refund)','');setSel([])},false,'Cancel construction and refund 75%.')}
  if(b.side===0&&b.built){
   if(b.d.prod)for(const t of b.d.prod){const d=UT[t];const why=d.req&&!hasB(0,d.req)?'Requires '+BT[d.req].n:'';btn(d.n,d.cost+' ◆ · '+d.sup+'⚑',()=>{const r=enqueueUnit(b,t);if(r){toast(r,'bad');snd(150,.15,'square',.06)}else snd(450,.05,'sine',.04);refreshUI(true)},!!why,'<b>'+d.n+'</b><br>'+d.desc+'<br>HP '+d.hp+' · '+(d.w?d.w.d+' '+(d.w.t==='K'?'Kinetic':'Explosive')+' · range '+d.w.rng+' · '+(d.w.a&&d.w.g?'ground+air':d.w.a?'air':'ground only'):'no weapon')+' · '+d.arm.replace('L','Light').replace('H','Heavy')+' armor · '+d.time+'s'+(why?'<br><span style=color:#f88>'+why+'</span>':''))}
   if(b.d.res)for(const k of['w','a','l']){const u=UP[k],lv=S[0].up[k],done=lv>=u.max;btn(u.n+(done?' ✓':' '+(lv+1)),done?'':u.cost[lv]+' ◆',()=>{const r=enqueueRes(b,k);if(r){toast(r,'bad');snd(150,.15,'square',.06)}else snd(450,.05,'sine',.04);refreshUI(true)},done,'<b>'+u.n+'</b><br>'+u.desc+(done?'':'<br>Time '+u.time[lv]+'s'))}
   if(b.type==='hq'){}
   }
 }else if(us.length){
  const cnt={};us.forEach(u=>cnt[u.type]=(cnt[u.type]||0)+1);
  if(us.length===1){const u=us[0];h='<b style="font-size:16px;color:#7cf">'+u.d.n+'</b><br>'+u.d.desc+'<div class=bar><i style="width:'+(100*u.hp/u.mhp)+'%"></i></div>HP '+Math.ceil(u.hp)+'/'+u.mhp+' · '+(u.d.arm==='H'?'Heavy':'Light')+' armor'+(u.d.w?' · dmg '+u.d.w.d+' '+(u.d.w.t==='K'?'Kinetic':'Explosive')+' · range '+u.d.w.rng:'')}
  else{h='<b>'+us.length+' units selected</b> (click a type to filter)<br>';for(const t in cnt)h+='<span class=chip data-t='+t+'>'+UT[t].n+' ×'+cnt[t]+'</span>';
   if(us.length<=24){h+='<br>';us.forEach(u=>h+='<span style="display:inline-block;width:9px;height:9px;margin:1px;background:'+(u.hp/u.mhp>.5?'#4d6':u.hp/u.mhp>.25?'#fc4':'#f44')+'"></span>')}}
  unitCmds();
  if(us.some(u=>u.type==='drone')){for(const t of['depot','barracks','lab','airfield','turret','hq']){const d=BT[t];const why=d.req&&!hasB(0,d.req)?'Requires '+BT[d.req].n:S[0].min<d.cost?'Not enough ore':'';
    btn(d.n,d.cost+' ◆',()=>{if(why){toast(why,'bad');snd(150,.15,'square',.06);return}placing=t;cmdMode=null;toast('Click a valid spot to place (Esc/right-click cancel, Shift = repeat)','')},!!why,'<b>'+d.n+'</b><br>'+d.desc+'<br>Build time '+d.time+'s'+(why?'<br><span style=color:#f88>'+why+'</span>':''))}}
 }else{h='<b style="color:#f88">'+(sel[0].isB?BT[sel[0].type].n:UT[sel[0].type].n)+' (enemy)</b><div class=bar><i style="width:'+(100*sel[0].hp/sel[0].mhp)+'%"></i></div>'}
 info.innerHTML=h;
 info.querySelectorAll('.q').forEach(el=>el.onclick=()=>{cancelQ(b1,+el.dataset.i);refreshUI(true)});
 info.querySelectorAll('.chip').forEach(el=>el.onclick=()=>setSel(us.filter(u=>u.type===el.dataset.t)));
}
function hud(){
 $('rMin').textContent=Math.floor(S[0].min);const over=S[0].used>S[0].cap;$('rSup').textContent=S[0].used+' / '+S[0].cap;$('rSup').style.color=S[0].used>=S[0].cap?'#f66':'#5ff';
 $('rArmy').textContent=units.filter(u=>u.side===0&&u.type!=='drone').length;const m=T/60|0;$('rTime').textContent=m+':'+String(T%60|0).padStart(2,'0');
 refreshUI(false)}
/* ---------- game flow ---------- */
function endGame(win){if(!gameOn)return;gameOn=false;$('endT').textContent=win?'VICTORY':'DEFEAT';$('endT').style.color=win?'#7f9':'#f66';
 $('endS').innerHTML=(win?'The Syndicate HQ has fallen.':'Your last Headquarters was destroyed.')+'<br>Time '+$('rTime').textContent+' · Enemy kills '+kills[0]+' · Losses '+kills[1];$('end').classList.add('on')}
function togglePause(){if(!gameOn)return;paused=!paused;$('pause').classList.toggle('on',paused);$('bPause').textContent=paused?'Resume':'Pause'}
function start(){['title','end','pause','help'].forEach(i=>$(i).classList.remove('on'));newGame();placing=null;cmdMode=null;toast('Mine ore with Drones, train more, build Depots. Scout for the enemy!','')}
$('bStart').onclick=start;$('bRestart1').onclick=start;$('bRestart2').onclick=start;$('bResume').onclick=togglePause;$('bPause').onclick=togglePause;
$('bHelp').onclick=()=>$('help').classList.add('on');$('bHelpClose').onclick=()=>$('help').classList.remove('on');
$('bMute').onclick=()=>{muted=!muted;$('bMute').textContent='Sound: '+(muted?'Off':'On')};
$('bRotL').onclick=()=>cam.yaw-=.25;$('bRotR').onclick=()=>cam.yaw+=.25;
for(const el of document.querySelectorAll('#top,#bot'))el.addEventListener('mousedown',e=>e.stopPropagation());
let last=performance.now(),acc=0;
function frame(now){
 requestAnimationFrame(frame);let dt=Math.min(.1,(now-last)/1000);last=now;
 if(gameOn&&!paused){
  const sp=cam.dist*.9*dt,sy=Math.sin(cam.yaw),cy=Math.cos(cam.yaw);let mx=0,mz=0;
  if(keys.ArrowLeft)mx-=1;if(keys.ArrowRight)mx+=1;if(keys.ArrowUp)mz-=1;if(keys.ArrowDown)mz+=1;
  if(inCanvas&&!drag){if(mouse.x<8)mx-=1;if(mouse.x>VW-8)mx+=1;if(mouse.y<40)mz-=1;if(mouse.y>VH-204)mz+=1}
  cam.x+=(mx*cy+mz*sy)*sp*1.3;cam.z+=(-mx*sy+mz*cy)*sp*1.3;cam.x=cl(cam.x,0,WD);cam.z=cl(cam.z,0,WD);
  update(dt);hud()}
 if(gameOn||$('end').classList.contains('on'))render();else{g.fillStyle='#050a10';g.fillRect(0,0,VW,VH)}
}
newGame();requestAnimationFrame(frame);
window.__dbg=()=>({units:units.length,T,S});
</script></body></html>
<!-- agent-meta {"model":"anthropic/claude-sonnet-5.5","provider":"openrouter","persona":"full","sessionId":"5a392196-6956-4949-a1bf-771c99f4b94a","tokensIn":295182,"tokensOut":84191,"tokensTotal":379373,"cost":1.0526388000000002,"turns":7,"toolCalls":7,"failedToolCalls":0,"timestamp":"2026-09-29T21:18:01.240Z"} -->