RTS
GPT 5.6 Sol · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 39,085 bytes · SHA-256 ce1938e9e8d5
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>FRONTIER WAR</title>
<style>
:root{--cyan:#5ee7ff;--orange:#ff8552;--ink:#081016;--panel:#0b1720e8;--line:#345263;--text:#d8edf3;--muted:#7995a1}
*{box-sizing:border-box}html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#03080b;color:var(--text);font:13px/1.25 system-ui,Segoe UI,sans-serif;user-select:none}canvas{display:block}#game{position:fixed;inset:0;width:100%;height:100%;cursor:crosshair}.hud{position:fixed;z-index:4;pointer-events:none}.panel{background:linear-gradient(145deg,#0e2029ed,#081118ed);border:1px solid #395766;box-shadow:0 8px 28px #000b, inset 0 0 20px #16324144;backdrop-filter:blur(5px);pointer-events:auto}.cut{clip-path:polygon(0 0,calc(100% - 13px) 0,100% 13px,100% 100%,13px 100%,0 calc(100% - 13px))}
#top{left:50%;top:10px;transform:translateX(-50%);height:48px;display:flex;align-items:center;gap:20px;padding:0 20px;min-width:560px}.brand{font-weight:900;letter-spacing:3px;color:#fff}.stat{display:flex;align-items:center;gap:7px;font-weight:700}.gem{width:13px;height:13px;transform:rotate(45deg);background:#7af0d4;box-shadow:0 0 10px #4ff}.supply{color:#ffe491}.clock{color:var(--muted);margin-left:auto}button{font:inherit;color:var(--text);background:#102631;border:1px solid #426072;padding:7px 10px;cursor:pointer}button:hover{background:#194051;border-color:#6ec7df}button:disabled{opacity:.35;cursor:not-allowed}.iconBtn{width:32px;height:30px;padding:0}
#leftInfo{left:12px;bottom:12px;width:305px;min-height:158px;padding:12px 14px}.eyebrow{font-size:10px;color:var(--cyan);letter-spacing:2px;font-weight:800}.title{font-size:19px;font-weight:800;margin:3px 0}.sub{color:var(--muted);font-size:12px}.hpbar,.progress{height:7px;background:#1a2730;margin:9px 0;border:1px solid #2e4652}.hpbar i,.progress i{display:block;height:100%;background:linear-gradient(90deg,#4ed89c,#a8f28a);width:60%}.progress i{background:var(--cyan)}#selectionCards{display:flex;gap:4px;flex-wrap:wrap;max-height:50px;overflow:hidden}.card{width:29px;height:29px;border:1px solid #335261;background:#132a34;position:relative}.card b{font-size:9px;position:absolute;left:3px;bottom:2px}.card i{height:2px;background:#6effac;position:absolute;bottom:0;left:0}
#actions{left:325px;right:270px;bottom:12px;height:158px;padding:11px;display:flex;gap:9px}.action{width:98px;height:62px;text-align:left;position:relative;padding:7px}.action strong{display:block;font-size:12px}.action small{color:#86a1ac;font-size:10px}.hot{position:absolute;right:5px;top:4px;color:#ffdc7d;font-size:10px}.cost{position:absolute;bottom:4px;left:7px;color:#71ead0;font-size:10px}#queue{margin-left:auto;width:205px;border-left:1px solid #294450;padding-left:10px}.queueItem{font-size:11px;margin-bottom:6px}.queueItem .progress{margin:3px 0}
#miniWrap{right:12px;bottom:12px;width:246px;height:246px;padding:7px}#mini{width:230px;height:230px;background:#071017;cursor:pointer}#help{right:12px;top:70px;width:258px;padding:12px;display:none}.helpRow{margin:6px 0;color:#aac1ca}.key{display:inline-block;border:1px solid #4b6470;background:#17242b;padding:1px 5px;color:#fff;min-width:22px;text-align:center}.toast{position:fixed;z-index:9;left:50%;top:72px;transform:translateX(-50%);background:#07141de8;border:1px solid #55a2b9;padding:9px 18px;letter-spacing:.5px;opacity:0;transition:.2s;pointer-events:none}.toast.on{opacity:1}.banner{position:fixed;z-index:8;left:50%;top:80px;transform:translateX(-50%);font-size:12px;color:#ffd27b;letter-spacing:2px;text-shadow:0 2px 3px #000}.overlay{position:fixed;z-index:20;inset:0;background:radial-gradient(circle at 70% 30%,#173b4b 0,#071219 36%,#020608 76%);display:grid;place-items:center}.overlay.hidden{display:none}.modal{width:min(720px,90vw);padding:38px 44px;border:1px solid #477487;background:#08151dde;box-shadow:0 0 100px #1fa4d326}.logo{font-size:56px;line-height:.9;letter-spacing:8px;font-weight:1000;color:white;text-shadow:0 0 24px #4dddf488}.logo span{display:block;color:var(--cyan);font-size:14px;letter-spacing:10px;margin:14px 0 24px}.brief{border-left:3px solid var(--orange);padding:10px 16px;background:#13232a;margin:20px 0}.cols{display:grid;grid-template-columns:1fr 1fr;gap:24px;color:#9bb1ba}.cols b{color:#fff}.primary{background:linear-gradient(90deg,#167a92,#214e68);border-color:#62e8ff;font-weight:800;letter-spacing:2px;padding:13px 28px;margin-top:22px}.endTitle{font-size:48px;font-weight:900;letter-spacing:6px}.victory{color:#6fffd0}.defeat{color:#ff795e}@media(max-width:850px){#actions{right:12px;left:12px;bottom:177px;height:130px}.action{width:84px}.action:nth-of-type(n+7){display:none}#leftInfo{width:calc(100% - 282px)}#top{min-width:90%}.brand{display:none}}
</style></head>
<body>
<canvas id="game"></canvas>
<div id="top" class="hud panel cut"><div class="brand">FRONTIER WAR</div><div class="stat"><i class="gem"></i><span id="res">0</span> IRIDIUM</div><div class="stat supply">⬡ <span id="sup">0 / 0</span></div><div id="time" class="clock">00:00</div><button id="pause" class="iconBtn">Ⅱ</button><button id="mute" class="iconBtn">♪</button><button id="helpBtn" class="iconBtn">?</button></div>
<div id="leftInfo" class="hud panel cut"><div class="eyebrow">SELECTION</div><div id="selTitle" class="title">NO UNITS SELECTED</div><div id="selSub" class="sub">Drag-select forces or click a structure.</div><div id="selHP"></div><div id="selectionCards"></div></div>
<div id="actions" class="hud panel cut"><div id="buttons" style="display:flex;gap:7px;flex-wrap:wrap;align-content:flex-start"></div><div id="queue"><div class="eyebrow">PRODUCTION / ORDERS</div><div id="queueList" class="sub" style="margin-top:8px">Select a production structure.</div></div></div>
<div id="miniWrap" class="hud panel cut"><canvas id="mini" width="230" height="230"></canvas></div>
<div id="help" class="hud panel cut"><div class="eyebrow">FIELD MANUAL</div><div class="helpRow"><span class="key">LMB</span> select / drag group</div><div class="helpRow"><span class="key">RMB</span> move, attack, gather</div><div class="helpRow"><span class="key">X</span> attack-move · <span class="key">S</span> stop · <span class="key">H</span> hold</div><div class="helpRow"><span class="key">WASD</span> camera · <span class="key">Q E</span> rotate · wheel zoom</div><div class="helpRow"><span class="key">1-9</span> recall groups · <span class="key">Ctrl+1-9</span> assign</div><hr style="border-color:#29434f"><div class="helpRow"><b>Economy:</b> workers automatically mine blue Iridium and return it to a Nexus.</div><div class="helpRow"><b>Counters:</b> Blades shred ranged; Lancers defeat armor/air; Siege crushes structures; Skyrays raid ground.</div><div class="helpRow"><b>Victory:</b> destroy every enemy Nexus. Build additional Nexuses to expand.</div></div>
<div id="toast" class="toast"></div><div id="mode" class="banner"></div>
<div id="start" class="overlay"><div class="modal cut"><div class="logo">FRONTIER WAR<span>THE IRIDIAN EXPANSE</span></div><div class="brief"><b>OPERATION DAWNLINE</b><br>Establish a frontier network, uncover the rival Vanta Directorate, and destroy every enemy Nexus.</div><div class="cols"><div><b>BUILD & EXPAND</b><br>Workers harvest finite Iridium. Construct relays for supply, production nodes, defenses, research and new Nexuses near remote deposits.</div><div><b>SCOUT & CONQUER</b><br>The map is hidden by true fog of war. Combine fast Blades, Lancers, heavy Siege Walkers and airborne Skyrays.</div></div><button id="startBtn" class="primary">BEGIN EXPEDITION</button></div></div>
<div id="end" class="overlay hidden"><div class="modal cut"><div id="endTitle" class="endTitle">VICTORY</div><p id="endText"></p><button id="restart" class="primary">RESTART SKIRMISH</button></div></div>
<script>
(()=>{'use strict';
const canvas=document.querySelector('#game'),ctx=canvas.getContext('2d'),mini=document.querySelector('#mini'),mx=mini.getContext('2d');
const $=s=>document.querySelector(s), TAU=Math.PI*2, MAP=2200, GRID=44, CELL=MAP/GRID;
let W,H,dpr=1,running=false,paused=false,ended=false,last=0,gameTime=0,nextId=1,toastT=0,attackMode=false,buildMode=null,mouse={x:0,y:0,down:false,sx:0,sy:0,world:null},keys={},selection=[],groups={},effects=[],orderMarks=[];
let cam={x:400,y:450,z:1.08,rot:-.38};
const TYPES={
worker:{name:'Pioneer',kind:'unit',cost:70,supply:1,time:7,hp:70,speed:58,sight:170,range:15,damage:6,rate:1.2,armor:'light',weapon:'kinetic',r:10,ground:1,role:'Harvests and constructs'},
blade:{name:'Flux Blade',kind:'unit',cost:95,supply:2,time:9,hp:125,speed:72,sight:190,range:18,damage:20,rate:.75,armor:'light',weapon:'plasma',r:12,ground:1,role:'Fast anti-ranged melee'},
lancer:{name:'Arc Lancer',kind:'unit',cost:130,supply:3,time:12,hp:105,speed:48,sight:220,range:145,damage:18,rate:1.05,armor:'light',weapon:'ion',r:12,ground:1,airTarget:1,role:'Ranged anti-armor / anti-air'},
siege:{name:'Siege Walker',kind:'unit',cost:260,supply:6,time:20,hp:360,speed:30,sight:230,range:175,damage:42,rate:1.8,armor:'heavy',weapon:'explosive',r:19,ground:1,role:'Heavy anti-structure artillery'},
sky:{name:'Skyray',kind:'unit',cost:220,supply:4,time:17,hp:170,speed:85,sight:260,range:120,damage:20,rate:.85,armor:'air',weapon:'plasma',r:15,air:1,role:'Flying ground raider'},
nexus:{name:'Frontier Nexus',kind:'building',cost:650,time:38,hp:1500,sight:260,r:54,supply:12,role:'Headquarters & resource return'},
relay:{name:'Supply Relay',kind:'building',cost:120,time:12,hp:420,sight:130,r:29,supply:12,role:'Provides 12 supply'},
forge:{name:'Warforge',kind:'building',cost:240,time:20,hp:720,sight:160,r:40,role:'Trains ground units'},
aerie:{name:'Sky Aerie',kind:'building',cost:310,time:25,hp:650,sight:170,r:42,role:'Trains flying units'},
lab:{name:'Quantum Lab',kind:'building',cost:280,time:23,hp:560,sight:150,r:36,role:'Researches army upgrades'},
turret:{name:'Arc Turret',kind:'building',cost:180,time:16,hp:520,sight:225,range:170,damage:19,rate:.72,weapon:'ion',r:25,airTarget:1,ground:1,role:'Anti-ground and anti-air defense'}
};
const costs={blade:TYPES.blade,lancer:TYPES.lancer,siege:TYPES.siege,sky:TYPES.sky,worker:TYPES.worker};
let sides,ents,deposits,obstacles,fog,knownBuildings,ai;
function resize(){dpr=Math.min(2,devicePixelRatio||1);W=innerWidth;H=innerHeight;canvas.width=W*dpr;canvas.height=H*dpr;canvas.style.width=W+'px';canvas.style.height=H+'px';ctx.setTransform(dpr,0,0,dpr,0,0)}addEventListener('resize',resize);resize();
const rand=(a,b)=>a+Math.random()*(b-a),dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y),clamp=(v,a,b)=>Math.max(a,Math.min(b,v));
function side(i){return sides[i]}
function make(type,team,x,y,built=true){let t=TYPES[type],e={id:nextId++,type,team,x,y,hp:t.hp,maxHp:t.hp,r:t.r,kind:t.kind,alive:true,built,progress:built?1:0,order:'idle',tx:x,ty:y,target:null,queue:[],cool:rand(0,.4),hold:false,angle:0,carry:0,gatherTarget:null,rally:null,flash:0};ents.push(e);return e}
function spawnDeposit(x,y,amount=9000){deposits.push({x,y,amount,r:38,id:nextId++})}
function reset(){nextId=1;gameTime=0;ended=false;paused=false;attackMode=false;buildMode=null;selection=[];effects=[];orderMarks=[];sides=[{res:600,upgrade:0,color:'#51ddff'},{res:600,upgrade:0,color:'#ff684f'}];ents=[];deposits=[];knownBuildings=new Map();fog=[new Uint8Array(GRID*GRID),new Uint8Array(GRID*GRID)];obstacles=[{x:1090,y:1080,rx:170,ry:330},{x:690,y:1240,rx:160,ry:85},{x:1480,y:780,rx:165,ry:90},{x:1070,y:470,rx:100,ry:150},{x:1150,y:1730,rx:130,ry:120}];
[[330,350],[560,640],[370,1080],[760,1660],[1080,1450],[1100,820],[1460,1160],[1700,1530],[1840,1840],[1720,430],[1120,2030]].forEach((p,i)=>spawnDeposit(p[0],p[1],i===5?14000:9000));
make('nexus',0,250,240);for(let i=0;i<4;i++){let w=make('worker',0,320+i*18,285);w.order='gather';w.gatherTarget=deposits[0]}
make('nexus',1,1940,1940);for(let i=0;i<4;i++){let w=make('worker',1,1870-i*18,1895);w.order='gather';w.gatherTarget=deposits[8]}
cam={x:390,y:400,z:1.08,rot:-.38};ai={think:0,build:4,attack:95,scout:18,stage:0};updateFog(0);updateUI();$('#end').classList.add('hidden')}
function supply(team){let used=0,cap=0,reserved=0;for(let e of ents)if(e.alive&&e.team===team){let t=TYPES[e.type];if(e.kind==='unit')used+=t.supply||0;if(e.kind==='building'&&e.built)cap+=t.supply||0;for(let q of e.queue)reserved+=TYPES[q.type]?.supply||0}return{used,cap,reserved}}
function project(x,y,z=0){let dx=x-cam.x,dy=y-cam.y,c=Math.cos(cam.rot),s=Math.sin(cam.rot),rx=dx*c-dy*s,ry=dx*s+dy*c;return{x:W/2+rx*cam.z,y:H/2+ry*cam.z*.56-z*cam.z}}
function unproject(sx,sy){let rx=(sx-W/2)/cam.z,ry=(sy-H/2)/(cam.z*.56),c=Math.cos(cam.rot),s=Math.sin(cam.rot);return{x:cam.x+rx*c+ry*s,y:cam.y-rx*s+ry*c}}
function visible(team,x,y){let gx=clamp(Math.floor(x/CELL),0,GRID-1),gy=clamp(Math.floor(y/CELL),0,GRID-1);return fog[team][gy*GRID+gx]===2}function explored(x,y){let gx=clamp(Math.floor(x/CELL),0,GRID-1),gy=clamp(Math.floor(y/CELL),0,GRID-1);return fog[0][gy*GRID+gx]>0}
function updateFog(team){let f=fog[team];for(let i=0;i<f.length;i++)if(f[i]===2)f[i]=1;for(let e of ents)if(e.alive&&e.team===team&&e.built){let sight=TYPES[e.type].sight||140,gx0=Math.floor((e.x-sight)/CELL),gx1=Math.ceil((e.x+sight)/CELL),gy0=Math.floor((e.y-sight)/CELL),gy1=Math.ceil((e.y+sight)/CELL);for(let gy=gy0;gy<=gy1;gy++)for(let gx=gx0;gx<=gx1;gx++)if(gx>=0&&gy>=0&&gx<GRID&&gy<GRID&&Math.hypot((gx+.5)*CELL-e.x,(gy+.5)*CELL-e.y)<sight)f[gy*GRID+gx]=2}}
function pathBlocked(x,y,r=8){if(x<20||y<20||x>MAP-20||y>MAP-20)return true;for(let o of obstacles)if(((x-o.x)/(o.rx+r))**2+((y-o.y)/(o.ry+r))**2<1)return true;for(let e of ents)if(e.alive&&e.kind==='building'&&e.built&&Math.hypot(x-e.x,y-e.y)<r+e.r*.72)return true;return false}
function moveToward(e,x,y,dt){let dx=x-e.x,dy=y-e.y,d=Math.hypot(dx,dy);if(d<3)return true;let sp=TYPES[e.type].speed*dt,nx=e.x+dx/d*sp,ny=e.y+dy/d*sp;if(!e.air&&pathBlocked(nx,ny,e.r)){let a=Math.atan2(dy,dx),opts=[a+.75,a-.75,a+1.35,a-1.35];let moved=false;for(let aa of opts){nx=e.x+Math.cos(aa)*sp;ny=e.y+Math.sin(aa)*sp;if(!pathBlocked(nx,ny,e.r)){moved=true;break}}if(!moved)return false}e.angle=Math.atan2(dy,dx);e.x=clamp(nx,12,MAP-12);e.y=clamp(ny,12,MAP-12);return d<sp+5}
function validTarget(e,t){if(!t||!t.alive||t.team===e.team||!visible(e.team,t.x,t.y))return false;if(TYPES[t.type].air&&!TYPES[e.type].airTarget)return false;if(!TYPES[t.type].air&&!TYPES[e.type].ground)return false;return true}
function nearestEnemy(e,range){let best=null,bd=range;for(let t of ents)if(validTarget(e,t)){let d=dist(e,t)-t.r;if(d<bd){bd=d;best=t}}return best}
function damage(att,t){let at=TYPES[att.type],tt=TYPES[t.type],d=at.damage*(1+side(att.team).upgrade*.12);if(at.weapon==='ion'&&(tt.armor==='heavy'||tt.armor==='air'))d*=1.55;if(at.weapon==='plasma'&&tt.armor==='light')d*=1.35;if(at.weapon==='explosive'&&tt.kind==='building')d*=1.7;if(at.weapon==='kinetic'&&tt.armor==='heavy')d*=.65;t.hp-=d;t.flash=.12;effects.push({x:t.x,y:t.y,z:tt.air?38:8,t:0,max:.35,team:att.team,color:at.weapon==='ion'?'#74efff':at.weapon==='plasma'?'#ff66ec':'#ffb04c',from:{x:att.x,y:att.y,z:TYPES[att.type].air?45:15}});if(t.hp<=0)kill(t)}
function kill(e){e.alive=false;selection=selection.filter(x=>x!==e);for(let i=0;i<10;i++)effects.push({x:e.x+rand(-e.r,e.r),y:e.y+rand(-e.r,e.r),z:rand(0,25),t:0,max:rand(.35,.8),color:i%2?'#ff9c4c':'#5b6b70'});if(e.type==='nexus'){let remain=ents.some(x=>x.alive&&x.team===e.team&&x.type==='nexus');if(!remain)finish(e.team===1)}}
function finish(win){ended=true;paused=true;$('#end').classList.remove('hidden');$('#endTitle').textContent=win?'VICTORY':'DEFEAT';$('#endTitle').className='endTitle '+(win?'victory':'defeat');$('#endText').textContent=win?'The Vanta command network has collapsed. The frontier belongs to the Expedition.':'Your final Nexus has fallen. The Directorate controls the Iridian Expanse.'}
function processWorker(e,dt){if(e.order==='gather'){let d=e.gatherTarget;if(!d||d.amount<=0){d=nearestDeposit(e);e.gatherTarget=d;if(!d){e.order='idle';return}}if(e.carry>=30){e.order='return';return}if(dist(e,d)>d.r+8)moveToward(e,d.x,d.y,dt);else{let q=Math.min(d.amount,14*dt,30-e.carry);d.amount-=q;e.carry+=q}}
else if(e.order==='return'){let n=nearestFriendly(e,'nexus');if(!n)return;if(dist(e,n)>n.r+10)moveToward(e,n.x,n.y,dt);else{side(e.team).res+=e.carry;e.carry=0;e.order='gather'}}
else if(e.order==='build'&&e.target){let b=e.target;if(!b.alive){e.order='idle';return}if(dist(e,b)>b.r+15)moveToward(e,b.x,b.y,dt);else{b.progress+=dt/TYPES[b.type].time;if(b.progress>=1){b.progress=1;b.built=true;b.hp=b.maxHp; e.order='idle';pop(`${TYPES[b.type].name} online`);if(e.team===1&&e.type==='worker')e.order='gather'}}}}
function nearestDeposit(e){let best=null,bd=1e9;for(let d of deposits)if(d.amount>1){let q=dist(e,d);if(q<bd){bd=q;best=d}}return best}function nearestFriendly(e,type){let best=null,bd=1e9;for(let n of ents)if(n.alive&&n.team===e.team&&n.type===type&&n.built){let d=dist(e,n);if(d<bd){bd=d;best=n}}return best}
function updateEntity(e,dt){if(!e.alive)return;e.flash=Math.max(0,e.flash-dt);if(!e.built)return;let t=TYPES[e.type];e.cool-=dt;if(e.kind==='building'){if(t.damage){let tar=nearestEnemy(e,t.range);if(tar&&e.cool<=0){damage(e,tar);e.cool=t.rate}}processQueue(e,dt);return}if(e.type==='worker')processWorker(e,dt);
if(e.order==='move'||e.order==='attackmove'){if(e.order==='attackmove'){let tar=nearestEnemy(e,t.sight*.85);if(tar)e.target=tar}if(e.target&&validTarget(e,e.target)){fight(e,e.target,dt)}else if(moveToward(e,e.tx,e.ty,dt)){e.order='idle';e.target=null}}
else if(e.order==='attack'){if(validTarget(e,e.target))fight(e,e.target,dt);else{e.order='idle';e.target=null}}
else if(!e.hold&&e.order!=='gather'&&e.order!=='return'&&e.order!=='build'){let tar=nearestEnemy(e,t.sight*.7);if(tar){e.target=tar;e.order='attack'}}}
function fight(e,t,dt){let et=TYPES[e.type],range=et.range+e.r+t.r*.5,d=dist(e,t);if(d>range){if(!e.hold)moveToward(e,t.x,t.y,dt)}else if(e.cool<=0){damage(e,t);e.cool=et.rate}}
function processQueue(e,dt){if(!e.queue.length)return;let q=e.queue[0],t=TYPES[q.type];if(q.type==='upgrade'){q.p+=dt/25;if(q.p>=1){side(e.team).upgrade++;e.queue.shift();pop('Weapon harmonics upgraded');}return}let sp=supply(e.team);if(sp.used+(t.supply||0)>sp.cap){q.blocked=true;return}q.blocked=false;q.p+=dt/t.time;if(q.p>=1){let a=rand(0,TAU),u=make(q.type,e.team,e.x+Math.cos(a)*(e.r+30),e.y+Math.sin(a)*(e.r+30));if(e.rally){u.order='move';u.tx=e.rally.x;u.ty=e.rally.y}if(q.type==='worker'){u.order='gather';u.gatherTarget=nearestDeposit(u)}e.queue.shift();if(e.team===0)pop(`${t.name} deployed`)}}
function update(dt){if(!running||paused)return;gameTime+=dt;for(let e of ents)updateEntity(e,dt);ents=ents.filter(e=>e.alive||gameTime%10<9.9);for(let ef of effects)ef.t+=dt;effects=effects.filter(e=>e.t<e.max);for(let m of orderMarks)m.t-=dt;orderMarks=orderMarks.filter(m=>m.t>0);updateFog(0);updateFog(1);updateAI(dt);cameraUpdate(dt);toastT-=dt;if(toastT<=0)$('#toast').classList.remove('on');updateUI()}
function aiBuild(type,x,y){let t=TYPES[type],w=ents.find(e=>e.alive&&e.team===1&&e.type==='worker'&&e.built&&e.order!=='build');if(!w||side(1).res<t.cost)return null;if(!validPlacement(type,x,y))return null;side(1).res-=t.cost;let b=make(type,1,x,y,false);b.hp=b.maxHp*.2;w.order='build';w.target=b;return b}
function updateAI(dt){ai.think-=dt;ai.attack-=dt;if(ai.think>0)return;ai.think=1.2;let base=ents.find(e=>e.alive&&e.team===1&&e.type==='nexus'),S=supply(1),workers=ents.filter(e=>e.alive&&e.team===1&&e.type==='worker').length,army=ents.filter(e=>e.alive&&e.team===1&&e.kind==='unit'&&e.type!=='worker');if(!base)return;
let nqs=ents.filter(e=>e.alive&&e.team===1&&e.type==='nexus');if(workers<Math.min(18,nqs.length*8))queue(base,'worker',1);
if(S.cap-S.used-S.reserved<7)aiBuild('relay',base.x+rand(-150,150),base.y+rand(-150,150));
let forges=ents.filter(e=>e.alive&&e.team===1&&e.type==='forge'&&e.built),aeries=ents.filter(e=>e.alive&&e.team===1&&e.type==='aerie'&&e.built);if(!forges.length)aiBuild('forge',base.x-120,base.y-60);if(gameTime>75&&forges.length<2)aiBuild('forge',base.x+rand(-170,100),base.y+rand(-170,100));if(gameTime>130&&!aeries.length)aiBuild('aerie',base.x+80,base.y-130);if(gameTime>60&&!ents.some(e=>e.alive&&e.team===1&&e.type==='turret'))aiBuild('turret',base.x-100,base.y-120);
for(let f of forges)if(f.queue.length<2){let type=gameTime>100?(Math.random()<.45?'siege':Math.random()<.5?'lancer':'blade'):(Math.random()<.55?'blade':'lancer');queue(f,type,1)}for(let a of aeries)if(a.queue.length<2)queue(a,'sky',1);
if(gameTime>170&&nqs.length<3&&side(1).res>800){let d=deposits.filter(d=>d.amount>3000).sort((a,b)=>dist(a,base)-dist(b,base))[Math.min(nqs.length+3,deposits.length-1)];if(d)aiBuild('nexus',d.x+100,d.y+80)}
if(ai.attack<=0&&army.length>5){let targets=ents.filter(e=>e.alive&&e.team===0&&visible(1,e.x,e.y));let tar=targets.sort((a,b)=>dist(base,a)-dist(base,b))[0];if(tar){for(let u of army){u.order='attackmove';u.tx=tar.x+rand(-50,50);u.ty=tar.y+rand(-50,50);u.target=null}pop('WARNING: hostile force detected');ai.attack=Math.max(38,90-gameTime*.08)}}
if(gameTime>25){let scouts=army.filter(u=>u.type==='blade').slice(0,2);for(let u of scouts)if(u.order==='idle'){u.order='attackmove';u.tx=rand(200,2000);u.ty=rand(200,2000)}}}
function queue(e,type,team=0){let t=TYPES[type];if(!e||!e.built)return false;let valid=(e.type==='nexus'&&type==='worker')||(e.type==='forge'&&['blade','lancer','siege'].includes(type))||(e.type==='aerie'&&type==='sky')||(e.type==='lab'&&type==='upgrade');if(!valid)return false;if(e.queue.length>=5)return false;let cost=type==='upgrade'?350:t.cost;if(side(team).res<cost)return false;if(type!=='upgrade'){let sp=supply(team);if(sp.used+sp.reserved+(t.supply||0)>sp.cap)return false}side(team).res-=cost;e.queue.push({type,p:0,blocked:false});return true}
function cancelQueue(e,i=0){let q=e.queue[i];if(!q)return;let cost=q.type==='upgrade'?350:TYPES[q.type].cost;side(e.team).res+=Math.round(cost*.75);e.queue.splice(i,1);pop('Cancelled — 75% refunded')}
function validPlacement(type,x,y){let r=TYPES[type].r;if(x<r+30||y<r+30||x>MAP-r-30||y>MAP-r-30||pathBlocked(x,y,r+15))return false;for(let e of ents)if(e.alive&&dist({x,y},e)<r+e.r+20)return false;for(let d of deposits)if(d.amount>0&&dist({x,y},d)<r+d.r+20)return false;return true}
function placeBuilding(){if(!buildMode||!mouse.world)return;let workers=selection.filter(e=>e.type==='worker'&&e.team===0),t=TYPES[buildMode];if(!workers.length)return pop('Select a Pioneer to construct');if(side(0).res<t.cost)return pop('Insufficient Iridium');if(!validPlacement(buildMode,mouse.world.x,mouse.world.y))return pop('Invalid construction site');side(0).res-=t.cost;let b=make(buildMode,0,mouse.world.x,mouse.world.y,false);b.hp=b.maxHp*.2;let w=workers[0];w.order='build';w.target=b;buildMode=null;$('#mode').textContent='';selection=[b];updateUI()}
function orderSelected(x,y,target=null,attack=false){let units=selection.filter(e=>e.kind==='unit'&&e.team===0);if(!units.length){let b=selection[0];if(b&&b.kind==='building'){b.rally={x,y};orderMarks.push({x,y,t:1,color:'#5ee7ff'});}return}let cols=Math.ceil(Math.sqrt(units.length)),space=22;units.forEach((u,i)=>{if(target&&target.team!==u.team){u.order='attack';u.target=target}else if(target&&target.id&&deposits.includes(target)&&u.type==='worker'){u.order='gather';u.gatherTarget=target}else{u.order=attack?'attackmove':'move';u.target=null;u.tx=x+(i%cols-(cols-1)/2)*space;u.ty=y+(Math.floor(i/cols)-(Math.ceil(units.length/cols)-1)/2)*space}});orderMarks.push({x,y,t:1,color:attack?'#ff7b62':'#5ee7ff'})}
function hitEntity(x,y,teamVision=true){let best=null,bd=30/cam.z;for(let e of ents)if(e.alive&&(!teamVision||e.team===0||visible(0,e.x,e.y))){let d=Math.hypot(e.x-x,e.y-y);if(d<e.r+12/cam.z&&d<bd+e.r){best=e;bd=d-e.r}}return best}function hitDeposit(x,y){return deposits.find(d=>d.amount>0&&explored(d.x,d.y)&&Math.hypot(x-d.x,y-d.y)<d.r+15)}
function selectAt(x,y,shift){let e=hitEntity(x,y);if(!shift)selection=[];if(e&&e.team===0){if(shift){let i=selection.indexOf(e);i>=0?selection.splice(i,1):selection.push(e)}else selection=[e]}updateUI()}
function boxSelect(){let a=unproject(mouse.sx,mouse.sy),b=unproject(mouse.x,mouse.y),minx=Math.min(a.x,b.x),maxx=Math.max(a.x,b.x),miny=Math.min(a.y,b.y),maxy=Math.max(a.y,b.y);selection=ents.filter(e=>e.alive&&e.built&&e.team===0&&e.kind==='unit'&&e.x>=minx&&e.x<=maxx&&e.y>=miny&&e.y<=maxy);updateUI()}
function actionButton(label,sub,hot,cb,disabled=false,cost=''){let b=document.createElement('button');b.className='action';b.disabled=disabled;b.innerHTML=`<strong>${label}</strong><small>${sub}</small><span class="hot">${hot||''}</span>${cost?`<span class="cost">◆ ${cost}</span>`:''}`;b.onclick=cb;return b}
function updateUI(){if(!sides)return;let sp=supply(0);$('#res').textContent=Math.floor(side(0).res);$('#sup').textContent=`${sp.used+sp.reserved} / ${sp.cap}`;$('#sup').style.color=sp.used+sp.reserved>=sp.cap?'#ff765d':'';$('#time').textContent=`${String(Math.floor(gameTime/60)).padStart(2,'0')}:${String(Math.floor(gameTime%60)).padStart(2,'0')}`;
let title=$('#selTitle'),sub=$('#selSub'),hp=$('#selHP'),cards=$('#selectionCards'),buttons=$('#buttons'),ql=$('#queueList');buttons.innerHTML='';cards.innerHTML='';hp.innerHTML='';ql.innerHTML='';selection=selection.filter(e=>e.alive);
if(!selection.length){title.textContent='NO UNITS SELECTED';sub.textContent='Drag-select forces or click a structure.';ql.textContent='Select a production structure.';return}let e=selection[0];if(selection.length===1){let t=TYPES[e.type];title.textContent=t.name.toUpperCase();sub.textContent=t.role+(e.built?'':' · CONSTRUCTING');hp.innerHTML=`<div class="hpbar"><i style="width:${100*e.hp/e.maxHp}%"></i></div><div class="sub">${Math.ceil(e.hp)} / ${e.maxHp} integrity${!e.built?` · ${Math.floor(e.progress*100)}%`:''}</div>`}else{title.textContent=`${selection.length} UNITS SELECTED`;let counts={};selection.forEach(x=>counts[x.type]=(counts[x.type]||0)+1);sub.textContent=Object.entries(counts).map(([k,v])=>`${v} ${TYPES[k].name}`).join(' · ');selection.slice(0,40).forEach(x=>{let c=document.createElement('div');c.className='card';c.innerHTML=`<b>${TYPES[x.type].name[0]}</b><i style="width:${100*x.hp/x.maxHp}%"></i>`;c.onclick=()=>{selection=[x];updateUI()};cards.append(c)})}
let friendly=selection.filter(x=>x.team===0),workers=friendly.filter(x=>x.type==='worker');if(workers.length){[['nexus','Nexus','HQ / Drop-off','N'],['relay','Relay','Supply +12','R'],['forge','Warforge','Ground production','F'],['aerie','Aerie','Air production','Y'],['lab','Q-Lab','Upgrades','L'],['turret','Turret','Defense','T']].forEach(([type,l,s,h])=>buttons.append(actionButton(l,s,h,()=>{buildMode=type;$('#mode').textContent=`PLACE ${l.toUpperCase()} · LMB confirm · ESC cancel`},false,TYPES[type].cost)))}
if(friendly.some(x=>x.kind==='unit')){buttons.append(actionButton('Attack-Move','Engage en route','X',()=>{attackMode=true;$('#mode').textContent='ATTACK-MOVE · choose destination'}));buttons.append(actionButton('Stop','Cancel orders','S',()=>friendly.forEach(x=>{x.order='idle';x.target=null})));buttons.append(actionButton('Hold','Toggle position','H',()=>friendly.forEach(x=>x.hold=!x.hold)))}
if(selection.length===1&&e.kind==='building'&&e.built){let opts=e.type==='nexus'?['worker']:e.type==='forge'?['blade','lancer','siege']:e.type==='aerie'?['sky']:e.type==='lab'?['upgrade']:[];for(let type of opts){let t=type==='upgrade'?{name:'Harmonics',role:'+12% army damage',cost:350}:TYPES[type],can=type==='upgrade'?side(0).res>=350:side(0).res>=t.cost;buttons.append(actionButton(t.name,t.role,type==='upgrade'?'U':'',()=>{if(!queue(e,type))pop(type==='upgrade'?'Need 350 Iridium':'Need resources, supply, or queue space')},!can,t.cost))}if(e.queue.length){e.queue.forEach((q,i)=>{let d=document.createElement('div');d.className='queueItem';d.innerHTML=`${TYPES[q.type]?.name||'Harmonics'} ${q.blocked?'<b style="color:#ff806e">SUPPLY BLOCKED</b>':''}<div class="progress"><i style="width:${q.p*100}%"></i></div>`;d.onclick=()=>cancelQueue(e,i);ql.append(d)});ql.insertAdjacentHTML('beforeend','<small>Click queue item to cancel (75% refund)</small>')}else ql.textContent='Queue idle · right-click terrain sets rally'}else ql.textContent='Orders active across selection.'}
function pop(s){let el=$('#toast');el.textContent=s;el.classList.add('on');toastT=2.2}
function cameraUpdate(dt){let sp=500*dt/cam.z,dx=(keys.KeyD?1:0)-(keys.KeyA?1:0),dy=(keys.KeyS?1:0)-(keys.KeyW?1:0);if(dx||dy){let c=Math.cos(cam.rot),s=Math.sin(cam.rot);cam.x+=(dx*c+dy*s)*sp;cam.y+=(-dx*s+dy*c)*sp}if(keys.KeyQ)cam.rot-=dt;if(keys.KeyE)cam.rot+=dt;cam.x=clamp(cam.x,0,MAP);cam.y=clamp(cam.y,0,MAP)}
function drawGround(){ctx.fillStyle='#101b1d';ctx.fillRect(0,0,W,H);let corners=[project(0,0),project(MAP,0),project(MAP,MAP),project(0,MAP)];ctx.beginPath();corners.forEach((p,i)=>i?ctx.lineTo(p.x,p.y):ctx.moveTo(p.x,p.y));ctx.closePath();ctx.fillStyle='#263735';ctx.fill();ctx.strokeStyle='#3a5048';ctx.lineWidth=1;for(let i=0;i<=MAP;i+=100){let a=project(i,0),b=project(i,MAP);ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();a=project(0,i);b=project(MAP,i);ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke()}
for(let o of obstacles){let p=project(o.x,o.y);ctx.save();ctx.translate(p.x,p.y);ctx.rotate(-cam.rot);ctx.scale(o.rx*cam.z,o.ry*cam.z*.56);let g=ctx.createRadialGradient(-.2,-.2,.1,0,0,1);g.addColorStop(0,'#536058');g.addColorStop(.6,'#303d39');g.addColorStop(1,'#182422');ctx.fillStyle=g;ctx.beginPath();ctx.arc(0,0,1,0,TAU);ctx.fill();ctx.restore()}}
function prism(x,y,r,h,color,shape=6){let p=project(x,y,0),top=project(x,y,h),pts=[];for(let i=0;i<shape;i++){let a=i/shape*TAU;pts.push(project(x+Math.cos(a)*r,y+Math.sin(a)*r,0))}ctx.fillStyle='#08111399';ctx.beginPath();pts.forEach((q,i)=>i?ctx.lineTo(q.x,q.y):ctx.moveTo(q.x,q.y));ctx.closePath();ctx.fill();for(let i=0;i<shape;i++){let a=i/shape*TAU,b=(i+1)/shape*TAU,q1=project(x+Math.cos(a)*r,y+Math.sin(a)*r,0),q2=project(x+Math.cos(b)*r,y+Math.sin(b)*r,0),t2=project(x+Math.cos(b)*r,y+Math.sin(b)*r,h),t1=project(x+Math.cos(a)*r,y+Math.sin(a)*r,h);ctx.fillStyle=i%2?color:shade(color,-25);ctx.beginPath();ctx.moveTo(q1.x,q1.y);ctx.lineTo(q2.x,q2.y);ctx.lineTo(t2.x,t2.y);ctx.lineTo(t1.x,t1.y);ctx.fill()}ctx.fillStyle=shade(color,35);ctx.beginPath();for(let i=0;i<shape;i++){let a=i/shape*TAU,q=project(x+Math.cos(a)*r,y+Math.sin(a)*r,h);i?ctx.lineTo(q.x,q.y):ctx.moveTo(q.x,q.y)}ctx.closePath();ctx.fill();return top}
function shade(hex,n){let c=parseInt(hex.slice(1),16),r=clamp((c>>16)+n,0,255),g=clamp((c>>8&255)+n,0,255),b=clamp((c&255)+n,0,255);return`rgb(${r},${g},${b})`}
function drawDeposit(d){if(!explored(d.x,d.y)||d.amount<=0)return;let p=project(d.x,d.y),alpha=visible(0,d.x,d.y)?1:.38;ctx.globalAlpha=alpha;for(let i=0;i<7;i++){let a=i*2.4,r=8+(i%3)*10,x=d.x+Math.cos(a)*r,y=d.y+Math.sin(a)*r;prism(x,y,6,15+(i%4)*7,'#28d6cc',5)}ctx.globalAlpha=1;}
function drawEntity(e,sil=false){let t=TYPES[e.type],col=e.team===0?'#28aeca':'#c34238',p=project(e.x,e.y,t.air?42:0);if(sil){ctx.globalAlpha=.22;prism(e.x,e.y,e.r,t.kind==='building'?35:15,'#7b8b8e',6);ctx.globalAlpha=1;return}if(selection.includes(e)){ctx.strokeStyle='#74f5ff';ctx.lineWidth=2;ctx.beginPath();ctx.ellipse(project(e.x,e.y).x,project(e.x,e.y).y,e.r*cam.z*1.25,e.r*cam.z*.55,0,0,TAU);ctx.stroke()}
if(t.kind==='building'){let h=e.type==='nexus'?62:e.type==='relay'?48:e.type==='turret'?36:46;ctx.globalAlpha=e.built?1:.5+.3*Math.sin(gameTime*6);prism(e.x,e.y,e.r,h,col,e.type==='forge'?4:6);if(e.type==='nexus'){prism(e.x,e.y,17,83,e.team?'#ff795d':'#5ee7ff',6)}if(e.type==='relay'){let q=project(e.x,e.y,72);ctx.strokeStyle=e.team?'#ff795d':'#66efff';ctx.beginPath();ctx.arc(q.x,q.y,8*cam.z,0,TAU);ctx.stroke()}if(e.type==='turret'){prism(e.x,e.y,10,58,'#73828a',8)}ctx.globalAlpha=1}
else{ctx.save();ctx.translate(p.x,p.y);let z=cam.z;if(t.air){ctx.fillStyle='#0006';ctx.beginPath();ctx.ellipse(0,28*z,17*z,6*z,0,0,TAU);ctx.fill();ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(20*z,0);ctx.lineTo(-15*z,-9*z);ctx.lineTo(-7*z,0);ctx.lineTo(-15*z,9*z);ctx.closePath();ctx.fill();ctx.strokeStyle='#c6fbff';ctx.stroke()}else if(e.type==='siege'){ctx.fillStyle='#111';ctx.fillRect(-18*z,-6*z,36*z,15*z);ctx.fillStyle=col;ctx.fillRect(-13*z,-14*z,26*z,17*z);ctx.fillStyle='#abb8bc';ctx.fillRect(0,-18*z,24*z,5*z)}else{ctx.fillStyle=col;ctx.beginPath();ctx.arc(0,-7*z,e.r*z,0,TAU);ctx.fill();ctx.fillStyle=e.type==='worker'?'#e9d071':e.type==='lancer'?'#92f3ff':'#d8e7eb';ctx.fillRect(-3*z,-18*z,6*z,12*z);if(e.type==='blade'){ctx.strokeStyle='#ff7ae8';ctx.beginPath();ctx.moveTo(4*z,-13*z);ctx.lineTo(14*z,-25*z);ctx.stroke()}}ctx.restore()}
if(e.hp<e.maxHp||selection.includes(e)){let q=project(e.x,e.y,t.air?70:(t.kind==='building'?75:26)),w=Math.max(24,e.r*1.5)*cam.z;ctx.fillStyle='#081015';ctx.fillRect(q.x-w/2,q.y,w,4);ctx.fillStyle=e.team?'#ff5d4b':'#63ee9d';ctx.fillRect(q.x-w/2,q.y,w*e.hp/e.maxHp,4)}if(e.carry){let q=project(e.x,e.y,30);ctx.fillStyle='#6fffe7';ctx.fillText('◆',q.x-4,q.y)}if(e.flash){ctx.globalAlpha=e.flash*4;ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(p.x,p.y,e.r*cam.z,0,TAU);ctx.fill();ctx.globalAlpha=1}}
function drawFog(){ctx.save();ctx.globalCompositeOperation='source-over';for(let gy=0;gy<GRID;gy++)for(let gx=0;gx<GRID;gx++){let v=fog[0][gy*GRID+gx];if(v===2)continue;let a=project(gx*CELL,gy*CELL),b=project((gx+1)*CELL,gy*CELL),c=project((gx+1)*CELL,(gy+1)*CELL),d=project(gx*CELL,(gy+1)*CELL);ctx.fillStyle=v===0?'#020608f2':'#0710149c';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.lineTo(c.x,c.y);ctx.lineTo(d.x,d.y);ctx.fill()}ctx.restore()}
function render(){drawGround();for(let d of deposits)drawDeposit(d);let draw=ents.filter(e=>e.alive&&(e.team===0||visible(0,e.x,e.y))).sort((a,b)=>a.x+a.y-b.x-b.y);for(let e of draw){drawEntity(e);if(e.team===1&&e.kind==='building')knownBuildings.set(e.id,{...e})}for(let [id,e] of knownBuildings)if(!ents.some(x=>x.id===id&&x.alive&&visible(0,x.x,x.y))&&!visible(0,e.x,e.y))drawEntity(e,true);drawFog();
for(let ef of effects){if(ef.team===1&&!visible(0,ef.x,ef.y))continue;let k=ef.t/ef.max,p=project(ef.x,ef.y,ef.z);if(ef.from){let a=project(ef.from.x,ef.from.y,ef.from.z);ctx.strokeStyle=ef.color;ctx.lineWidth=2;ctx.globalAlpha=1-k;ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(p.x,p.y);ctx.stroke()}ctx.fillStyle=ef.color;ctx.globalAlpha=1-k;ctx.beginPath();ctx.arc(p.x,p.y,(3+k*12)*cam.z,0,TAU);ctx.fill()}ctx.globalAlpha=1;for(let m of orderMarks){let p=project(m.x,m.y);ctx.strokeStyle=m.color;ctx.globalAlpha=m.t;ctx.beginPath();ctx.arc(p.x,p.y,(1-m.t)*22+5,0,TAU);ctx.stroke()}ctx.globalAlpha=1;if(mouse.down&&Math.hypot(mouse.x-mouse.sx,mouse.y-mouse.sy)>6){ctx.strokeStyle='#77ecff';ctx.fillStyle='#48d8ff18';let x=Math.min(mouse.x,mouse.sx),y=Math.min(mouse.y,mouse.sy),w=Math.abs(mouse.x-mouse.sx),h=Math.abs(mouse.y-mouse.sy);ctx.fillRect(x,y,w,h);ctx.strokeRect(x,y,w,h)}if(buildMode&&mouse.world){let ok=validPlacement(buildMode,mouse.world.x,mouse.world.y);ctx.globalAlpha=.6;prism(mouse.world.x,mouse.world.y,TYPES[buildMode].r,35,ok?'#45ddaa':'#e74f4f',6);ctx.globalAlpha=1}drawMini()}
function drawMini(){let sc=230/MAP;mx.fillStyle='#091315';mx.fillRect(0,0,230,230);for(let gy=0;gy<GRID;gy++)for(let gx=0;gx<GRID;gx++){let v=fog[0][gy*GRID+gx];mx.fillStyle=v===0?'#010405':v===1?'#172322':'#30443f';mx.fillRect(gx*230/GRID,gy*230/GRID,230/GRID+1,230/GRID+1)}for(let d of deposits)if(d.amount>0&&explored(d.x,d.y)){mx.fillStyle='#55e1d2';mx.fillRect(d.x*sc-2,d.y*sc-2,4,4)}for(let e of ents)if(e.alive&&e.built&&(e.team===0||visible(0,e.x,e.y))){mx.fillStyle=e.team?'#ff654e':'#56e8ff';let z=e.kind==='building'?5:2;mx.fillRect(e.x*sc-z/2,e.y*sc-z/2,z,z)}let spanX=W/(cam.z)*.75,spanY=H/(cam.z*.56)*.55;mx.strokeStyle='#fff';mx.strokeRect((cam.x-spanX/2)*sc,(cam.y-spanY/2)*sc,spanX*sc,spanY*sc)}
function frame(ts){let dt=Math.min(.05,(ts-last)/1000||0);last=ts;update(dt);ctx.clearRect(0,0,W,H);if(sides)render();requestAnimationFrame(frame)}requestAnimationFrame(frame);
canvas.addEventListener('pointerdown',e=>{if(e.button!==0||!running||paused)return;mouse.down=true;mouse.sx=mouse.x=e.clientX;mouse.sy=mouse.y=e.clientY;mouse.world=unproject(mouse.x,mouse.y)});canvas.addEventListener('pointermove',e=>{mouse.x=e.clientX;mouse.y=e.clientY;mouse.world=unproject(mouse.x,mouse.y)});canvas.addEventListener('pointerup',e=>{if(e.button!==0||!mouse.down)return;mouse.down=false;let w=unproject(e.clientX,e.clientY);if(buildMode){placeBuilding();return}if(attackMode){orderSelected(w.x,w.y,null,true);attackMode=false;$('#mode').textContent='';return}if(Math.hypot(e.clientX-mouse.sx,e.clientY-mouse.sy)>8)boxSelect();else selectAt(w.x,w.y,e.shiftKey)});canvas.addEventListener('contextmenu',e=>{e.preventDefault();if(!running||paused)return;let w=unproject(e.clientX,e.clientY),t=hitEntity(w.x,w.y),d=hitDeposit(w.x,w.y);orderSelected(w.x,w.y,t||d,false)});canvas.addEventListener('wheel',e=>{e.preventDefault();cam.z=clamp(cam.z*Math.exp(-e.deltaY*.001),.38,2.1)},{passive:false});
mini.addEventListener('pointerdown',e=>{let r=mini.getBoundingClientRect();cam.x=(e.clientX-r.left)/r.width*MAP;cam.y=(e.clientY-r.top)/r.height*MAP});
addEventListener('keydown',e=>{keys[e.code]=true;if(e.target.tagName==='BUTTON')return;if(e.code==='Escape'){buildMode=null;attackMode=false;$('#mode').textContent=''}if(e.code==='Space'){paused=!paused;$('#pause').textContent=paused?'▶':'Ⅱ'}if(e.code==='KeyX'&&!keys.ControlLeft){attackMode=true;$('#mode').textContent='ATTACK-MOVE · choose destination'}if(e.code==='KeyS')selection.forEach(x=>{x.order='idle';x.target=null});if(e.code==='KeyH')selection.forEach(x=>x.hold=!x.hold);if(/^Digit[1-9]$/.test(e.code)){let n=e.code.slice(-1);if(e.ctrlKey)groups[n]=selection.slice();else{selection=(groups[n]||[]).filter(x=>x.alive);if(selection.length){cam.x=selection[0].x;cam.y=selection[0].y}updateUI()}}});addEventListener('keyup',e=>keys[e.code]=false);
$('#startBtn').onclick=()=>{reset();running=true;$('#start').classList.add('hidden');pop('Opening window secured — establish your economy')};$('#restart').onclick=()=>{reset();running=true;paused=false;$('#end').classList.add('hidden')};$('#pause').onclick=()=>{paused=!paused;$('#pause').textContent=paused?'▶':'Ⅱ'};$('#helpBtn').onclick=()=>{$('#help').style.display=$('#help').style.display==='block'?'none':'block'};$('#mute').onclick=()=>{$('#mute').textContent=$('#mute').textContent==='♪'?'×':'♪'};
reset();running=false;
})();
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-5.6-sol","provider":"openrouter","persona":"full","sessionId":"2ff14285-414a-41aa-9676-33fc8c404373","tokensIn":275348,"tokensOut":16863,"tokensTotal":292211,"cost":0.2970884,"turns":12,"toolCalls":17,"failedToolCalls":0,"timestamp":"2026-09-30T21:50:19.848Z"} -->