← RTS results

RTS

muse spark 1.3 contributor · 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 80,110 bytes · SHA-256 19c086a5d30a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>FRONTIER WAR — 3D Sci-Fi RTS</title>
<style>
:root{--cy:#35e0ff;--bg:#05080f;--panel:rgba(8,16,28,.92);--line:rgba(53,224,255,.25);--txt:#cfeefc;--dim:#7fa3b8;--warn:#ffb347;--bad:#ff5468;--good:#5dffa0;}
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
html,body{width:100%;height:100%;overflow:hidden;background:#04070d;font-family:"Segoe UI",Roboto,Helvetica,Arial,sans-serif;color:var(--txt);user-select:none;-webkit-user-select:none}
#wrap{position:fixed;inset:0}
#game{position:absolute;inset:0;display:block;cursor:crosshair}
#vignette{position:absolute;inset:0;pointer-events:none;background:radial-gradient(ellipse at center,transparent 55%,rgba(0,0,10,.55) 100%);z-index:2}
#scan{position:absolute;inset:0;pointer-events:none;z-index:2;opacity:.06;background:repeating-linear-gradient(0deg,#fff 0 1px,transparent 1px 3px)}
#topbar{position:absolute;top:0;left:0;right:0;height:44px;display:flex;align-items:center;gap:10px;padding:0 10px;background:linear-gradient(180deg,rgba(6,14,26,.97),rgba(6,14,26,.75) 80%,transparent);z-index:10;pointer-events:none}
#topbar .pill{pointer-events:auto;display:flex;align-items:center;gap:7px;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:5px 11px;font-size:13px;font-weight:600;white-space:nowrap}
#resFlux b{color:#ffd75e;font-size:15px}#supPill b{color:var(--cy)}#armyPill b{color:var(--good)}
#objBtn,#helpBtn,#muteBtn,#pauseBtn{cursor:pointer;pointer-events:auto;background:var(--panel);border:1px solid var(--line);color:var(--txt);border-radius:8px;padding:6px 10px;font-size:13px;font-weight:700}
#objBtn:hover,#helpBtn:hover,#muteBtn:hover,#pauseBtn:hover{border-color:var(--cy);color:#fff}
#timer{margin-left:auto;font-variant-numeric:tabular-nums;color:var(--dim);font-size:13px}
#bottom{position:absolute;left:0;right:0;bottom:0;display:flex;gap:10px;padding:10px;z-index:10;pointer-events:none;align-items:flex-end}
#selPanel{pointer-events:auto;flex:1;min-width:0;max-width:560px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:9px 11px;min-height:118px}
#selTitle{font-size:13px;font-weight:800;margin-bottom:6px;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
#selGrid{display:flex;gap:6px;flex-wrap:wrap;max-height:66px;overflow-y:auto}
.unitChip{width:52px;text-align:center;font-size:9px;color:var(--dim);cursor:pointer}
.unitChip .face{width:44px;height:36px;border-radius:7px;margin:0 auto 2px;border:1px solid var(--line)}
.hpbar{height:4px;background:#12202e;border-radius:2px;overflow:hidden;margin-top:2px}
.hpbar i{display:block;height:100%;background:linear-gradient(90deg,#37ff9b,#b6ff5e)}
#cmdPanel{pointer-events:auto;width:348px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:9px}
#cmdPanel h4{font-size:11px;color:var(--dim);letter-spacing:1.2px;margin:2px 2px 7px}
#cmdGrid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:6px}
.cmdBtn{position:relative;background:rgba(20,36,54,.9);border:1px solid rgba(53,224,255,.3);border-radius:8px;padding:7px 5px 6px;text-align:center;cursor:pointer;font-size:11px;font-weight:700;line-height:1.25;color:var(--txt)}
.cmdBtn small{display:block;font-weight:400;color:var(--dim);font-size:9.5px}
.cmdBtn:hover:not(.off){border-color:var(--cy);background:rgba(30,60,90,.95)}
.cmdBtn.off{opacity:.42;cursor:not-allowed;filter:grayscale(.6)}
.cmdBtn .cost{color:#ffd75e}.cmdBtn .key{position:absolute;top:3px;right:5px;font-size:9px;color:var(--cy);border:1px solid var(--line);border-radius:4px;padding:0 4px}
.cmdBtn.building{border-color:rgba(255,183,71,.45)}
#queueBar{display:flex;gap:5px;flex-wrap:wrap;margin-top:8px}
.qItem{background:#0e1c2c;border:1px solid var(--line);border-radius:6px;font-size:10px;padding:3px 6px;cursor:pointer}
.qItem:hover{border-color:var(--bad)}
#orderRow{display:flex;gap:6px;margin-top:8px}
.ordBtn{flex:1;background:#0d1a29;border:1px solid var(--line);border-radius:7px;color:var(--txt);font-size:11px;font-weight:800;padding:7px 0;cursor:pointer}
.ordBtn.on{background:rgba(53,224,255,.25);border-color:var(--cy)}
#mmWrap{pointer-events:auto;width:218px;background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:8px}
#minimap{display:block;width:100%;height:172px;border-radius:8px;cursor:pointer;background:#02040a;border:1px solid rgba(53,224,255,.2)}
#mmRow{display:flex;justify-content:space-between;align-items:center;margin-top:6px;font-size:10px;color:var(--dim)}
#alerts{position:absolute;left:50%;transform:translateX(-50%);top:52px;z-index:11;display:flex;flex-direction:column;gap:6px;align-items:center;pointer-events:none;width:min(560px,90vw)}
.toast{background:rgba(8,16,28,.94);border:1px solid var(--line);border-left:4px solid var(--cy);border-radius:8px;padding:7px 14px;font-size:13px;font-weight:600;animation:tin .25s ease-out;max-width:100%;text-align:center}
.toast.warn{border-left-color:var(--warn)}.toast.bad{border-left-color:var(--bad);color:#ffdfe4}.toast.good{border-left-color:var(--good)}
@keyframes tin{from{opacity:0;transform:translateY(-8px)}}
.overlay{position:absolute;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;background:rgba(2,5,12,.72);padding:16px}
.card{width:min(760px,95vw);max-height:92vh;overflow-y:auto;background:linear-gradient(180deg,#0a1524,#060d18);border:1px solid rgba(53,224,255,.4);border-radius:18px;padding:30px 32px;box-shadow:0 0 80px rgba(53,224,255,.15),0 30px 80px #000;text-align:center}
.card h1{font-size:44px;letter-spacing:4px;background:linear-gradient(90deg,#8ff3ff,#35e0ff 40%,#b48cff 70%,#ff8fb4);-webkit-background-clip:text;background-clip:text;color:transparent;font-weight:900}
.card h2{font-size:13px;letter-spacing:5px;color:var(--dim);margin:4px 0 14px}
.card p{font-size:14px;line-height:1.55;color:#b9d6e6;margin:8px 0}
.factions{display:flex;gap:10px;margin:14px 0}
.faction{flex:1;border-radius:12px;padding:12px;border:1px solid var(--line);background:rgba(15,30,48,.7);font-size:12.5px;line-height:1.5;text-align:left}
.faction b{font-size:14px}
.howto{display:grid;grid-template-columns:1fr 1fr;gap:8px;text-align:left;margin:14px 0;font-size:12.5px}
.howto div{background:rgba(12,24,40,.8);border:1px solid rgba(53,224,255,.15);border-radius:10px;padding:9px 11px;line-height:1.5}
.howto b{color:var(--cy)}kbd{background:#13273c;border:1px solid #2a4a66;border-bottom-width:3px;border-radius:5px;padding:0 6px;font-size:11px;font-family:inherit}
.bigBtn{display:inline-block;margin-top:14px;background:linear-gradient(180deg,#2ad4ff,#0a8fd6);color:#02141f;font-weight:900;font-size:18px;letter-spacing:1px;border:none;border-radius:12px;padding:14px 54px;cursor:pointer;box-shadow:0 6px 30px rgba(53,224,255,.4)}
.bigBtn:hover{filter:brightness(1.12)}
.ghostBtn{display:inline-block;margin-top:12px;margin-left:10px;background:transparent;color:var(--txt);border:1px solid var(--line);border-radius:12px;padding:13px 26px;font-weight:700;cursor:pointer;font-size:14px}
#loadTip{font-size:11px;color:var(--dim);margin-top:10px}
.hidden{display:none!important}
#hint{position:absolute;bottom:175px;left:50%;transform:translateX(-50%);z-index:9;pointer-events:none;font-size:12px;background:rgba(5,12,22,.85);border:1px solid var(--line);padding:6px 14px;border-radius:20px;color:var(--dim);white-space:nowrap;max-width:92vw;overflow:hidden;text-overflow:ellipsis}
#fpsTag{position:absolute;left:10px;bottom:155px;z-index:9;font-size:10px;color:rgba(127,163,184,.7);pointer-events:none}
@media(max-width:900px){#cmdPanel{width:290px}#mmWrap{width:170px}#minimap{height:130px}.howto{grid-template-columns:1fr}.card h1{font-size:30px}}
</style>
</head>
<body>
<div id="wrap">
<canvas id="game"></canvas>
<div id="vignette"></div><div id="scan"></div>
<div id="topbar">
<div class="pill" id="resFlux" title="Flux crystals">◆ <b>0</b><span style="color:var(--dim);font-weight:400">flux</span><span id="incRate" style="color:var(--good);font-size:11px"></span></div>
<div class="pill" id="supPill" title="Supply used / capacity">⬢ <b>0/0</b></div>
<div class="pill" id="armyPill" title="Combat units">⚔ <b>0</b></div>
<button id="objBtn">◉ Objective</button><button id="helpBtn">? Help</button><button id="muteBtn">🔊</button><button id="pauseBtn">⏸</button>
<span id="timer">00:00</span>
</div>
<div id="alerts"></div><div id="hint" class="hidden"></div><div id="fpsTag"></div>
<div id="bottom">
<div id="selPanel"><div id="selTitle">No selection</div><div id="selGrid"></div></div>
<div id="cmdPanel"><h4 id="cmdTitle">COMMAND</h4><div id="cmdGrid"></div><div id="queueBar"></div><div id="orderRow"><button class="ordBtn" id="oAtk" title="Attack-move (A)">⚔ ATTACK (A)</button><button class="ordBtn" id="oStop" title="Stop (S)">■ STOP (S)</button><button class="ordBtn" id="oHold" title="Hold (H)">◉ HOLD (H)</button></div></div>
<div id="mmWrap"><canvas id="minimap" width="202" height="172"></canvas><div id="mmRow"><span>MINIMAP click: camera · right-click: order</span></div></div>
</div>
<div id="titleScr" class="overlay"><div class="card">
<h1>FRONTIER WAR</h1><h2>3D SCI-FI REAL-TIME STRATEGY · SKIRMISH: EMBER VALE</h2>
<p><b style="color:#ffd75e">The frontier world Ember Vale is rich in flux crystals.</b> You lead the <b style="color:var(--cy)">Vanguard Expedition</b> against the rival <b style="color:#ff7a6b">Crimson Combine</b>. Destroy <b>all enemy Headquarters (Nexus)</b> to win. Lose all of yours and the expedition falls.</p>
<div class="factions">
<div class="faction"><b style="color:var(--cy)">◈ VANGUARD (you)</b><br>Start: 1 Nexus + 4 Mules. Scout, harvest ◆ flux, raise ⬢ Pylons, mass Blades, Lancers, Golems &amp; Wasps.</div>
<div class="faction"><b style="color:#ff7a6b">⬢ CRIMSON COMBINE (AI)</b><br>Real economy, scouts, expands and raids. Starts across the vale — location <b>unknown</b>. Raids begin after a few minutes.</div>
</div>
<div class="howto">
<div><b>Select / Order</b><br><kbd>Left-click</kbd> select · <kbd>drag</kbd> box-select · <kbd>Shift</kbd> add · <kbd>double-click</kbd> type-select · <kbd>Right-click</kbd> move/attack/harvest · <kbd>A</kbd> attack-move <kbd>S</kbd> stop <kbd>H</kbd> hold · <kbd>Shift</kbd>+order queues</div>
<div><b>Camera</b><br><kbd>WASD</kbd>/arrows or edges pan · <kbd>wheel</kbd> zoom · <kbd>Q</kbd>/<kbd>E</kbd> or <kbd>middle-drag</kbd> rotate · click minimap to jump</div>
<div><b>Economy</b><br>Mules harvest ◆ → return to Nexus/Depot. <b>Pylons ⬢</b> = supply. Expand with new Nexus near far fields. Deposits deplete!</div>
<div><b>Counters</b><br>🔪 <b>Blade</b> melee anti-light · 🔫 <b>Lancer</b> ranged, hits air · 🛡 <b>Golem</b> heavy siege · 🐝 <b>Wasp</b> flies, hits all. <b>Kinetic ≫ light · Plasma ≫ heavy.</b> Upgrades in Spire.</div>
</div>
<button class="bigBtn" id="startBtn">▶ DEPLOY</button><button class="ghostBtn" id="helpBtn2">Field Manual</button>
<div id="loadTip">No downloads · 100% procedural 3D · synthesized sound · best on desktop</div>
</div></div>
<div id="pauseScr" class="overlay hidden"><div class="card" style="width:min(480px,92vw)"><h1 style="font-size:30px">PAUSED</h1><p>The frontier waits for no commander… except now.</p><button class="bigBtn" id="resumeBtn">RESUME</button><button class="ghostBtn" id="restartBtn1">Restart</button></div></div>
<div id="endScr" class="overlay hidden"><div class="card" style="width:min(520px,92vw)"><h1 id="endTitle" style="font-size:38px">VICTORY</h1><h2 id="endSub"></h2><p id="endStats"></p><button class="bigBtn" id="restartBtn2">↻ NEW SKIRMISH</button></div></div>
<div id="helpScr" class="overlay hidden"><div class="card" style="text-align:left"><h1 style="font-size:26px;text-align:center">FIELD MANUAL</h1>
<div class="howto">
<div><b>Goal</b> — Destroy every enemy <b>Nexus (HQ)</b>. Extra Nexus act as expansions + drop-offs. Lose all of yours = defeat.</div>
<div><b>Supply ⬢</b> — Units cost supply; queued units reserve it. <b>Pylons</b> +10 each. Losing Pylons blocks training, never kills units.</div>
<div><b>Buildings</b> — <b>Nexus</b> HQ/drop-off · <b>Pylon</b> supply · <b>Barracks</b> Mule/Blade/Lancer · <b>Factory</b> Golem · <b>Skyport</b> Wasp · <b>Spire</b> research · <b>Cannon</b> hits air+ground · <b>Depot</b> cheap drop-off.</div>
<div><b>Upgrades (Spire)</b> — Plasma Edge +20% dmg · Aegis −20% taken · Ion Thrusters +18% speed.</div>
<div><b>Hotkeys</b> — <kbd>1–4</kbd> groups (<kbd>Ctrl</kbd>+<kbd>1–4</kbd> set) · <kbd>A</kbd>/<kbd>S</kbd>/<kbd>H</kbd> orders · <kbd>Esc</kbd> cancel · <kbd>Space</kbd> jump to battle · <kbd>double-click</kbd> select type.</div>
<div><b>Tips</b> — Scout early. Cannons + Lancers beat Wasps. Golems break turtles. Raid workers to cripple the AI. Expand before fields run dry.</div>
</div><div style="text-align:center"><button class="bigBtn" id="helpClose">CLOSE</button></div></div></div>
<div id="objScr" class="overlay hidden"><div class="card" style="width:min(480px,92vw);text-align:left"><h1 style="font-size:24px;text-align:center">OBJECTIVE</h1><p>💥 Destroy <b>all Crimson Nexus (HQ)</b> buildings.<br>🛡 If you lose <b>all your Nexus</b>, you are defeated.<br><br>◆ Harvest flux · ⬢ raise supply · expand · scout the unknown · mass combined arms.<br><br>The Combine base is <b>hidden</b> — scout to find it.</p><div style="text-align:center"><button class="bigBtn" id="objClose">CLOSE</button></div></div></div>
</div>
<script>
"use strict";
const TAU=Math.PI*2;
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const dist2=(ax,az,bx,bz)=>{const dx=ax-bx,dz=az-bz;return dx*dx+dz*dz;};
const rand=(a,b)=>a+Math.random()*(b-a);
const randi=(a,b)=>Math.floor(rand(a,b+1));
const $=id=>document.getElementById(id);
function fmtT(s){s=Math.floor(s);return String(Math.floor(s/60)).padStart(2,"0")+":"+String(s%60).padStart(2,"0");}
/* audio */
const AudioSys={ctx:null,muted:false,
init(){if(this.ctx)return;try{this.ctx=new (window.AudioContext||window.webkitAudioContext)();if(this.ctx.state==="suspended")this.ctx.resume();}catch(e){}},
tone(f,d,type,vol,slide){if(this.muted||!this.ctx)return;try{const t=this.ctx.currentTime,o=this.ctx.createOscillator(),g=this.ctx.createGain();o.type=type||"square";o.frequency.setValueAtTime(f,t);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(20,slide),t+d);g.gain.setValueAtTime(vol||.12,t);g.gain.exponentialRampToValueAtTime(.001,t+d);o.connect(g);g.connect(this.ctx.destination);o.start(t);o.stop(t+d);}catch(e){}},
click(){this.tone(700,.06,"square",.06);},order(){this.tone(440,.09,"square",.07,660);},build(){this.tone(220,.15,"sawtooth",.07,330);},
ready(){this.tone(880,.12,"sine",.1);setTimeout(()=>this.tone(1320,.15,"sine",.1),110);},
boom(){this.tone(120,.4,"sawtooth",.16,35);},shot(){this.tone(rand(900,1400),.05,"square",.03,300);},
alarm(){this.tone(520,.25,"sawtooth",.12,260);},coin(){this.tone(1500,.07,"sine",.05,2100);}};
/* data */
const UNIT_DEFS={
mule:{name:"Mule",icon:"🛠",cost:60,sup:1,hp:70,speed:11,range:1.6,dmg:4,rof:1,dtype:"kinetic",armor:"light",air:false,targets:"ground",desc:"Worker. Harvests ◆ flux, builds."},
blade:{name:"Blade",icon:"🔪",cost:90,sup:1,hp:140,speed:12.5,range:2.2,dmg:11,rof:1.1,dtype:"kinetic",armor:"light",air:false,targets:"ground",desc:"Melee skirmisher. Shreds light units."},
lancer:{name:"Lancer",icon:"🔫",cost:130,sup:2,hp:110,speed:10.5,range:13,dmg:9,rof:.9,dtype:"kinetic",armor:"light",air:false,targets:"both",desc:"Ranged trooper. Hits air & ground."},
golem:{name:"Golem",icon:"🛡",cost:260,sup:4,hp:520,speed:6.5,range:11,dmg:30,rof:.55,dtype:"plasma",armor:"heavy",air:false,targets:"ground",desc:"Heavy siege walker. Crushes armor & buildings."},
wasp:{name:"Wasp",icon:"🐝",cost:170,sup:2,hp:130,speed:17,range:10,dmg:12,rof:1,dtype:"plasma",armor:"light",air:true,targets:"both",desc:"Gunship. Flies over terrain. Hits all."}};
const BLD_DEFS={
nexus:{name:"Nexus",icon:"◈",cost:450,hp:1600,size:7.5,sight:26,sup:0,dropoff:true,desc:"HQ. Trains Mules. Drop-off. Lose all = defeat.",time:28},
pylon:{name:"Pylon",icon:"⬢",cost:100,hp:350,size:3.4,sight:14,sup:10,dropoff:false,desc:"+10 supply. Cheap & fragile.",time:9},
depot:{name:"Depot",icon:"▣",cost:120,hp:500,size:4.2,sight:13,sup:0,dropoff:true,desc:"Forward drop-off for miners.",time:11},
barracks:{name:"Barracks",icon:"⚒",cost:220,hp:800,size:6.2,sight:16,sup:0,dropoff:false,desc:"Trains Mule / Blade / Lancer.",time:18},
factory:{name:"War Factory",icon:"🏭",cost:330,hp:950,size:6.8,sight:16,sup:0,dropoff:false,desc:"Builds Golem siege walkers.",time:22},
skyport:{name:"Skyport",icon:"🛸",cost:330,hp:850,size:6.8,sight:18,sup:0,dropoff:false,desc:"Builds Wasp gunships.",time:22},
spire:{name:"Spire",icon:"🔬",cost:260,hp:650,size:5.2,sight:17,sup:0,dropoff:false,desc:"Research: damage / armor / speed.",time:20},
cannon:{name:"Cannon",icon:"💥",cost:200,hp:600,size:4.4,sight:22,sup:0,dropoff:false,desc:"Defense turret. Hits ground + air.",time:14,weapon:{range:17,dmg:16,rof:.8,dtype:"plasma",targets:"both"}}};
const BUILD_MENU=[{id:"nexus",key:"N"},{id:"pylon",key:"P"},{id:"depot",key:"D"},{id:"barracks",key:"B"},{id:"factory",key:"F"},{id:"skyport",key:"S"},{id:"spire",key:"R"},{id:"cannon",key:"C"}];
const UPGRADES={edge:{name:"Plasma Edge",icon:"⚡",cost:250,time:30,desc:"+20% attack damage"},aegis:{name:"Aegis Plating",icon:"🛡",cost:250,time:30,desc:"−20% damage taken"},thrust:{name:"Ion Thrusters",icon:"💨",cost:220,time:25,desc:"+18% move speed"}};
function dmgMult(dtype,armor){if(dtype==="kinetic"&&armor==="light")return 1.35;if(dtype==="kinetic"&&armor==="heavy")return .7;if(dtype==="plasma"&&armor==="heavy")return 1.4;if(dtype==="plasma"&&armor==="light")return .85;return 1;}
/* world */
const WORLD=240;
const Game={state:"title",time:0,paused:false,over:false,units:[],buildings:[],projectiles:[],particles:[],floaters:[],crystals:[],obstacles:[],fogN:72,fogVis:null,fogSeen:null,aiVis:null,aiSeen:null,players:[],nextId:1,cam:{x:-88,z:-88,yaw:.7,pitch:.9,dist:70},stats:{kills:0,losses:0,harvested:0}};
function P(o){return Game.players[o];}
function newPlayer(name,color,dark,isAI){return{name:name,color:color,dark:dark,isAI:isAI,flux:0,supplyUsed:0,supplyCap:0,upg:{edge:false,aegis:false,thrust:false},researching:null};}
function genMap(){
Game.crystals.length=0;Game.obstacles.length=0;
const obs=[{x:-60,z:-40,w:36,h:10},{x:60,z:40,w:36,h:10},{x:-70,z:55,w:10,h:34},{x:70,z:-55,w:10,h:34},{x:-28,z:0,w:8,h:44},{x:28,z:0,w:8,h:44},{x:0,z:-72,w:44,h:9},{x:0,z:72,w:44,h:9},{x:-95,z:-85,w:22,h:12},{x:95,z:85,w:22,h:12}];
for(const o of obs)Game.obstacles.push(o);
function field(cx,cz,n,amt){for(let i=0;i<n;i++){const a=Math.random()*TAU,r=rand(0,7);Game.crystals.push({x:cx+Math.cos(a)*r,z:cz+Math.sin(a)*r,amount:amt,max:amt,id:Game.nextId++});}}
const s=88;
field(-s,-s,7,1500);field(s,s,7,1500);field(-s+18,-s+2,5,1200);field(s-18,s-2,5,1200);
field(0,0,8,1600);field(-60,60,6,1400);field(60,-60,6,1400);field(0,-55,5,1100);field(0,55,5,1100);field(-55,0,4,1000);field(55,0,4,1000);
}
function basePos(o){return o===0?{x:-88,z:-88}:{x:88,z:88};}
/* fog */
function fogInit(){const n=Game.fogN;Game.fogVis=new Uint8Array(n*n);Game.fogSeen=new Uint8Array(n*n);Game.aiVis=new Uint8Array(n*n);Game.aiSeen=new Uint8Array(n*n);}
function w2f(x){return clamp(Math.floor((x+WORLD/2)/WORLD*Game.fogN),0,Game.fogN-1);}
function fogStamp(arr,x,z,radius){const n=Game.fogN,cx=w2f(x),cz=w2f(z),cr=Math.ceil(radius/WORLD*n);for(let j=-cr;j<=cr;j++)for(let i=-cr;i<=cr;i++){const fx=cx+i,fz=cz+j;if(fx<0||fz<0||fx>=n||fz>=n)continue;const wx=(fx+.5)/n*WORLD-WORLD/2,wz=(fz+.5)/n*WORLD-WORLD/2;if(dist2(x,z,wx,wz)<radius*radius)arr[fz*n+fx]=1;}}
function fogUpdate(){Game.fogVis.fill(0);Game.aiVis.fill(0);for(const u of Game.units){if(u.hp<=0)continue;const s=u.sight||14;if(u.owner===0)fogStamp(Game.fogVis,u.x,u.z,s);else fogStamp(Game.aiVis,u.x,u.z,s);}for(const b of Game.buildings){if(b.hp<=0)continue;const d=BLD_DEFS[b.type];if(b.owner===0)fogStamp(Game.fogVis,b.x,b.z,d.sight);else fogStamp(Game.aiVis,b.x,b.z,d.sight);}for(let i=0;i<Game.fogSeen.length;i++){if(Game.fogVis[i])Game.fogSeen[i]=1;if(Game.aiVis[i])Game.aiSeen[i]=1;}}
function visibleToPlayer(x,z){return Game.fogVis[w2f(z)*Game.fogN+w2f(x)]===1;}
function seenToPlayer(x,z){return Game.fogSeen[w2f(z)*Game.fogN+w2f(x)]===1;}
function visibleToAI(x,z){return Game.aiVis[w2f(z)*Game.fogN+w2f(x)]===1;}
/* entities */
function addUnit(owner,type,x,z){const d=UNIT_DEFS[type];const u={id:Game.nextId++,kind:"unit",owner:owner,type:type,x:x,z:z,tx:null,tz:null,hp:d.hp,maxhp:d.hp,cd:rand(0,.3),target:null,targetManual:false,order:null,queue:[],hold:false,atkMove:false,harv:null,returnTo:null,carry:0,carryMax:12,gatherT:0,buildTarget:null,sight:type==="wasp"?24:(type==="mule"?15:17),fly:!!d.air,retarget:rand(0,.4),swing:rand(0,6),hitT:0,moveT:0};Game.units.push(u);P(owner).supplyUsed+=d.sup;return u;}
function addBuilding(owner,type,x,z,instant){const d=BLD_DEFS[type];const b={id:Game.nextId++,kind:"bld",owner:owner,type:type,x:x,z:z,hp:instant?d.hp:Math.floor(d.hp*.12),maxhp:d.hp,buildPct:instant?1:.12,queue:[],qMax:5,rally:null,cd:rand(0,.5),target:null,research:null,researchT:0,researchNeed:0,progNeed:d.time,hitT:0,doneSnd:instant?true:false,everSeenBy0:owner===0,everSeenBy1:owner===1};Game.buildings.push(b);if(type==="pylon")P(owner).supplyCap+=d.sup;return b;}
function explode(x,z,h,color,n){for(let i=0;i<n;i++)Game.particles.push({x:x+rand(-2,2),z:z+rand(-2,2),h:h+rand(0,3),vx:rand(-9,9),vz:rand(-9,9),vh:rand(2,14),life:rand(.3,.9),t:0,color:Math.random()<.3?"#fff":color,size:rand(1,3)});}
function floater(x,z,txt,color){Game.floaters.push({x:x,z:z,txt:txt,color:color||"#ffd75e",t:0,life:1.4});}
function toast(txt,cls){const d=document.createElement("div");d.className="toast "+(cls||"");d.textContent=txt;$("alerts").appendChild(d);setTimeout(()=>{d.style.opacity="0";d.style.transition="opacity .5s";setTimeout(()=>d.remove(),500);},3400);while($("alerts").children.length>4)$("alerts").firstChild.remove();}
function removeDead(){
for(let i=Game.units.length-1;i>=0;i--){const u=Game.units[i];if(u.hp<=0){P(u.owner).supplyUsed=Math.max(0,P(u.owner).supplyUsed-UNIT_DEFS[u.type].sup);explode(u.x,u.z,u.fly?6:0,u.owner===0?"#35e0ff":"#ff6a5e",14);AudioSys.boom();if(u.owner===0){Game.stats.losses++;Sel.ids.delete(u.id);}else Game.stats.kills++;Game.units.splice(i,1);}}
for(let i=Game.buildings.length-1;i>=0;i--){const b=Game.buildings[i];if(b.hp<=0){if(b.type==="pylon")P(b.owner).supplyCap=Math.max(0,P(b.owner).supplyCap-BLD_DEFS.pylon.sup);explode(b.x,b.z,2,b.owner===0?"#35e0ff":"#ff6a5e",34);AudioSys.boom();Sel.ids.delete(b.id);if(b.owner===0){toast("⚠ "+BLD_DEFS[b.type].name+" destroyed!","bad");AudioSys.alarm();}Game.buildings.splice(i,1);}}
for(let i=Game.crystals.length-1;i>=0;i--)if(Game.crystals[i].amount<=0)Game.crystals.splice(i,1);
}
/* collision */
function collideCircle(x,z,r,ignoreId){for(const o of Game.obstacles){const cx=clamp(x,o.x-o.w/2,o.x+o.w/2),cz=clamp(z,o.z-o.h/2,o.z+o.h/2);const dx=x-cx,dz=z-cz;if(dx*dx+dz*dz<r*r)return o;}for(const b of Game.buildings){if(b.hp<=0)continue;if(b.buildPct<0.5)continue;if(ignoreId&&b.id===ignoreId)continue;const d=BLD_DEFS[b.type];const hw=d.size/2-0.4;const cx=clamp(x,b.x-hw,b.x+hw),cz=clamp(z,b.z-hw,b.z+hw);const dx=x-cx,dz=z-cz;if(dx*dx+dz*dz<r*r)return b;}return null;}
function resolveMove(u,dt){
if(u.tx===null||u.tx===undefined)return;
let dx=u.tx-u.x,dz=u.tz-u.z;const d=Math.hypot(dx,dz);
if(d<.7){u.tx=null;u.tz=null;return;}
dx/=d;dz/=d;
const spd=UNIT_DEFS[u.type].speed*(P(u.owner).upg.thrust?1.18:1)*(u.carry>0&&u.type==="mule"?.92:1);
let nx=u.x+dx*spd*dt,nz=u.z+dz*spd*dt;
nx=clamp(nx,-WORLD/2+2,WORLD/2-2);nz=clamp(nz,-WORLD/2+2,WORLD/2-2);
if(!u.fly){const hit=collideCircle(nx,nz,1.1);if(hit){if(!collideCircle(nx,u.z,1.1))nz=u.z;else if(!collideCircle(u.x,nz,1.1))nx=u.x;else{const a=Math.atan2(dz,dx)+.9;nx=u.x+Math.cos(a)*spd*dt;nz=u.z+Math.sin(a)*spd*dt;if(collideCircle(nx,nz,1.1)){u.tx=null;u.tz=null;return;}}}}
u.x=nx;u.z=nz;u.moveT+=dt;
if(!u.fly){for(const o of Game.units){if(o===u||o.fly||o.hp<=0)continue;const dd=dist2(u.x,u.z,o.x,o.z);if(dd<2.6&&dd>.0001){const l=Math.sqrt(dd);u.x+=(u.x-o.x)/l*(1.6-l)*dt*3;u.z+=(u.z-o.z)/l*(1.6-l)*dt*3;}}}
}
function nearestDropoff(owner,x,z){let best=null,bd=1e18;for(const b of Game.buildings){if(b.owner!==owner||b.hp<=0||b.buildPct<1)continue;if(!BLD_DEFS[b.type].dropoff)continue;const dd=dist2(x,z,b.x,b.z);if(dd<bd){bd=dd;best=b;}}return best;}
function nearestCrystal(x,z,forOwner){let best=null,bd=1e18;for(const c of Game.crystals){if(c.amount<=0)continue;if(forOwner===0&&!seenToPlayer(c.x,c.z))continue;const dd=dist2(x,z,c.x,c.z);if(dd<bd){bd=dd;best=c;}}return best;}
/* orders */
function issueOrder(units,order){AudioSys.order();for(const u of units){if(order.shift){u.queue.push(order);continue;}u.queue.length=0;applyOrder(u,order);}if(order.x!==undefined)Game.particles.push({x:order.x,z:order.z,h:1,vx:0,vz:0,vh:0,life:.5,t:0,color:"#5df2ff",size:5,ring:true});}
function applyOrder(u,o){u.hold=false;if(o.mode==="move"){u.order="move";u.tx=o.x;u.tz=o.z;u.target=null;u.targetManual=false;u.atkMove=false;u.harv=null;u.buildTarget=null;}else if(o.mode==="attackmove"){u.order="attackmove";u.tx=o.x;u.tz=o.z;u.target=null;u.atkMove=true;u.harv=null;u.buildTarget=null;}else if(o.mode==="attack"){u.order="attack";u.target=o.target||null;u.targetManual=!!o.target;u.tx=null;u.tz=null;u.atkMove=false;u.harv=null;}else if(o.mode==="harvest"){u.order="harvest";u.harv=o.crystal||null;u.buildTarget=null;u.atkMove=false;u.target=null;if(o.crystal){u.tx=o.crystal.x;u.tz=o.crystal.z;}}else if(o.mode==="return"){u.order="harvest";u.harv=null;const d=nearestDropoff(u.owner,u.x,u.z);if(d){u.returnTo=d;u.tx=d.x+rand(-4,4);u.tz=d.z+rand(-4,4);}}else if(o.mode==="build"){u.order="build";u.buildTarget=o.building;u.tx=o.building.x;u.tz=o.building.z;u.harv=null;u.target=null;}else if(o.mode==="stop"){u.order=null;u.tx=null;u.tz=null;u.target=null;u.targetManual=false;u.atkMove=false;u.queue.length=0;}else if(o.mode==="hold"){u.order="hold";u.hold=true;u.tx=null;u.tz=null;u.target=null;}}
/* combat */
function canTarget(u,e){if(!e||e.hp===undefined||e.hp<=0)return false;const d=UNIT_DEFS[u.type];if(e.kind==="unit"){if(d.targets==="ground"&&UNIT_DEFS[e.type].air)return false;}return true;}
function acquireTarget(u){
const d=UNIT_DEFS[u.type];const sight=u.sight||16;let best=null,bd=1e18;
const visOK=e=>{if(e.owner===u.owner||e.hp<=0)return false;if(u.owner===0){if(e.kind==="unit")return visibleToPlayer(e.x,e.z);return visibleToPlayer(e.x,e.z)||e.everSeenBy0;}return e.kind==="unit"?visibleToAI(e.x,e.z):(visibleToAI(e.x,e.z)||e.everSeenBy1);};
const consider=e=>{if(!visOK(e)||!canTarget(u,e))return;const dd=dist2(u.x,u.z,e.x,e.z);if(dd>sight*sight*1.7)return;const s=dd+(e.kind==="bld"?900:0);if(s<bd){bd=s;best=e;}};
for(const e of Game.units){if(e.owner!==u.owner)consider(e);}
for(const b of Game.buildings){if(b.owner!==u.owner)consider(b);}
return best;
}
function unitAttack(u,e,dt){
const d=UNIT_DEFS[u.type];const dd=Math.sqrt(dist2(u.x,u.z,e.x,e.z));const range=d.range+(e.kind==="bld"?2:0);
if(dd>range){if(u.hold&&!u.targetManual)return false;const spd=UNIT_DEFS[u.type].speed*(P(u.owner).upg.thrust?1.18:1);let nx=u.x+(e.x-u.x)/dd*spd*dt,nz=u.z+(e.z-u.z)/dd*spd*dt;nx=clamp(nx,-WORLD/2+2,WORLD/2-2);nz=clamp(nz,-WORLD/2+2,WORLD/2-2);if(!u.fly){if(!collideCircle(nx,nz,1.1)){u.x=nx;u.z=nz;}}else{u.x=nx;u.z=nz;}return false;}
u.cd-=dt;
if(u.cd<=0){u.cd=1/d.rof;u.swing+=2;fireProjectile(u,e);}
return true;
}
function fireProjectile(u,e){
const d=UNIT_DEFS[u.type];let mult=dmgMult(d.dtype,e.kind==="unit"?UNIT_DEFS[e.type].armor:"heavy");
if(e.kind==="bld")mult*=(u.type==="golem"?1.5:(u.type==="wasp"?.8:.7));
let dmg=d.dmg*mult*(P(u.owner).upg.edge?1.2:1);
if(e.kind==="unit"&&P(e.owner).upg.aegis)dmg*=.8;
Game.projectiles.push({x:u.x,z:u.z,h:u.fly?7:2.5,tx:e,ox:e.x,oz:e.z,spd:u.type==="lancer"?70:(u.type==="wasp"?60:40),dmg:dmg,kind:u.type==="golem"?"shell":(u.type==="lancer"?"bolt":(u.type==="wasp"?"plasma":"slash")),color:u.owner===0?(u.type==="golem"?"#ffb347":(u.type==="wasp"?"#c07bff":"#5df2ff")):(u.type==="golem"?"#ff8a5e":"#ff5d6e"),t:0});
if(u.owner===0||visibleToPlayer(u.x,u.z))AudioSys.shot();
}
/* production */
function trainTime(t){return{mule:9,blade:10,lancer:13,golem:22,wasp:17}[t];}
function tryQueueUnit(b,type){const pl=P(b.owner);if(b.buildPct<1){if(b.owner===0)toast("Building under construction","warn");return false;}if(b.queue.length>=b.qMax){if(b.owner===0)toast("Queue full (5)","warn");return false;}const c=UNIT_DEFS[type].cost;if(pl.flux<c){if(b.owner===0)toast("Not enough flux ("+c+"◆)","warn");return false;}if(pl.supplyUsed+UNIT_DEFS[type].sup>pl.supplyCap){if(b.owner===0){toast("Need more supply — build Pylons ⬢","warn");AudioSys.alarm();}return false;}pl.flux-=c;pl.supplyUsed+=UNIT_DEFS[type].sup;b.queue.push({type:type,t:0,need:trainTime(type)});if(b.owner===0)AudioSys.build();return true;}
function cancelQueue(b,idx){const q=b.queue[idx];if(!q)return;P(b.owner).flux+=UNIT_DEFS[q.type].cost;P(b.owner).supplyUsed=Math.max(0,P(b.owner).supplyUsed-UNIT_DEFS[q.type].sup);b.queue.splice(idx,1);}
function tryResearch(b,key){const pl=P(b.owner);const u=UPGRADES[key];if(pl.upg[key]||pl.researching){if(b.owner===0)toast("Already researched / busy","warn");return false;}if(pl.flux<u.cost){if(b.owner===0)toast("Not enough flux ("+u.cost+"◆)","warn");return false;}pl.flux-=u.cost;b.research=key;b.researchT=0;b.researchNeed=u.time;pl.researching={b:b,key:key};if(b.owner===0)AudioSys.build();return true;}
function canPlace(owner,type,x,z){const d=BLD_DEFS[type];if(Math.abs(x)>WORLD/2-6||Math.abs(z)>WORLD/2-6)return{ok:false,why:"Out of bounds"};for(const o of Game.obstacles){if(x+d.size/2>o.x-o.w/2&&x-d.size/2<o.x+o.w/2&&z+d.size/2>o.z-o.h/2&&z-d.size/2<o.z+o.h/2)return{ok:false,why:"Blocked by rocks"};}for(const b of Game.buildings){if(b.hp<=0)continue;const od=BLD_DEFS[b.type];if(Math.abs(x-b.x)<(d.size+od.size)/2+.6&&Math.abs(z-b.z)<(d.size+od.size)/2+.6)return{ok:false,why:"Overlaps building"};}for(const c of Game.crystals){if(Math.abs(x-c.x)<d.size/2+1.5&&Math.abs(z-c.z)<d.size/2+1.5)return{ok:false,why:"Overlaps crystals"};}return{ok:true};}
function placeBuilding(owner,type,x,z){const chk=canPlace(owner,type,x,z);if(!chk.ok){if(owner===0)toast("Can't build: "+chk.why,"warn");return null;}const pl=P(owner);if(pl.flux<BLD_DEFS[type].cost){if(owner===0)toast("Not enough flux ("+BLD_DEFS[type].cost+"◆)","warn");return null;}pl.flux-=BLD_DEFS[type].cost;const b=addBuilding(owner,type,x,z,false);if(owner===0)AudioSys.build();return b;}
/* simulation */
let aiTimer=0,fogTimer=0,uiTimer=0,aiScoutT=20,baseWarnT=0;
function simulate(dt){
Game.time+=dt;
for(const u of Game.units){
if(u.hp<=0)continue;u.cd-=dt;u.hitT-=dt;u.retarget-=dt;
if(u.buildTarget&&(u.buildTarget.hp<=0||u.buildTarget.buildPct>=1)){u.buildTarget=null;if(u.order==="build"){u.order=null;u.tx=null;}}
if(u.order==="build"&&u.buildTarget){const b=u.buildTarget,dd=Math.sqrt(dist2(u.x,u.z,b.x,b.z));if(dd>BLD_DEFS[b.type].size/2+1.5){u.tx=b.x+rand(-1,1);u.tz=b.z+rand(-1,1);resolveMove(u,dt);}else{b.buildPct=Math.min(1,b.buildPct+dt/b.progNeed*3.2);b.hp=Math.max(b.hp,b.maxhp*b.buildPct);if(Math.random()<dt*6)Game.particles.push({x:b.x+rand(-3,3),z:b.z+rand(-3,3),h:rand(0,4),vx:0,vz:0,vh:3,life:.4,t:0,color:"#ffd75e",size:2});if(b.buildPct>=1&&!b.doneSnd){b.doneSnd=true;if(b.owner===0){toast("✔ "+BLD_DEFS[b.type].name+" complete","good");AudioSys.ready();floater(b.x,b.z,"COMPLETE","#5dffa0");}}}continue;}
if(u.type==="mule"&&(u.order==="harvest")){harvestLogic(u,dt);}
if(!u.target||u.target.hp<=0){u.target=null;u.targetManual=false;
if(u.retarget<=0){u.retarget=.4+Math.random()*.3;
const military=u.type!=="mule";
if(u.order==="attackmove"||u.order==="attack"||u.atkMove||(military&&(!u.order||u.hold)))u.target=acquireTarget(u);}}
if(u.target&&u.target.hp>0){
let canSee=true;
if(u.target.kind==="unit")canSee=u.owner===0?visibleToPlayer(u.target.x,u.target.z):visibleToAI(u.target.x,u.target.z);
else canSee=u.owner===0?(visibleToPlayer(u.target.x,u.target.z)||u.target.everSeenBy0):(visibleToAI(u.target.x,u.target.z)||u.target.everSeenBy1);
if(canSee&&canTarget(u,u.target)){if(u.type==="mule"){u.target=null;}else unitAttack(u,u.target,dt);}
else{u.target=null;u.targetManual=false;}
}else if(u.order==="attackmove"||u.order==="move"||u.order==="attack"){
if(u.tx!==null&&u.tx!==undefined)resolveMove(u,dt);
if(u.order==="attackmove"&&u.tx===null){u.order=null;u.atkMove=false;}
}else if(!u.order&&u.tx!==null&&u.tx!==undefined){resolveMove(u,dt);}
if(u.queue.length&&u.target===null&&u.tx===null){const nx=u.queue.shift();applyOrder(u,nx);}
}
for(const b of Game.buildings){
if(b.hp<=0)continue;b.hitT-=dt;
const def=BLD_DEFS[b.type];
if(b.buildPct<1)continue;
if(b.queue.length){const q=b.queue[0];q.t+=dt;if(q.t>=q.need){b.queue.shift();const sx=clamp(b.x+rand(-def.size,def.size),-WORLD/2+2,WORLD/2-2),sz=clamp(b.z+def.size/2+2,-WORLD/2+2,WORLD/2-2);const u=addUnit(b.owner,q.type,sx,sz);P(b.owner).supplyUsed-=UNIT_DEFS[q.type].sup;if(b.rally){u.tx=b.rally.x;u.tz=b.rally.z;u.order="move";}else if(b.owner===1){u.tx=b.x+rand(-8,8);u.tz=b.z+rand(-8,8);u.order="move";}if(b.owner===0){AudioSys.ready();floater(b.x,b.z,UNIT_DEFS[q.type].name+" ready","#5dffa0");refreshPanel();}}}
if(b.research){b.researchT+=dt;if(b.researchT>=b.researchNeed){P(b.owner).upg[b.research]=true;P(b.owner).researching=null;if(b.owner===0){toast(UPGRADES[b.research].icon+" "+UPGRADES[b.research].name+" researched!","good");AudioSys.ready();refreshPanel();}b.research=null;}}
if(def.weapon){b.cd-=dt;if(b.cd<=0){const tgt=acquireBldTarget(b);if(tgt){b.cd=1/def.weapon.rof;b.target=tgt;Game.projectiles.push({x:b.x,z:b.z,h:6,tx:tgt,ox:tgt.x,oz:tgt.z,spd:80,dmg:def.weapon.dmg*(P(b.owner).upg.edge?1.2:1),kind:"bolt",color:b.owner===0?"#5df2ff":"#ff5d6e",t:0});if(b.owner===0||visibleToPlayer(b.x,b.z))AudioSys.shot();}else b.target=null;}}
}
for(let i=Game.projectiles.length-1;i>=0;i--){const p=Game.projectiles[i];p.t+=dt;let tx=p.ox,tz=p.oz;if(p.tx&&p.tx.hp>0){tx=p.tx.x;tz=p.tx.z;}const dx=tx-p.x,dz=tz-p.z,dd=Math.hypot(dx,dz);const step=p.spd*dt;
if(dd<Math.max(1.4,step)){if(p.tx&&p.tx.hp>0){let dmg=p.dmg;if(p.tx.kind==="unit"&&P(p.tx.owner).upg.aegis)dmg*=.8;p.tx.hp-=dmg;p.tx.hitT=.15;
if(p.tx.owner===0){baseWarnT-=0;if(p.tx.kind==="bld"&&baseWarnT<=0){baseWarnT=8;toast("⚠ Base under attack!","bad");AudioSys.alarm();}}
Game.particles.push({x:tx,z:tz,h:(p.tx.kind==="unit"&&UNIT_DEFS[p.tx.type].air)?7:2,vx:0,vz:0,vh:2,life:.3,t:0,color:p.color,size:3});if(p.kind==="shell")explode(tx,tz,1,"#ffb347",6);}else Game.particles.push({x:tx,z:tz,h:1,vx:0,vz:0,vh:2,life:.25,t:0,color:p.color,size:2});Game.projectiles.splice(i,1);}else{p.x+=dx/dd*step;p.z+=dz/dd*step;}if(p.t>4)Game.projectiles.splice(i,1);}
for(let i=Game.particles.length-1;i>=0;i--){const q=Game.particles[i];q.t+=dt;if(q.t>q.life){Game.particles.splice(i,1);continue;}q.x+=q.vx*dt;q.z+=q.vz*dt;q.h+=q.vh*dt;q.vh-=20*dt;}
for(let i=Game.floaters.length-1;i>=0;i--){const f=Game.floaters[i];f.t+=dt;if(f.t>f.life)Game.floaters.splice(i,1);}
removeDead();
fogTimer-=dt;if(fogTimer<=0){fogTimer=.4;fogUpdate();}
uiTimer-=dt;if(uiTimer<=0){uiTimer=.5;updateTop();}
aiTimer-=dt;if(aiTimer<=0){aiTimer=.5;aiThink();}
checkEnd();
}
function acquireBldTarget(b){const w=BLD_DEFS[b.type].weapon;if(!w)return null;let best=null,bd=1e18;for(const e of Game.units){if(e.owner===b.owner||e.hp<=0)continue;if(w.targets==="ground"&&UNIT_DEFS[e.type].air)continue;const vis=b.owner===0?visibleToPlayer(e.x,e.z):visibleToAI(e.x,e.z);if(!vis)continue;const dd=dist2(b.x,b.z,e.x,e.z);if(dd>w.range*w.range)continue;if(dd<bd){bd=dd;best=e;}}return best;}
function harvestLogic(u,dt){
if(u.carry>=u.carryMax){const d=(u.returnTo&&u.returnTo.hp>0)?u.returnTo:nearestDropoff(u.owner,u.x,u.z);if(!d){u.tx=null;return;}u.returnTo=d;const dd=Math.sqrt(dist2(u.x,u.z,d.x,d.z));if(dd<BLD_DEFS[d.type].size/2+2.5){P(u.owner).flux+=u.carry;if(u.owner===0){Game.stats.harvested+=u.carry;if(Math.random()<.25)AudioSys.coin();}u.carry=0;if(u.harv&&u.harv.amount>0){u.tx=u.harv.x;u.tz=u.harv.z;}else{u.harv=nearestCrystal(u.x,u.z,u.owner===0?0:-1);if(u.harv){u.tx=u.harv.x;u.tz=u.harv.z;}else{u.order=null;u.tx=null;}}}else{u.tx=d.x;u.tz=d.z;resolveMove(u,dt);}return;}
if(!u.harv||u.harv.amount<=0){u.harv=nearestCrystal(u.x,u.z,u.owner===0?0:-1);if(!u.harv){u.order=null;u.tx=null;return;}u.tx=u.harv.x;u.tz=u.harv.z;}
const c=u.harv,dd=Math.sqrt(dist2(u.x,u.z,c.x,c.z));
if(dd<3){u.tx=null;u.tz=null;u.gatherT+=dt;if(u.gatherT>.7){u.gatherT=0;const g=Math.min(3,c.amount,u.carryMax-u.carry);u.carry+=g;c.amount-=g;if(Math.random()<.4&&u.owner===0)Game.particles.push({x:c.x+rand(-1,1),z:c.z+rand(-1,1),h:2,vx:0,vz:0,vh:4,life:.4,t:0,color:"#ffd75e",size:2});}}
else{u.tx=c.x;u.tz=c.z;resolveMove(u,dt);}
}
function checkEnd(){
if(Game.over||Game.state!=="play")return;
const pHQ=Game.buildings.filter(b=>b.owner===0&&b.type==="nexus").length;
const aHQ=Game.buildings.filter(b=>b.owner===1&&b.type==="nexus").length;
if(aHQ===0&&pHQ===0){endGame(false);}else if(aHQ===0){endGame(true);}else if(pHQ===0){endGame(false);}
}
function endGame(win){Game.over=true;Game.state="over";$("endTitle").textContent=win?"VICTORY":"DEFEAT";$("endSub").textContent=win?"THE VALE IS YOURS · "+fmtT(Game.time):"THE EXPEDITION HAS FALLEN · "+fmtT(Game.time);$("endStats").innerHTML="Kills: <b>"+Game.stats.kills+"</b> · Losses: <b>"+Game.stats.losses+"</b> · Flux mined: <b>"+Math.floor(Game.stats.harvested)+"</b>";$("endScr").classList.remove("hidden");if(win)AudioSys.ready();else AudioSys.boom();}
/* AI */
const AI={atkTimer:150,raidTimer:70};
function aiBuilderCount(){return Game.units.filter(u=>u.owner===1&&u.type==="mule"&&u.hp>0&&u.order==="build").length;}
function aiThink(){
if(Game.over||Game.state!=="play")return;
const me=P(1);AI.atkTimer-=.5;AI.raidTimer-=.5;
const myUnits=Game.units.filter(u=>u.owner===1&&u.hp>0);
const workers=myUnits.filter(u=>u.type==="mule");
const army=myUnits.filter(u=>u.type!=="mule");
const myBlds=Game.buildings.filter(b=>b.owner===1&&b.hp>0);
const unfinished=myBlds.filter(b=>b.buildPct<1).length;
const has=t=>myBlds.some(b=>b.type===t&&b.buildPct>=1);
const dropoffs=myBlds.filter(b=>BLD_DEFS[b.type].dropoff&&b.buildPct>=1);
const wantWorkers=dropoffs.length*7+4;
if(workers.length<wantWorkers&&me.flux>=60){const nx=myBlds.find(b=>b.type==="nexus"&&b.buildPct>=1&&b.queue.length<5);if(nx&&me.supplyUsed+1<=me.supplyCap)tryQueueUnit(nx,"mule");}
for(const w of workers){if(w.order==="build"&&w.buildTarget&&w.buildTarget.hp>0)continue;if(!w.order&&!w.harv){const c=nearestCrystal(w.x,w.z,-1);if(c){w.harv=c;w.order="harvest";w.tx=c.x;w.tz=c.z;}}}
const builders=aiBuilderCount();
if(builders<2&&unfinished<2){
if(army.length+workers.length>=me.supplyCap-2&&me.flux>=100)aiBuild("pylon");
else if(!has("barracks")&&!myBlds.some(b=>b.type==="barracks")&&me.flux>=220)aiBuild("barracks");
else if(!has("pylon")&&!myBlds.some(b=>b.type==="pylon")&&me.flux>=100)aiBuild("pylon");
else if(Game.time>150&&!myBlds.some(b=>b.type==="cannon")&&me.flux>=200)aiBuild("cannon");
else if(Game.time>170&&!myBlds.some(b=>b.type==="factory")&&me.flux>=330)aiBuild("factory");
else if(Game.time>200&&!myBlds.some(b=>b.type==="spire")&&me.flux>=260)aiBuild("spire");
else if(Game.time>260&&!myBlds.some(b=>b.type==="skyport")&&me.flux>=330)aiBuild("skyport");
else if(Game.time>300&&myBlds.filter(b=>b.type==="nexus").length<2&&me.flux>=450)aiExpand();
else if(Game.time>430&&myBlds.filter(b=>b.type==="nexus").length<3&&me.flux>=600)aiExpand();
else if(me.flux>650&&Math.random()<.4)aiBuild("pylon");
else if(Game.time>360&&me.flux>400&&myBlds.filter(b=>b.type==="factory").length<2)aiBuild("factory");
else if(Game.time>400&&me.flux>350&&myBlds.filter(b=>b.type==="cannon").length<3)aiBuild("cannon");
}
const barr=myBlds.filter(b=>b.type==="barracks"&&b.buildPct>=1);
const fact=myBlds.filter(b=>b.type==="factory"&&b.buildPct>=1);
const sky=myBlds.filter(b=>b.type==="skyport"&&b.buildPct>=1);
const nL=army.filter(u=>u.type==="lancer").length;
for(const b of barr){if(b.queue.length>=2)continue;let want="blade";const r=Math.random();if(Game.time>240&&nL<army.length*.4&&r<.45)want="lancer";else if(r<.5)want="blade";else want="lancer";if(me.flux>=UNIT_DEFS[want].cost&&me.supplyUsed+UNIT_DEFS[want].sup<=me.supplyCap)tryQueueUnit(b,want);}
for(const b of fact){if(b.queue.length>=2)continue;if(Game.time>180&&me.flux>=260&&me.supplyUsed+4<=me.supplyCap)tryQueueUnit(b,"golem");}
for(const b of sky){if(b.queue.length>=2)continue;if(Game.time>280&&me.flux>=170&&me.supplyUsed+2<=me.supplyCap)tryQueueUnit(b,"wasp");}
const spire=myBlds.find(b=>b.type==="spire"&&b.buildPct>=1);
if(spire&&!me.researching&&!me.upg.edge&&me.flux>300)tryResearch(spire,"edge");
else if(spire&&!me.researching&&me.upg.edge&&!me.upg.aegis&&me.flux>300)tryResearch(spire,"aegis");
else if(spire&&!me.researching&&me.upg.aegis&&!me.upg.thrust&&me.flux>280)tryResearch(spire,"thrust");
aiScoutT-=.5;
if(aiScoutT<=0){aiScoutT=40;const cands=workers.filter(w=>w.order!=="build");if(cands.length&&Game.time>60){const w=cands[randi(0,cands.length-1)];if(w.order==="harvest"&&Math.random()<0.5){}else{const spot=aiScoutTarget();w.tx=spot.x;w.tz=spot.z;w.order="move";w.target=null;w.harv=null;}}}
const known=[...Game.units.filter(u=>u.owner===0&&visibleToAI(u.x,u.z)),...Game.buildings.filter(b=>b.owner===0&&(visibleToAI(b.x,b.z)||b.everSeenBy1))];
for(const b of Game.buildings)if(b.owner===0&&visibleToAI(b.x,b.z))b.everSeenBy1=true;
if(AI.raidTimer<=0&&Game.time>150){AI.raidTimer=rand(70,110);if(army.length>=4){const sq=army.slice(0,Math.min(army.length,5));const tgt=known.length?known[randi(0,known.length-1)]:{x:rand(-60,60),z:rand(-60,60)};for(const u of sq){u.tx=tgt.x+rand(-6,6);u.tz=tgt.z+rand(-6,6);u.order="attackmove";u.atkMove=true;u.target=null;}}}
if(AI.atkTimer<=0){AI.atkTimer=Game.time<300?120:rand(75,105);if(Game.time>210&&army.length>=10){let tgt;const nx=known.find(e=>e.kind==="bld"&&e.type==="nexus");if(known.length)tgt=nx||known[randi(0,known.length-1)];else tgt={x:rand(-90,90),z:rand(-90,90)};for(const u of army){if(u.type==="mule")continue;u.tx=tgt.x+rand(-10,10);u.tz=tgt.z+rand(-10,10);u.order="attackmove";u.atkMove=true;u.target=null;}}}
const bp=basePos(1);
const intr=Game.units.filter(u=>u.owner===0&&u.hp>0&&dist2(u.x,u.z,bp.x,bp.z)<45*45&&visibleToAI(u.x,u.z));
if(intr.length>=2){const defs=army.slice(0,8);for(const u of defs){u.tx=intr[0].x+rand(-5,5);u.tz=intr[0].z+rand(-5,5);u.order="attackmove";u.atkMove=true;}}
}
function aiBuild(type){const me=P(1);if(me.flux<BLD_DEFS[type].cost)return false;const bp=basePos(1);for(let k=0;k<60;k++){const x=clamp(bp.x+rand(-20,20),-WORLD/2+8,WORLD/2-8),z=clamp(bp.z+rand(-20,20),-WORLD/2+8,WORLD/2-8);if(!canPlace(1,type,x,z).ok)continue;const b=placeBuilding(1,type,x,z);if(!b)return false;const ws=Game.units.filter(u=>u.owner===1&&u.type==="mule"&&u.hp>0&&u.order!=="build").sort((a,c)=>dist2(a.x,a.z,x,z)-dist2(c.x,c.z,x,z))[0];if(ws){ws.buildTarget=b;ws.order="build";ws.tx=x;ws.tz=z;ws.harv=null;}return true;}return false;}
function aiExpand(){let best=null,bs=-1;for(const c of Game.crystals){if(c.amount<300)continue;let near=false;for(const b of Game.buildings){if(b.owner===1&&BLD_DEFS[b.type].dropoff&&dist2(b.x,b.z,c.x,c.z)<30*30){near=true;break;}}if(near)continue;if(c.amount>bs){bs=c.amount;best=c;}}if(!best)return false;const me=P(1);const type=me.flux>=450?"nexus":"depot";for(let k=0;k<20;k++){const px=clamp(best.x+rand(-8,8),-WORLD/2+8,WORLD/2-8),pz=clamp(best.z+rand(-8,8),-WORLD/2+8,WORLD/2-8);if(!canPlace(1,type,px,pz).ok)continue;if(me.flux<BLD_DEFS[type].cost)return false;const b=placeBuilding(1,type,px,pz);if(!b)return false;const ws=Game.units.filter(u=>u.owner===1&&u.type==="mule"&&u.hp>0).sort((a,c)=>dist2(a.x,a.z,px,pz)-dist2(c.x,c.z,px,pz))[0];if(ws){ws.buildTarget=b;ws.order="build";ws.tx=px;ws.tz=pz;ws.harv=null;}return true;}return false;}
function aiScoutTarget(){for(let k=0;k<8;k++){const x=rand(-100,100),z=rand(-100,100);if(!Game.aiSeen[w2f(z)*Game.fogN+w2f(x)])return{x:x,z:z};}return{x:rand(-90,90),z:rand(-90,90)};}
/* input/camera/selection */
const canvas=$("game"),ctx=canvas.getContext("2d");
const mm=$("minimap"),mmc=mm.getContext("2d");
let W=0,H=0,DPR=1;
function resize(){DPR=Math.min(2,window.devicePixelRatio||1);W=window.innerWidth;H=window.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);}
window.addEventListener("resize",resize);resize();
const Sel={ids:new Set(),buildMode:null,atkMode:false,rallyBld:null,groups:{},drag:null};
function selectedUnits(){return Game.units.filter(u=>Sel.ids.has(u.id)&&u.hp>0);}
function selectedBlds(){return Game.buildings.filter(b=>Sel.ids.has(b.id)&&b.hp>0);}
function clearSel(){Sel.ids.clear();Sel.buildMode=null;Sel.atkMode=false;refreshPanel();}
function project(x,z,h){
const c=Game.cam;
const dx=x-c.x,dz=z-c.z;
const cy=Math.cos(c.yaw),sy=Math.sin(c.yaw);
const fwd=dx*sy+dz*cy;
const right=dx*cy-dz*sy;
const cp=Math.cos(c.pitch),sp=Math.sin(c.pitch);
const B=c.dist*cp, Hc=c.dist*sp;
const hh=h||0;
const zc=(fwd+B)*cp-(hh-Hc)*sp;
const yc=(fwd+B)*sp+(hh-Hc)*cp;
const zn=Math.max(7,zc);
const focal=H*1.15;
const sx=W/2+right*focal/zn;
const sy2=H*0.58-yc*focal/zn;
const s=focal/zn;
return{x:sx,y:clamp(sy2,-400,H+400),s:s,depth:zc};
}
function screenToGround(sx,sy){
const c=Game.cam;let best={x:c.x,z:c.z},bd=1e18;const R=c.dist*1.7;
for(let iter=0;iter<3;iter++){const range=iter===0?R:(iter===1?30:8);const cxp=iter===0?c.x:best.x,czp=iter===0?c.z:best.z;const steps=iter===0?22:12;
for(let j=-steps;j<=steps;j++)for(let i=-steps;i<=steps;i++){const x=cxp+i/steps*range,z=czp+j/steps*range;if(Math.abs(x)>WORLD/2+12||Math.abs(z)>WORLD/2+12)continue;const p=project(x,z,0);const dd=(p.x-sx)*(p.x-sx)+(p.y-sy)*(p.y-sy);if(dd<bd){bd=dd;best={x:x,z:z};}}}
return best;
}
let mouse={x:0,y:0,down:false,mdown:false,lx:0,ly:0};
canvas.addEventListener("contextmenu",e=>e.preventDefault());
canvas.addEventListener("mousedown",e=>{
AudioSys.init();
if(Game.state!=="play"||Game.paused)return;
if(e.button===1){mouse.mdown=true;mouse.lx=e.clientX;mouse.ly=e.clientY;e.preventDefault();return;}
if(e.button===2){handleRightClick(e.clientX,e.clientY,e.shiftKey);return;}
mouse.down=true;mouse.x=e.clientX;mouse.y=e.clientY;mouse.lx=e.clientX;mouse.ly=e.clientY;
Sel.drag={x0:e.clientX,y0:e.clientY,x1:e.clientX,y1:e.clientY};
});
canvas.addEventListener("mousemove",e=>{
mouse.x=e.clientX;mouse.y=e.clientY;
if(mouse.mdown){Game.cam.yaw+=(e.clientX-mouse.lx)*.006;Game.cam.pitch=clamp(Game.cam.pitch+(e.clientY-mouse.ly)*.004,.45,1.35);mouse.lx=e.clientX;mouse.ly=e.clientY;}
if(Sel.drag){Sel.drag.x1=e.clientX;Sel.drag.y1=e.clientY;}
});
canvas.addEventListener("mouseup",e=>{
if(e.button===1){mouse.mdown=false;return;}
if(e.button!==0)return;mouse.down=false;
if(Game.state!=="play"||Game.paused){Sel.drag=null;return;}
const d=Sel.drag;Sel.drag=null;if(!d)return;
const moved=Math.hypot(d.x1-d.x0,d.y1-d.y0);
if(Sel.buildMode){tryBuildAt(e.clientX,e.clientY,e.shiftKey);return;}
if(Sel.atkMode){orderAttackAt(e.clientX,e.clientY,e.shiftKey);Sel.atkMode=false;refreshPanel();return;}
if(Sel.rallyBld){setRallyAt(Sel.rallyBld,e.clientX,e.clientY);Sel.rallyBld=null;canvas.style.cursor="crosshair";return;}
if(moved<8)clickSelect(e.clientX,e.clientY,e.shiftKey);else boxSelect(d,e.shiftKey);
});
canvas.addEventListener("wheel",e=>{if(Game.state!=="play")return;Game.cam.dist=clamp(Game.cam.dist*(1+Math.sign(e.deltaY)*.1),30,220);e.preventDefault();},{passive:false});
canvas.addEventListener("dblclick",e=>{
if(Game.state!=="play"||Game.paused)return;
const t=entityAtScreen(e.clientX,e.clientY);
if(t&&t.kind==="unit"&&t.owner===0){Sel.ids.clear();for(const u of Game.units)if(u.owner===0&&u.type===t.type&&u.hp>0){const p=project(u.x,u.z,1);if(p.x>-50&&p.x<W+50&&p.y>-50&&p.y<H+50)Sel.ids.add(u.id);}refreshPanel();AudioSys.click();}
});
function entityAtScreen(sx,sy){
let best=null,bd=28*28;
const test=(e,px,py,r)=>{const dx=px-sx,dy=py-sy,dd=dx*dx+dy*dy;if(dd<r*r&&dd<bd){bd=dd;best=e;}};
for(const u of Game.units){if(u.hp<=0)continue;if(u.owner!==0&&!visibleToPlayer(u.x,u.z))continue;const p=project(u.x,u.z,u.fly?7:1);test(u,p.x,p.y,u.fly?17:15);}
for(const b of Game.buildings){if(b.hp<=0)continue;if(b.owner!==0&&!seenToPlayer(b.x,b.z)&&!b.everSeenBy0)continue;const p=project(b.x,b.z,3);test(b,p.x,p.y,clamp(p.s*BLD_DEFS[b.type].size*.6,20,80));}
for(const c of Game.crystals){if(c.amount<=0||!seenToPlayer(c.x,c.z))continue;const p=project(c.x,c.z,1);test({kind:"crys",crystal:c,x:c.x,z:c.z,owner:-1},p.x,p.y,15);}
return best;
}
function clickSelect(sx,sy,add){const e=entityAtScreen(sx,sy);if(!e){if(!add)clearSel();return;}if(e.kind==="crys")return;AudioSys.click();if(add){if(Sel.ids.has(e.id))Sel.ids.delete(e.id);else Sel.ids.add(e.id);}else{Sel.ids.clear();Sel.ids.add(e.id);}refreshPanel();}
function boxSelect(d,add){const x0=Math.min(d.x0,d.x1),x1=Math.max(d.x0,d.x1),y0=Math.min(d.y0,d.y1),y1=Math.max(d.y0,d.y1);const found=[];for(const u of Game.units){if(u.hp<=0||u.owner!==0)continue;if(!visibleToPlayer(u.x,u.z))continue;const p=project(u.x,u.z,1);if(p.x>=x0&&p.x<=x1&&p.y>=y0&&p.y<=y1)found.push(u);}if(!add)Sel.ids.clear();for(const u of found)Sel.ids.add(u.id);AudioSys.click();refreshPanel();}
function handleRightClick(sx,sy,shift){
const units=selectedUnits();
if(Sel.buildMode){Sel.buildMode=null;refreshPanel();return;}
if(Sel.rallyBld){Sel.rallyBld=null;canvas.style.cursor="crosshair";return;}
if(Sel.atkMode){orderAttackAt(sx,sy,shift);Sel.atkMode=false;refreshPanel();return;}
const target=entityAtScreen(sx,sy);const g=screenToGround(sx,sy);
if(selectedBlds().length&&!units.length){const b=selectedBlds()[0];if(b.owner===0&&(b.type==="barracks"||b.type==="factory"||b.type==="skyport"||b.type==="nexus")){b.rally={x:g.x,z:g.z};toast("Rally set");AudioSys.click();}return;}
if(!units.length)return;
if(target){
if(target.kind==="crys"){const ws=units.filter(u=>u.type==="mule");if(ws.length){issueOrder(ws,{mode:"harvest",crystal:target.crystal,shift:shift});hint("Mules harvesting ◆ flux");}return;}
if(target.owner!==0&&(target.kind==="unit"||target.kind==="bld")){for(const u of units)u.targetManual=true;issueOrder(units,{mode:"attack",target:target,shift:shift});hint("Attacking "+(target.kind==="bld"?BLD_DEFS[target.type].name:UNIT_DEFS[target.type].name));return;}
if(target.owner===0&&target.kind==="bld"&&BLD_DEFS[target.type].dropoff){const ws=units.filter(u=>u.type==="mule");if(ws.length){issueOrder(ws,{mode:"return",shift:shift});}return;}
if(target.kind==="bld"&&target.owner===0&&target.buildPct<1){const ws=units.filter(u=>u.type==="mule");if(ws.length){issueOrder(ws,{mode:"build",building:target,shift:shift});hint("Mules constructing");}return;}
}
const c=nearestCrystal(g.x,g.z,0);
if(c&&dist2(g.x,g.z,c.x,c.z)<49&&units.some(u=>u.type==="mule")){const ws=units.filter(u=>u.type==="mule");issueOrder(ws,{mode:"harvest",crystal:c,shift:shift});}
else issueOrder(units,{mode:"move",x:g.x,z:g.z,shift:shift});
}
function orderAttackAt(sx,sy,shift){const units=selectedUnits();if(!units.length)return;const t=entityAtScreen(sx,sy);const g=screenToGround(sx,sy);if(t&&(t.kind==="unit"||t.kind==="bld")&&t.owner!==0){for(const u of units)u.targetManual=true;issueOrder(units,{mode:"attack",target:t,shift:shift});}else issueOrder(units,{mode:"attackmove",x:g.x,z:g.z,shift:shift});hint("Attack-move: engages on contact");}
function tryBuildAt(sx,sy,keep){const g=screenToGround(sx,sy);const type=Sel.buildMode;if(P(0).flux<BLD_DEFS[type].cost){toast("Not enough flux","warn");return;}const b=placeBuilding(0,type,g.x,g.z);if(b){toast(BLD_DEFS[type].icon+" "+BLD_DEFS[type].name+" foundation placed — Mules auto-assigned","good");const ws=selectedUnits().filter(u=>u.type==="mule");if(ws.length)issueOrder(ws,{mode:"build",building:b,shift:false});else hint("Select a Mule → right-click foundation to build");if(!keep)Sel.buildMode=null;refreshPanel();}}
function setRallyAt(b,sx,sy){const g=screenToGround(sx,sy);b.rally={x:g.x,z:g.z};toast("Rally point set");AudioSys.click();refreshPanel();}
const keys={};
window.addEventListener("keydown",e=>{
if(e.key==="Escape"){if(!$("helpScr").classList.contains("hidden")){$("helpScr").classList.add("hidden");return;}if(!$("objScr").classList.contains("hidden")){$("objScr").classList.add("hidden");return;}Sel.buildMode=null;Sel.atkMode=false;Sel.rallyBld=null;canvas.style.cursor="crosshair";refreshPanel();return;}
if(Game.state!=="play")return;
keys[e.key.toLowerCase()]=true;
const k=e.key.toLowerCase();
if(k==="a"&&selectedUnits().length){Sel.atkMode=true;refreshPanel();hint("Attack-move: click target or ground");}
if(k==="s"&&selectedUnits().length)issueOrder(selectedUnits(),{mode:"stop"});
if(k==="h"&&selectedUnits().length){for(const u of selectedUnits()){u.hold=true;u.order="hold";u.tx=null;u.target=null;}refreshPanel();}
if(k===" "){e.preventDefault();const bb=Game.buildings.filter(b=>b.owner===0&&b.hitT>0);if(bb.length){Game.cam.x=bb[0].x;Game.cam.z=bb[0].z;}}
if(k>="1"&&k<="4"){if(e.ctrlKey||e.metaKey){Sel.groups[k]=[...Sel.ids];toast("Group "+k+" set ("+Sel.groups[k].length+")");e.preventDefault();}else if(Sel.groups[k]){Sel.ids=new Set(Sel.groups[k].filter(id=>Game.units.some(u=>u.id===id&&u.hp>0)||Game.buildings.some(b=>b.id===id&&b.hp>0)));refreshPanel();AudioSys.click();}}
if(!Sel.buildMode){const bm=BUILD_MENU.find(m=>m.key.toLowerCase()===k);if(bm&&selectedUnits().some(u=>u.type==="mule"))startBuild(bm.id);}
});
window.addEventListener("keyup",e=>{keys[e.key.toLowerCase()]=false;});
function hint(t){const h=$("hint");h.textContent=t;h.classList.remove("hidden");clearTimeout(h._t);h._t=setTimeout(()=>h.classList.add("hidden"),2600);}
/* UI panels */
function updateTop(){$("resFlux").querySelector("b").textContent=Math.floor(P(0).flux);$("supPill").querySelector("b").textContent=P(0).supplyUsed+"/"+P(0).supplyCap;$("supPill").style.borderColor=P(0).supplyUsed>=P(0).supplyCap?"var(--bad)":"";$("armyPill").querySelector("b").textContent=Game.units.filter(u=>u.owner===0&&u.type!=="mule").length;$("timer").textContent=fmtT(Game.time);const miners=Game.units.filter(u=>u.owner===0&&u.type==="mule"&&(u.order==="harvest"||u.carry>0)).length;$("incRate").textContent=miners?("+"+miners+"⛏"):"";}
function faceHTML(owner,type,isB){const col=owner===0?"linear-gradient(135deg,#0e3a52,#0a6d8f)":"linear-gradient(135deg,#52141a,#8f1f2a)";const ic=isB?BLD_DEFS[type].icon:UNIT_DEFS[type].icon;return '<div class="face" style="background:'+col+'"><div style="font-size:20px;line-height:36px">'+ic+'</div></div>';}
function refreshPanel(){
const units=selectedUnits(),blds=selectedBlds();
const title=$("selTitle"),grid=$("selGrid"),cmd=$("cmdGrid"),q=$("queueBar");
grid.innerHTML="";cmd.innerHTML="";q.innerHTML="";$("cmdTitle").textContent="COMMAND";
document.querySelectorAll(".ordBtn").forEach(b=>b.classList.remove("on"));
if(Sel.atkMode)$("oAtk").classList.add("on");
if(units.some(u=>u.hold))$("oHold").classList.add("on");
if(!units.length&&!blds.length){title.textContent="No selection — drag to box-select · right-click to order";cmd.innerHTML='<div style="font-size:11px;color:var(--dim);padding:6px">Select a <b>Mule 🛠</b> to build & harvest.<br><br>Goal: destroy all enemy <b>Nexus ◈</b>.</div>';return;}
if(blds.length&&(units.length===0||(blds.length===1&&units.length<3))){
const b=blds[0],d=BLD_DEFS[b.type];
title.innerHTML=(b.owner===0?"◈ ":"⬢ ")+d.name+' <span style="color:var(--dim)">HP '+Math.ceil(b.hp)+"/"+b.maxhp+(b.buildPct<1?" · 🚧 "+Math.floor(b.buildPct*100)+"%":"")+"</span>";
grid.innerHTML='<div class="unitChip">'+faceHTML(b.owner,b.type,true)+'<div class="hpbar"><i style="width:'+(b.hp/b.maxhp*100)+'%"></i></div></div><div style="font-size:11px;color:var(--dim);max-width:300px">'+d.desc+'</div>';
if(b.owner===0){
$("cmdTitle").textContent=d.name.toUpperCase()+" — PRODUCTION";
if(b.type==="nexus")addCmd("mule","Train Mule","60◆ · 1⬢ · 9s","T");
if(b.type==="barracks"){addCmd("blade","Blade 🔪","90◆ · 1⬢","Q");addCmd("lancer","Lancer 🔫","130◆ · 2⬢ air","W");}
if(b.type==="factory")addCmd("golem","Golem 🛡","260◆ · 4⬢ siege","Q");
if(b.type==="skyport")addCmd("wasp","Wasp 🐝","170◆ · 2⬢ flies","Q");
if(b.type==="spire"){for(const k in UPGRADES){const u=UPGRADES[k];const owned=P(0).upg[k];addCmd("upg:"+k,u.icon+" "+u.name,u.cost+"◆ · "+(owned?"DONE":(P(0).researching?"...":u.time+"s")),"",owned||!!P(0).researching,u.desc);}}
if(b.type==="nexus"||b.type==="barracks"||b.type==="factory"||b.type==="skyport")addCmd("rally","📍 Rally","gather point","",false,"New units move to rally.");
b.queue.forEach((qi,i)=>{const el=document.createElement("div");el.className="qItem";el.textContent=UNIT_DEFS[qi.type].name+" "+Math.floor(qi.t/qi.need*100)+"% ✕";el.title="Cancel (refund)";el.onclick=()=>{cancelQueue(b,i);refreshPanel();};q.appendChild(el);});
if(b.research){const el=document.createElement("div");el.className="qItem";el.textContent="🔬 "+UPGRADES[b.research].name+" "+Math.floor(b.researchT/b.researchNeed*100)+"%";q.appendChild(el);}
if(b.queue.length){const qi=b.queue[0];const el=document.createElement("div");el.style.cssText="width:100%;height:5px;background:#12202e;border-radius:3px;margin-top:6px";el.innerHTML='<div style="height:100%;width:'+(qi.t/qi.need*100)+'%;background:var(--cy);border-radius:3px"></div>';q.appendChild(el);}
}else{cmd.innerHTML='<div style="font-size:11px;color:#ff9a9a">Enemy '+d.name+' — HP '+Math.ceil(b.hp)+'. Destroy it.</div>';}
return;}
const counts={};for(const u of units)counts[u.type]=(counts[u.type]||0)+1;
title.innerHTML=Object.keys(counts).map(t=>counts[t]+"× "+UNIT_DEFS[t].name).join(" · ")+' <span style="color:var(--dim)">('+units.length+')</span>';
const byType={};for(const u of units)(byType[u.type]=byType[u.type]||[]).push(u);
for(const t in byType){const arr=byType[t];const el=document.createElement("div");el.className="unitChip";el.innerHTML=faceHTML(0,t,false)+"<div>"+arr.length+"× "+UNIT_DEFS[t].name+"</div>"+'<div class="hpbar"><i style="width:'+(arr.reduce((s,u)=>s+u.hp/u.maxhp,0)/arr.length*100)+'%"></i></div>';el.title=UNIT_DEFS[t].desc+" — double-click: select all of type";grid.appendChild(el);}
const ft=Object.keys(counts)[0],fd=UNIT_DEFS[ft];const fu=units.find(u=>u.type===ft);
const info=document.createElement("div");info.style.cssText="font-size:11px;color:var(--dim);width:100%";info.innerHTML="<b style='color:var(--txt)'>"+fd.icon+" "+fd.name+"</b> — "+fd.desc+" · HP "+Math.ceil(fu.hp)+"/"+fd.hp+" · "+counts[ft]+" selected";grid.appendChild(info);
if(units.some(u=>u.type==="mule")){$("cmdTitle").textContent="BUILD — click to place · right-click cancels";for(const m of BUILD_MENU){const d=BLD_DEFS[m.id];const afford=P(0).flux>=d.cost;addCmd("bld:"+m.id,d.icon+" "+d.name,d.cost+"◆ · "+d.time+"s",m.key,!afford,d.desc+(afford?"":" — need "+d.cost+"◆"));}}
if(Sel.buildMode)$("cmdTitle").textContent="PLACING "+BLD_DEFS[Sel.buildMode].name.toUpperCase()+" — click ground · right-click cancels";
}
function addCmd(id,label,sub,key,off,tip){const g=$("cmdGrid");const el=document.createElement("div");el.className="cmdBtn"+(off?" off":"")+(id.indexOf("bld:")===0?" building":"");el.innerHTML=(key?'<span class="key">'+key+'</span>':"")+label+"<small>"+sub+"</small>";el.title=tip||label;el.onclick=()=>doCmd(id);g.appendChild(el);}
function doCmd(id){AudioSys.init();if(id==="rally"){const b=selectedBlds()[0];if(b){Sel.rallyBld=b;canvas.style.cursor="copy";hint("Click ground to set rally point");}return;}if(id.indexOf("bld:")===0){startBuild(id.slice(4));return;}if(id.indexOf("upg:")===0){const b=selectedBlds()[0];if(b)tryResearch(b,id.slice(4));refreshPanel();return;}const b=selectedBlds()[0];if(b){tryQueueUnit(b,id);refreshPanel();}}
function startBuild(type){if(P(0).flux<BLD_DEFS[type].cost){toast("Need "+BLD_DEFS[type].cost+"◆ for "+BLD_DEFS[type].name,"warn");return;}Sel.buildMode=type;Sel.atkMode=false;hint("Placing "+BLD_DEFS[type].name+" — right-click cancels");AudioSys.click();refreshPanel();}
$("oAtk").onclick=()=>{if(selectedUnits().length){Sel.atkMode=!Sel.atkMode;refreshPanel();}};
$("oStop").onclick=()=>{const u=selectedUnits();if(u.length)issueOrder(u,{mode:"stop"});};
$("oHold").onclick=()=>{for(const u of selectedUnits()){u.hold=true;u.order="hold";u.tx=null;u.target=null;}refreshPanel();};
/* renderer */
function shade(hex,f){const n=parseInt(hex.slice(1),16);let r=(n>>16)&255,g=(n>>8)&255,b=n&255;r=clamp(r*f|0,0,255);g=clamp(g*f|0,0,255);b=clamp(b*f|0,0,255);return "rgb("+r+","+g+","+b+")";}
function drawGround(){
const sky=ctx.createLinearGradient(0,0,0,H);
sky.addColorStop(0,"#030710");sky.addColorStop(.35,"#0a1626");sky.addColorStop(.62,"#0d1a2b");sky.addColorStop(1,"#050a12");
ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
ctx.fillStyle="rgba(255,255,255,.5)";
for(let i=0;i<60;i++){ctx.fillRect((i*197.3)%W,(i*131.7)%(H*.28),1.4,1.4);}
const yb=H*.30;
ctx.fillStyle="rgba(70,120,170,.25)";ctx.fillRect(0,yb-46,W,46);
for(let L=0;L<2;L++){ctx.fillStyle=L?"rgba(30,60,110,.8)":"rgba(18,36,70,.9)";ctx.beginPath();ctx.moveTo(0,yb+20);for(let x=0;x<=W;x+=40){const h=Math.sin((x+L*300)*.01)*(L?46:70)+Math.sin(x*.05+L)*8;ctx.lineTo(x,yb-h);}ctx.lineTo(W,yb+20);ctx.closePath();ctx.fill();}
// outer terrain: flat fill everywhere below sky; map cells drawn on top
ctx.fillStyle="#101c30";ctx.fillRect(0,0,W,H);
const n=26,half=WORLD/2;
for(let j=0;j<n;j++)for(let i=0;i<n;i++){
const x0=-half+i/n*WORLD,x1=-half+(i+1)/n*WORLD,z0=-half+j/n*WORLD,z1=-half+(j+1)/n*WORLD;
const p00=project(x0,z0,0),p10=project(x1,z0,0),p11=project(x1,z1,0),p01=project(x0,z1,0);
if(Math.min(p00.x,p10.x,p11.x,p01.x)>W||Math.max(p00.x,p10.x,p11.x,p01.x)<0)continue;
const cx=(x0+x1)/2,cz=(z0+z1)/2;const fi=w2f(cz)*Game.fogN+w2f(cx);
const vis=Game.fogVis[fi],seen=Game.fogSeen[fi];let col;const checker=(i+j)%2===0;
if(!seen)col=checker?"#0a1424":"#0b1626";else if(!vis)col=checker?"#0a1220":"#0b1422";
else{const dC=Math.sqrt(cx*cx+cz*cz)/half;const base=checker?[24,44,58]:[26,48,64];const warm=Math.max(0,1-dC*1.4)*22;col="rgb("+(base[0]+warm|0)+","+(base[1]+warm*.7|0)+","+(base[2]|0)+")";}
ctx.fillStyle=col;ctx.beginPath();ctx.moveTo(p00.x,p00.y);ctx.lineTo(p10.x,p10.y);ctx.lineTo(p11.x,p11.y);ctx.lineTo(p01.x,p01.y);ctx.closePath();ctx.fill();
}
ctx.strokeStyle="rgba(53,224,255,.08)";ctx.lineWidth=1;
for(let g=-half;g<=half;g+=10){
 // segment-by-segment so grid only glows in explored territory
 for(let sgm=-half;sgm<half;sgm+=10){
  if(seenToPlayer(g,sgm+5)&&seenToPlayer(g,sgm+5)){const a=project(g,sgm,0),b=project(g,sgm+10,0);ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();}
  if(seenToPlayer(sgm+5,g)){const c=project(sgm,g,0),d=project(sgm+10,g,0);ctx.beginPath();ctx.moveTo(c.x,c.y);ctx.lineTo(d.x,d.y);ctx.stroke();}
 }
}
for(const o of Game.obstacles){const fi=w2f(o.z)*Game.fogN+w2f(o.x);if(!Game.fogSeen[fi])continue;const vis=visibleToPlayer(o.x,o.z);drawBox(o.x,o.z,0,o.w,7,o.h,vis?"#3a2f28":"#14161f",vis?"#6b5a44":"#232838",vis);}
for(const c of Game.crystals){if(!seenToPlayer(c.x,c.z))continue;drawCrystal(c,visibleToPlayer(c.x,c.z));}
}
function drawBox(x,z,h,w,hh,d,cTop,cSide,lit){
const p0=project(x-w/2,z-d/2,h),p1=project(x+w/2,z-d/2,h),p2=project(x+w/2,z+d/2,h),p3=project(x-w/2,z+d/2,h);
const q0=project(x-w/2,z-d/2,h+hh),q1=project(x+w/2,z-d/2,h+hh),q2=project(x+w/2,z+d/2,h+hh),q3=project(x-w/2,z+d/2,h+hh);
ctx.fillStyle=shade(cSide,.55);ctx.beginPath();ctx.moveTo(p2.x,p2.y);ctx.lineTo(p3.x,p3.y);ctx.lineTo(q3.x,q3.y);ctx.lineTo(q2.x,q2.y);ctx.closePath();ctx.fill();
ctx.fillStyle=shade(cSide,.85);ctx.beginPath();ctx.moveTo(p1.x,p1.y);ctx.lineTo(p2.x,p2.y);ctx.lineTo(q2.x,q2.y);ctx.lineTo(q1.x,q1.y);ctx.closePath();ctx.fill();
ctx.fillStyle=lit?cTop:shade(cTop,.6);ctx.beginPath();ctx.moveTo(q0.x,q0.y);ctx.lineTo(q1.x,q1.y);ctx.lineTo(q2.x,q2.y);ctx.lineTo(q3.x,q3.y);ctx.closePath();ctx.fill();
}
function drawCrystal(c,lit){
const p=project(c.x,c.z,0);const s=clamp(p.s*1.6,3,26);const frac=c.amount/c.max;
ctx.save();ctx.globalAlpha=lit?1:.55;
if(lit){const g=ctx.createRadialGradient(p.x,p.y,1,p.x,p.y,s*2.4);g.addColorStop(0,"rgba(255,215,94,.5)");g.addColorStop(1,"rgba(255,215,94,0)");ctx.fillStyle=g;ctx.beginPath();ctx.arc(p.x,p.y,s*2.4,0,TAU);ctx.fill();}
for(let k=0;k<3;k++){const ox=(k-1)*s*.55,hh=s*(0.8+frac*1.6)*(1-k*.22);ctx.fillStyle=k===1?"#fff3c4":(lit?"#ffd75e":"#4a4436");ctx.beginPath();ctx.moveTo(p.x+ox-s*.32,p.y);ctx.lineTo(p.x+ox,p.y-hh);ctx.lineTo(p.x+ox+s*.32,p.y);ctx.closePath();ctx.fill();}
ctx.restore();
}
function drawEntities(){
const items=[];
for(const b of Game.buildings){if(b.hp<=0)continue;if(b.owner!==0&&!seenToPlayer(b.x,b.z)&&!b.everSeenBy0)continue;items.push({d:(b.x-Game.cam.x)*Math.sin(-Game.cam.yaw)+(b.z-Game.cam.z)*Math.cos(-Game.cam.yaw),e:b});}
for(const u of Game.units){if(u.hp<=0)continue;if(u.owner!==0&&!visibleToPlayer(u.x,u.z))continue;items.push({d:(u.x-Game.cam.x)*Math.sin(-Game.cam.yaw)+(u.z-Game.cam.z)*Math.cos(-Game.cam.yaw),e:u});}
items.sort((a,b)=>b.d-a.d);
for(const it of items){if(it.e.kind==="bld")drawBuilding(it.e);else drawUnit(it.e);}
for(const p of Game.projectiles){const a=project(p.x,p.z,p.h||2);ctx.fillStyle=p.color;ctx.beginPath();ctx.arc(a.x,a.y,clamp(a.s*.5,1.5,5),0,TAU);ctx.fill();}
for(const q of Game.particles){const a=project(q.x,q.z,Math.max(0,q.h));ctx.globalAlpha=clamp(1-q.t/q.life,0,1);if(q.ring){ctx.strokeStyle=q.color;ctx.lineWidth=2;ctx.beginPath();ctx.arc(a.x,a.y,q.t*40+6,0,TAU);ctx.stroke();}else{ctx.fillStyle=q.color;ctx.beginPath();ctx.arc(a.x,a.y,clamp(a.s*q.size*.4,1,7),0,TAU);ctx.fill();}ctx.globalAlpha=1;}
ctx.textAlign="center";ctx.font="bold 13px sans-serif";
for(const f of Game.floaters){const a=project(f.x,f.z,10-f.t*6);ctx.globalAlpha=1-f.t/f.life;ctx.fillStyle="#000";ctx.fillText(f.txt,a.x+1,a.y+1);ctx.fillStyle=f.color;ctx.fillText(f.txt,a.x,a.y);ctx.globalAlpha=1;}
for(const id of Sel.ids){
const u=Game.units.find(u=>u.id===id),b=Game.buildings.find(b=>b.id===id);const e=u||b;if(!e||e.hp<=0)continue;
const h=e.kind==="bld"?6:(e.fly?7:1);const p=project(e.x,e.z,h);
ctx.strokeStyle="#5df2ff";ctx.lineWidth=2;
const r=e.kind==="bld"?clamp(p.s*BLD_DEFS[e.type].size*.6,20,80):clamp(p.s*2.2,10,26);
ctx.beginPath();ctx.ellipse(p.x,p.y,r,r*.45,0,0,TAU);ctx.stroke();
const w=Math.min(64,r*1.6);
ctx.fillStyle="rgba(0,0,0,.6)";ctx.fillRect(p.x-w/2,p.y-r*.55-12,w,5);
ctx.fillStyle=e.hp/e.maxhp>.5?"#37ff9b":e.hp/e.maxhp>.25?"#ffd75e":"#ff5468";
ctx.fillRect(p.x-w/2,p.y-r*.55-12,w*clamp(e.hp/e.maxhp,0,1),5);
if(e.carry>0){ctx.fillStyle="#ffd75e";ctx.font="10px sans-serif";ctx.fillText("◆"+e.carry,p.x,p.y-r*.55-16);}
}
ctx.strokeStyle="rgba(93,242,255,.5)";ctx.setLineDash([4,4]);
for(const id of Sel.ids){const u=Game.units.find(u=>u.id===id);if(!u||u.tx===null||u.tx===undefined)continue;const a=project(u.x,u.z,1),b=project(u.tx,u.tz,1);ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();}
ctx.setLineDash([]);
if(Sel.buildMode&&Game.state==="play"){const g=screenToGround(mouse.x,mouse.y);const chk=canPlace(0,Sel.buildMode,g.x,g.z);const p=project(g.x,g.z,0);ctx.globalAlpha=.65;const d=BLD_DEFS[Sel.buildMode];drawBox(g.x,g.z,0,d.size,4,d.size,chk.ok?"#2affa0":"#ff5468",chk.ok?"#1a7a55":"#7a1a2a",true);ctx.globalAlpha=1;ctx.fillStyle=chk.ok?"#5dffa0":"#ff8090";ctx.font="bold 12px sans-serif";ctx.fillText(chk.ok?d.name+" — click to place":"✕ "+chk.why,p.x,p.y-70);}
if(Sel.drag){const d=Sel.drag;ctx.strokeStyle="#5df2ff";ctx.lineWidth=1.5;ctx.fillStyle="rgba(53,224,255,.12)";ctx.fillRect(Math.min(d.x0,d.x1),Math.min(d.y0,d.y1),Math.abs(d.x1-d.x0),Math.abs(d.y1-d.y0));ctx.strokeRect(Math.min(d.x0,d.x1),Math.min(d.y0,d.y1),Math.abs(d.x1-d.x0),Math.abs(d.y1-d.y0));}
}
function drawUnit(u){
const h=u.fly?7+Math.sin(Game.time*3+u.id)*.8:0;
const p=project(u.x,u.z,h);const s=clamp(p.s,2,30);
const friendly=u.owner===0;const bob=u.fly?0:Math.abs(Math.sin(u.moveT*10))*s*.12;
if(u.fly){const g=project(u.x,u.z,0);ctx.fillStyle="rgba(0,0,0,.4)";ctx.beginPath();ctx.ellipse(g.x,g.y,s*.9,s*.4,0,0,TAU);ctx.fill();}
const flash=u.hitT>0;const C=flash?"#ffffff":(friendly?"#1fa8cf":"#cf3a2e"),D=flash?"#ffffff":(friendly?"#0b3d52":"#521014");
ctx.save();ctx.translate(p.x,p.y-bob);
const sc=Math.max(.35,s/10);ctx.scale(sc,sc);
if(!u.fly){ctx.fillStyle="rgba(0,0,0,.45)";ctx.beginPath();ctx.ellipse(0,3,7,3.2,0,0,TAU);ctx.fill();}
if(u.type==="mule"){ctx.fillStyle=D;ctx.fillRect(-4,-3,8,7);ctx.fillStyle=C;ctx.fillRect(-3,-6,6,4);ctx.fillStyle="#ffd75e";ctx.fillRect(-2,-5,4,2);if(u.carry>0){ctx.fillStyle="#ffd75e";ctx.fillRect(-5,-10,10,3);}ctx.fillStyle="#04141c";ctx.fillRect(-6,2,4,3);ctx.fillRect(2,2,4,3);}
else if(u.type==="blade"){ctx.fillStyle=D;ctx.beginPath();ctx.arc(0,0,6,0,TAU);ctx.fill();ctx.fillStyle=C;ctx.beginPath();ctx.arc(0,0,4,0,TAU);ctx.fill();const sw=Math.sin(u.swing*6)*2;ctx.strokeStyle=friendly?"#aef7ff":"#ffb0a0";ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(-6,-2+sw);ctx.lineTo(-10,-6+sw);ctx.moveTo(6,-2-sw);ctx.lineTo(10,-6-sw);ctx.stroke();ctx.fillStyle="#fff";ctx.fillRect(-1,-1,2,2);}
else if(u.type==="lancer"){ctx.fillStyle=D;ctx.fillRect(-4,-5,8,9);ctx.fillStyle=C;ctx.fillRect(-3,-8,6,4);ctx.strokeStyle="#0a0a0a";ctx.lineWidth=2.5;ctx.beginPath();ctx.moveTo(0,-6);ctx.lineTo(0,-13);ctx.stroke();ctx.fillStyle="#fff";ctx.fillRect(-1,-9,2,2);}
else if(u.type==="golem"){ctx.fillStyle=D;ctx.fillRect(-7,-6,14,12);ctx.fillStyle=C;ctx.fillRect(-5,-9,10,5);ctx.fillStyle="#0a0a0a";ctx.fillRect(-9,-4,3,8);ctx.fillRect(6,-4,3,8);ctx.fillStyle=friendly?"#ffd75e":"#ff9a5e";ctx.fillRect(-2,-8,4,2);ctx.strokeStyle="#111";ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(0,4);ctx.lineTo(0,10);ctx.stroke();}
else if(u.type==="wasp"){ctx.fillStyle="rgba(255,255,255,.25)";ctx.beginPath();ctx.ellipse(-8,0,6,2.5,-.4,0,TAU);ctx.ellipse(8,0,6,2.5,.4,0,TAU);ctx.fill();ctx.fillStyle=D;ctx.beginPath();ctx.ellipse(0,0,4,6,0,0,TAU);ctx.fill();ctx.fillStyle=C;ctx.beginPath();ctx.arc(0,-3,2.5,0,TAU);ctx.fill();ctx.fillStyle="#fff";ctx.fillRect(-1,0,2,4);}
ctx.restore();
if(u.hp<u.maxhp){const w=clamp(s*3,14,40);ctx.fillStyle="rgba(0,0,0,.6)";ctx.fillRect(p.x-w/2,p.y-s*1.1-8,w,3.5);ctx.fillStyle=u.owner===0?"#37ff9b":"#ff8090";ctx.fillRect(p.x-w/2,p.y-s*1.1-8,w*clamp(u.hp/u.maxhp,0,1),3.5);}
}
function drawBuilding(b){
const d=BLD_DEFS[b.type];const friendly=b.owner===0;
const vis=b.owner===0?true:visibleToPlayer(b.x,b.z);
const top=b.buildPct<1?"#5a5a5a":(friendly?"#1899c2":"#b03028");
const side=b.buildPct<1?"#333":(friendly?"#0e4a63":"#5e1611");
const hgt={nexus:9,pylon:10,depot:4,barracks:6,factory:7,skyport:6,spire:12,cannon:5}[b.type]||6;
drawBox(b.x,b.z,0,d.size,hgt*b.buildPct+(b.buildPct<1?1:0),d.size,top,side,vis);
const p=project(b.x,b.z,hgt+2);
const s=clamp(p.s,2,30);
ctx.save();ctx.translate(p.x,p.y);const sc=Math.max(.4,s/9);ctx.scale(sc,sc);
ctx.font="bold 11px sans-serif";ctx.textAlign="center";
ctx.fillStyle=b.buildPct<1?"#ffd75e":(friendly?"#bff3ff":"#ffc4bc");
ctx.fillText(b.buildPct<1?("🚧 "+Math.floor(b.buildPct*100)+"%"):d.icon,0,0);
if(b.type==="pylon"&&b.buildPct>=1){ctx.strokeStyle=friendly?"rgba(53,224,255,.7)":"rgba(255,90,90,.7)";ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(0,-2);ctx.lineTo(0,-14);ctx.stroke();ctx.fillStyle=friendly?"#aef7ff":"#ffb0a0";ctx.beginPath();ctx.arc(0,-16,3+Math.sin(Game.time*4)*1,0,TAU);ctx.fill();}
if(b.type==="cannon"&&b.buildPct>=1){let a=Game.time*1.5;if(b.target)a=Math.atan2(b.target.x-b.x,b.target.z-b.z);ctx.strokeStyle="#111";ctx.lineWidth=3;ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(Math.sin(a)*10,Math.cos(a)*10-4);ctx.stroke();}
if(b.type==="spire"&&b.buildPct>=1){ctx.fillStyle="rgba(180,140,255,.8)";ctx.beginPath();ctx.arc(0,-8,2.5+Math.sin(Game.time*3)*1,0,TAU);ctx.fill();}
ctx.restore();
if(b.hp<b.maxhp){const w=clamp(s*d.size*1.2,24,70);ctx.fillStyle="rgba(0,0,0,.6)";ctx.fillRect(p.x-w/2,p.y+8,w,4);ctx.fillStyle=b.owner===0?"#37ff9b":"#ff8090";ctx.fillRect(p.x-w/2,p.y+8,w*clamp(b.hp/b.maxhp,0,1),4);}
if(b.queue.length&&b.owner===0){ctx.fillStyle="#5df2ff";ctx.font="9px sans-serif";ctx.textAlign="center";ctx.fillText("⏳"+b.queue.length,p.x,p.y+20);}
}
/* minimap */
function drawMinimap(){
const w=mm.width,h=mm.height;mmc.clearRect(0,0,w,h);
mmc.fillStyle="#02040a";mmc.fillRect(0,0,w,h);
const fx=x=>(x+WORLD/2)/WORLD*w,fz=z=>(z+WORLD/2)/WORLD*h;
const n=Game.fogN,cell=w/n;
for(let j=0;j<n;j++)for(let i=0;i<n;i++){
const seen=Game.fogSeen[j*n+i],vis=Game.fogVis[j*n+i];
if(!seen)continue;
mmc.fillStyle=vis?"rgba(40,90,120,.9)":"rgba(16,30,46,.9)";
mmc.fillRect(i*cell,j*cell,cell+0.5,cell+0.5);
}
mmc.fillStyle="#ffd75e";
for(const c of Game.crystals){if(!seenToPlayer(c.x,c.z))continue;mmc.fillRect(fx(c.x)-1.5,fz(c.z)-1.5,3,3);}
for(const b of Game.buildings){
let show=false;
if(b.owner===0)show=true;else if(visibleToPlayer(b.x,b.z))show=true;else if(b.everSeenBy0&&seenToPlayer(b.x,b.z))show=true;
if(!show)continue;
const vis=b.owner===0?true:visibleToPlayer(b.x,b.z);
mmc.fillStyle=b.owner===0?(b.type==="nexus"?"#35e0ff":"#2a9ec4"):(vis?(b.type==="nexus"?"#ff2a3a":"#c44"):"#7a3a3a");
const s=b.type==="nexus"?6:4;
mmc.fillRect(fx(b.x)-s/2,fz(b.z)-s/2,s,s);
}
for(const u of Game.units){
if(u.owner===0){mmc.fillStyle=u.type==="mule"?"#7fd4ff":"#35ff9b";mmc.fillRect(fx(u.x)-1,fz(u.z)-1,2,2);}
else if(visibleToPlayer(u.x,u.z)){mmc.fillStyle="#ff4a5a";mmc.fillRect(fx(u.x)-1,fz(u.z)-1,2.4,2.4);}
}
// camera footprint
const corners=[screenToGround(0,0),screenToGround(W,0),screenToGround(W,H),screenToGround(0,H)];
mmc.strokeStyle="rgba(255,255,255,.8)";mmc.lineWidth=1;mmc.beginPath();
corners.forEach((c,i)=>{const X=clamp(fx(c.x),0,w),Y=clamp(fz(c.z),0,h);if(i===0)mmc.moveTo(X,Y);else mmc.lineTo(X,Y);});
mmc.closePath();mmc.stroke();
}
function mmJump(e,order){
const r=mm.getBoundingClientRect();
const x=((e.clientX-r.left)/r.width)*WORLD-WORLD/2,z=((e.clientY-r.top)/r.height)*WORLD-WORLD/2;
if(order){const units=selectedUnits();if(units.length){if(entityNearEnemy(x,z))issueOrder(units,{mode:"attackmove",x:x,z:z,shift:e.shiftKey});else issueOrder(units,{mode:"move",x:x,z:z,shift:e.shiftKey});}}
else{Game.cam.x=clamp(x,-WORLD/2,WORLD/2);Game.cam.z=clamp(z,-WORLD/2,WORLD/2);}
}
function entityNearEnemy(x,z){for(const u of Game.units)if(u.owner===1&&visibleToPlayer(u.x,u.z)&&dist2(u.x,u.z,x,z)<100)return true;return false;}
mm.addEventListener("mousedown",e=>{AudioSys.init();if(Game.state!=="play")return;if(e.button===2)mmJump(e,true);else mmJump(e,false);});
mm.addEventListener("contextmenu",e=>e.preventDefault());
/* camera keys + edge */
function cameraTick(dt){
const sp=Game.cam.dist*0.9*dt;let mx=0,mz=0;
if(keys["w"]||keys["arrowup"])mz-=1;if(keys["arrowdown"])mz+=1;
if(keys["a"]||keys["arrowleft"])mx-=1;if(keys["d"]||keys["arrowright"])mx+=1;
if(keys["q"])Game.cam.yaw-=1.6*dt;if(keys["e"])Game.cam.yaw+=1.6*dt;
if(mouse.x<8)mx-=1;if(mouse.x>W-8)mx+=1;if(mouse.y<8)mz-=1;if(mouse.y>H-140&&mouse.y<H-40){}else if(mouse.y>H-8)mz+=1;
if(mx||mz){const c=Math.cos(-Game.cam.yaw),s=Math.sin(-Game.cam.yaw);const l=Math.hypot(mx,mz);mx/=l;mz/=l;Game.cam.x=clamp(Game.cam.x+(mx*c-mz*s)*sp,-WORLD/2,WORLD/2);Game.cam.z=clamp(Game.cam.z+(mx*s+mz*c)*sp,-WORLD/2,WORLD/2);}
}
/* main loop */
let lastT=performance.now(),fpsA=60;
function frame(now){
requestAnimationFrame(frame);
let dt=(now-lastT)/1000;lastT=now;
if(dt>0.1)dt=0.1;
fpsA=fpsA*0.95+(1/Math.max(dt,1e-4))*0.05;
if(Game.state==="play"&&!Game.paused&&!Game.over){cameraTick(dt);simulate(dt);}
if(Game.state!=="title"){drawGround();drawEntities();drawMinimap();}
else{drawTitleBG(now/1000);}
$("fpsTag").textContent=Math.round(fpsA)+" fps · "+Game.units.length+" units · "+fmtT(Game.time);
}
function drawTitleBG(t){
const sky=ctx.createLinearGradient(0,0,0,H);sky.addColorStop(0,"#040814");sky.addColorStop(.6,"#0b1a30");sky.addColorStop(1,"#060b14");
ctx.fillStyle=sky;ctx.fillRect(0,0,W,H);
Game.cam.x=Math.sin(t*.05)*40;Game.cam.z=Math.cos(t*.04)*40;Game.cam.yaw=t*.05;
drawGround();
}
/* setup / restart */
function startMatch(){
Game.units.length=0;Game.buildings.length=0;Game.projectiles.length=0;Game.particles.length=0;Game.floaters.length=0;
Game.players=[newPlayer("Vanguard","#35e0ff","#0b3d52",false),newPlayer("Combine","#ff5d5e","#521014",true)];
Game.nextId=1;Game.time=0;Game.over=false;Game.paused=false;Game.state="title";
Game.stats={kills:0,losses:0,harvested:0};
Sel.ids.clear();Sel.buildMode=null;Sel.atkMode=false;Sel.groups={};
AI.atkTimer=150;AI.raidTimer=70;aiScoutT=20;
genMap();fogInit();
P(0).flux=400;P(1).flux=400;P(0).supplyCap=0;P(1).supplyCap=0;
const b0=basePos(0),b1=basePos(1);
addBuilding(0,"nexus",b0.x,b0.z,true);
addBuilding(1,"nexus",b1.x,b1.z,true);
fogUpdate();
for(let i=0;i<4;i++){const m=addUnit(0,"mule",b0.x+rand(-2,2),b0.z+12+i*2.5);const c=nearestCrystal(m.x,m.z,-1);if(c){m.harv=c;m.order="harvest";m.tx=c.x+rand(-1.5,1.5);m.tz=c.z+rand(-1.5,1.5);}else{m.order="harvest";m.tx=b0.x+rand(-6,6);m.tz=b0.z+rand(6,10);}}
for(let i=0;i<4;i++){const m=addUnit(1,"mule",b1.x+rand(-2,2),b1.z-12-i*2.5);const c=nearestCrystal(m.x,m.z,-1);if(c){m.harv=c;m.order="harvest";m.tx=c.x+rand(-1.5,1.5);m.tz=c.z+rand(-1.5,1.5);}else{m.order="harvest";m.tx=b1.x+rand(-6,6);m.tz=b1.z+rand(-10,-6);}}
Game.cam.x=b0.x;Game.cam.z=b0.z;Game.cam.yaw=.7;Game.cam.pitch=.9;Game.cam.dist=70;
fogUpdate();
Game.state="play";
$("titleScr").classList.add("hidden");$("endScr").classList.add("hidden");$("pauseScr").classList.add("hidden");
updateTop();refreshPanel();
toast("◈ Expedition deployed — harvest ◆, build ⬢ Pylons, scout!","good");
setTimeout(()=>{if(Game.state==="play")toast("❓ Enemy base location unknown — scout the vale", "warn");},6000);
setTimeout(()=>{if(Game.state==="play")toast("◉ Objective: destroy all enemy Nexus · defend yours", "");},12000);
hint("Select Mules → build Pylons (P) & Barracks (B) · right-click crystals to harvest");
}
$("startBtn").onclick=()=>{AudioSys.init();startMatch();};
$("restartBtn1").onclick=()=>{startMatch();};
$("restartBtn2").onclick=()=>{startMatch();};
$("resumeBtn").onclick=()=>{Game.paused=false;$("pauseScr").classList.add("hidden");};
$("pauseBtn").onclick=()=>{if(Game.state!=="play")return;Game.paused=!Game.paused;$("pauseScr").classList.toggle("hidden",!Game.paused);$("pauseBtn").textContent=Game.paused?"▶":"⏸";};
$("muteBtn").onclick=e=>{AudioSys.muted=!AudioSys.muted;e.target.textContent=AudioSys.muted?"🔇":"🔊";};
$("helpBtn").onclick=()=>{$("helpScr").classList.remove("hidden");};
$("helpBtn2").onclick=()=>{$("helpScr").classList.remove("hidden");};
$("helpClose").onclick=()=>{$("helpScr").classList.add("hidden");};
$("objBtn").onclick=()=>{$("objScr").classList.remove("hidden");};
$("objClose").onclick=()=>{$("objScr").classList.add("hidden");};
/* boot: title-screen backdrop skirmish preview */
Game.players=[newPlayer("Vanguard","#35e0ff","#0b3d52",false),newPlayer("Combine","#ff5d5e","#521014",true)];
genMap();fogInit();
Game.fogSeen.fill(1);Game.fogVis.fill(1);
requestAnimationFrame(frame);
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.3-contributor","provider":"openrouter","persona":"full","sessionId":"84661a16-9605-401e-b924-5fc013f4b0a1","tokensIn":26118429,"tokensOut":97425,"tokensTotal":26215854,"cost":0.3412737979999999,"turns":139,"toolCalls":138,"failedToolCalls":2,"timestamp":"2026-09-29T21:55:12.702Z"} -->