← RTS results

RTS

GPT 6.1 Sol · 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 43,732 bytes · SHA-256 74d58f3d8504
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Frontier War · Meridian Expedition</title><style>
*{box-sizing:border-box}body{margin:0;background:#050b13;color:#cddde7;font-family:system-ui,sans-serif;overflow:hidden}canvas{display:block}#world{width:100vw;height:100vh;cursor:crosshair}button{font:inherit;color:inherit;border:1px solid #345566;background:#122735;cursor:pointer;padding:9px 12px;border-radius:3px}button:hover{background:#244654;border-color:#61e5cf}button:disabled{opacity:.4;cursor:not-allowed}.hud{position:absolute;pointer-events:none;inset:0}.hud>*{pointer-events:auto}.top{position:absolute;top:16px;left:20px;right:20px;display:flex;justify-content:space-between;align-items:center}.brand{letter-spacing:3px;font-size:17px;font-weight:800}.brand small{display:block;font-size:9px;color:#7b9aaf;letter-spacing:2px;margin-top:3px}.stats{display:flex;gap:25px;background:#081521e8;padding:12px 22px;border:1px solid #294351}.stats b{color:#6cf5d1;font-size:20px}.stats small{font-size:10px;letter-spacing:1px;display:block;color:#7b9aaf}.bottom{position:absolute;bottom:18px;left:20px;right:20px;display:flex;gap:12px;align-items:stretch;height:190px}.panel{background:#081521ee;border:1px solid #2a4554;box-shadow:0 8px 30px #0005;padding:14px;border-radius:4px}#minimap{width:208px;height:160px;cursor:pointer;background:#030910}.miniWrap{padding:8px;flex-shrink:0}.miniWrap span{font-size:9px;letter-spacing:2px;display:block;margin-bottom:4px;color:#7fa7b8}#selection{width:260px;overflow:auto}h3{font-size:13px;letter-spacing:1px;margin:0 0 9px;color:#ecfcff}p{font-size:12px;line-height:1.7;color:#9bb3c2;margin:6px 0}#actions{flex:1;overflow:auto}#buttons{display:flex;gap:6px;flex-wrap:wrap}#buttons button{font-size:11px;padding:7px 9px;min-width:93px;text-align:left}#buttons small{display:block;color:#8aaebc;font-size:9px;margin-top:4px}.queue{display:flex;gap:5px;margin-top:8px}.queue button{font-size:10px;padding:5px}#notice{position:absolute;top:86px;left:50%;transform:translateX(-50%);background:#091b27d9;padding:9px 18px;border-left:3px solid #69eacb;font-size:12px;max-width:70%;text-align:center}#drag{position:absolute;border:1px solid #79ffdb;background:#63eacb18;pointer-events:none;display:none}#overlay{position:absolute;inset:0;background:linear-gradient(100deg,#050d19f2,#091927d4);display:flex;align-items:center;justify-content:center;z-index:10}.intro{width:790px;padding:45px;border:1px solid #365360;background:#091421e8;box-shadow:0 25px 100px #0008}.eyebrow{font-size:11px;color:#6ceccc;letter-spacing:4px}.intro h1{font-size:58px;letter-spacing:5px;margin:14px 0 4px;line-height:1}.intro h2{font-size:16px;color:#93b4c4;font-weight:400;letter-spacing:3px}.intro .columns{display:grid;grid-template-columns:1fr 1fr;gap:25px;margin:24px 0}.intro strong{color:#d4f8ee}.intro button{background:#57ddbb;color:#051b20;font-weight:800;letter-spacing:2px;padding:15px 26px}.intro .secondary{background:transparent;color:#a4c2d1;margin-left:10px}#help{position:absolute;right:20px;top:78px;max-width:370px;display:none;z-index:4}kbd{font-size:10px;border:1px solid #36505e;padding:1px 4px;color:#e0f5f4}#tooltip{position:absolute;right:30px;bottom:225px;font-size:12px;padding:10px;background:#091622;display:none;max-width:350px;border:1px solid #375462}#clock{color:#a8c1ce;font-size:12px}@media(max-width:900px){.bottom{left:8px;right:8px;gap:6px;height:180px}.miniWrap{display:none}#selection{width:190px}.stats{gap:12px}.brand{font-size:12px}.intro{width:90%;padding:25px}.intro h1{font-size:38px}}
</style></head><body><canvas id="world"></canvas><div class="hud"><div class="top"><div class="brand">FRONTIER WAR<small>MERIDIAN EXPEDITION / SECTOR 09</small></div><div class="stats"><div><small>◈ AETHER</small><b id="money">600</b></div><div><small>SUPPLY</small><b id="supply">4 / 20</b></div><div><small>MISSION TIME</small><span id="clock">00:00</span></div></div><div><button id="mute">♪ ON</button> <button id="helpBtn">?</button> <button id="pause">Ⅱ</button></div></div><div id="notice">Establish an economy. Scout the frontier. Destroy every rival Nexus.</div><div id="help" class="panel"><h3>FIELD MANUAL</h3><p><strong>Left click</strong> selects · drag selects troops · Shift adds / queues orders · double-click selects same type.<br>Right click: move, attack, gather, return or set rally.<br><kbd>A</kbd> attack-move, then click · <kbd>S</kbd> stop · <kbd>H</kbd> hold.<br><kbd>Ctrl 1–9</kbd> save group · <kbd>1–9</kbd> recall.<br><kbd>WASD / arrows</kbd> pan · wheel zoom · <kbd>Q E</kbd> rotate · middle drag rotates. Minimap click pans; right-click orders.<br><kbd>Esc</kbd> cancel / pause · <kbd>F1</kbd> select idle workers.</p><h3>ECONOMY & COUNTERS</h3><p>Fabricators haul 15 aether to a Nexus or Relay. Build near distant fields as deposits run out. Pylons add 20 reusable supply, without a hard cap. Queues reserve supply; shortages pause completion. Cancel queues for 75% refund.<br>Blades shred light targets. Lancers shoot aircraft and penetrate armor. Bastions resist kinetic fire. Wraiths strike ground from the air, but cannot attack aircraft.<br>Destroy ALL rival Nexuses to win. Losing ALL your Nexuses ends the expedition. Fog conceals enemies; camera movement is not scouting.</p></div><div class="bottom"><div class="panel miniWrap"><span>TACTICAL TELEMETRY</span><canvas id="minimap" width="208" height="160"></canvas></div><div id="selection" class="panel"></div><div id="actions" class="panel"><h3 id="actionTitle">COMMAND INTERFACE</h3><div id="buttons"></div><div id="queues"></div></div></div><div id="tooltip"></div></div><div id="drag"></div><div id="overlay"><div class="intro" id="intro"></div></div><script>
'use strict';
const canvas=document.getElementById('world'),gl=canvas.getContext('webgl',{antialias:true,alpha:false});
const $=id=>document.getElementById(id),SIZE=144,CELL=3,N=48,teamColor=[[.20,.91,.75],[1,.29,.26]];
const defs={worker:{name:'Fabricator',cost:55,time:9,hp:65,speed:6,range:1.5,dmg:4,rate:1,supply:1,armor:'light',weapon:'kinetic',sight:14},blade:{name:'Blade',cost:85,time:12,hp:130,speed:7,range:2.3,dmg:20,rate:.65,supply:2,armor:'light',weapon:'kinetic',sight:16},lancer:{name:'Lancer',cost:120,time:16,hp:95,speed:5.5,range:12,dmg:18,rate:1.1,supply:2,armor:'light',weapon:'plasma',airTarget:true,sight:19},tank:{name:'Bastion',cost:240,time:25,hp:380,speed:3.8,range:11,dmg:38,rate:1.6,supply:4,armor:'heavy',weapon:'kinetic',sight:19},flyer:{name:'Wraith',cost:210,time:23,hp:165,speed:9,range:9,dmg:26,rate:1.0,supply:3,armor:'light',weapon:'plasma',flying:true,sight:22},hq:{name:'Nexus',cost:400,time:40,hp:1700,r:4,sight:24,armor:'heavy',supplyCap:20,produce:['worker']},depot:{name:'Relay',cost:130,time:18,hp:500,r:2.4,sight:15,armor:'heavy'},supply:{name:'Pylon',cost:90,time:14,hp:420,r:2,sight:14,armor:'heavy',supplyCap:20},barracks:{name:'Foundry',cost:180,time:24,hp:900,r:3,sight:17,armor:'heavy',produce:['blade','lancer','tank']},air:{name:'Skyport',cost:250,time:30,hp:750,r:3.2,sight:18,armor:'heavy',produce:['flyer']},lab:{name:'Archive',cost:200,time:26,hp:600,r:2.5,sight:16,armor:'heavy'},turret:{name:'Sentinel',cost:150,time:22,hp:650,r:1.7,sight:22,armor:'heavy',range:16,dmg:23,rate:.9,weapon:'plasma',airTarget:true}};
const upgrades={weapons:{name:'Resonant weapons',cost:250,time:40,desc:'+25% damage · all forces'},armor:{name:'Adaptive plating',cost:250,time:40,desc:'20% incoming damage reduction'},harvest:{name:'Extraction lattice',cost:180,time:30,desc:'+50% harvest speed'}};
let entities=[],resources=[],rocks=[],effects=[],selection=[],groups={},players=[],vision=[],known=[],memories=[],time=0,idCounter=1,playing=false,paused=false,ended=false,placement=null,mode=null,mouse={x:0,y:0},drag=null,keys={},last=0,aiTimer=0,fogTimer=0,uiTimer=0,noticeTimer=0,muted=false,audio=null;
let cam={x:24,z:26,yaw:0,zoom:64},screens=new Map(),selectedCache='',seed=17;
function rand(){seed=(seed*1664525+1013904223)>>>0;return seed/4294967296}function dist(a,b){return Math.hypot(a.x-b.x,a.z-b.z)}function cell(x,z){return Math.max(0,Math.min(N-1,Math.floor(z/CELL)))*N+Math.max(0,Math.min(N-1,Math.floor(x/CELL)))}
function sound(f=440,t=.08){if(muted)return;try{audio=audio||new (window.AudioContext||window.webkitAudioContext)();let o=audio.createOscillator(),g=audio.createGain();o.type='triangle';o.frequency.value=f;g.gain.setValueAtTime(.035,audio.currentTime);g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+t);o.connect(g);g.connect(audio.destination);o.start();o.stop(audio.currentTime+t)}catch(e){}}
function notify(s){$('notice').textContent=s;noticeTimer=6}
function spawn(type,team,x,z,complete=true){const d=defs[type];let e={id:idCounter++,type,team,x,z,hp:d.hp,build:complete?1:0,queue:[],order:null,orders:[],cool:0,carry:0,gather:0,rally:{x:x+6,z:z+6},dead:false};entities.push(e);return e}
function cap(t){return entities.reduce((n,e)=>n+(!e.dead&&e.team===t&&e.build>=1?(defs[e.type].supplyCap||0):0),0)}
function used(t){return entities.reduce((n,e)=>n+(e.team===t&&!e.dead?(defs[e.type].supply||0)+e.queue.reduce((s,q)=>s+(defs[q.type]?.supply||0),0):0),0)}
function has(t,type){return entities.some(e=>e.team===t&&e.type===type&&e.build>=1&&!e.dead)}
function init(){seed=17;entities=[];resources=[];rocks=[];effects=[];selection=[];groups={};idCounter=1;time=0;aiTimer=0;fogTimer=0;players=[{money:600,up:{},research:{}},{money:600,up:{},research:{}}];vision=[new Uint8Array(N*N),new Uint8Array(N*N)];known=[new Uint8Array(N*N),new Uint8Array(N*N)];memories=[new Map(),new Map()];cam={x:24,z:26,yaw:0,zoom:64};placement=null;mode=null;ended=false;paused=false;
const fields=[[17,34],[35,16],[113,124],[130,105],[64,26],[31,72],[78,73],[114,54],[61,119],[17,121],[128,21]];fields.forEach(([x,z],i)=>{for(let j=0;j<6;j++){let a=j*Math.PI/3;resources.push({id:idCounter++,x:x+Math.cos(a)*5,z:z+Math.sin(a)*5,left:i<4?1700:2400});}});
for(let i=0;i<45;i++){let x=45+rand()*52,z=8+rand()*128;if(Math.abs(z-70)<13||Math.abs(z-34)<6||Math.abs(z-109)<6||Math.hypot(x-78,z-73)<14)continue;rocks.push({x,z,r:2+rand()*3,h:2+rand()*5})}
spawn('hq',0,22,22);spawn('hq',1,122,122);for(let t=0;t<2;t++)for(let j=0;j<4;j++){let e=spawn('worker',t,t?117-j*1.7:27+j*1.7,t?116:28);autoGather(e)}selection=[entities[0]];updateFog();refreshUI();}
function visible(e,t=0){return e.team===t||!!vision[t][cell(e.x,e.z)]}function nearest(e,list,fn=()=>true){let b=null,dd=Infinity;for(let a of list){if(a.dead||!fn(a))continue;let d=dist(e,a);if(d<dd){dd=d;b=a}}return b}
function autoGather(e){let r=nearest(e,resources,r=>r.left>0&&known[e.team][cell(r.x,r.z)]);if(!r)r=nearest(e,resources,r=>r.left>0&&dist(e,r)<25);e.order=r?{kind:'gather',target:r}:null}
function updateFog(){for(let t=0;t<2;t++){vision[t].fill(0);for(let e of entities){if(e.dead||e.team!==t)continue;let r=defs[e.type].sight*(e.build<1?.55:1),cx=Math.floor(e.x/CELL),cz=Math.floor(e.z/CELL),rr=Math.ceil(r/CELL);for(let z=Math.max(0,cz-rr);z<=Math.min(N-1,cz+rr);z++)for(let x=Math.max(0,cx-rr);x<=Math.min(N-1,cx+rr);x++){if(Math.hypot((x+.5)*CELL-e.x,(z+.5)*CELL-e.z)<r){vision[t][z*N+x]=1;known[t][z*N+x]=1}}}for(let [id,m]of memories[t]){if(vision[t][cell(m.x,m.z)]&&!entities.some(e=>e.id===id&&!e.dead))memories[t].delete(id)}for(let e of entities)if(e.team!==t&&defs[e.type].r&&visible(e,t))memories[t].set(e.id,{id:e.id,type:e.type,x:e.x,z:e.z,team:e.team,hp:e.hp,build:e.build});}}
function requirement(type,t){if(type==='tank'&&!has(t,'lab'))return 'Requires Archive';if(type==='flyer'&&!has(t,'lab'))return 'Requires Archive';if(['air','lab'].includes(type)&&!has(t,'barracks'))return 'Requires Foundry';return ''}
function enqueue(e,type){let d=defs[type]||upgrades[type],p=players[e.team];if(!d||e.build<1)return;if(requirement(type,e.team)){if(!e.team)notify(requirement(type,e.team));return}if(p.money<d.cost){if(!e.team)notify('Insufficient aether');return}if(defs[type]&&used(e.team)+d.supply>cap(e.team)){if(!e.team){notify('Supply blocked — construct more Pylons');sound(170)}return}if(upgrades[type]&&(p.up[type]||p.research[type]))return;p.money-=d.cost;e.queue.push({type,progress:0});if(upgrades[type])p.research[type]=true;refreshUI()}
function validBuild(type,x,z,t){let r=defs[type].r;if(x<r+1||z<r+1||x>SIZE-r-1||z>SIZE-r-1||!vision[t][cell(x,z)])return false;return !rocks.some(a=>Math.hypot(x-a.x,z-a.z)<r+a.r+.8)&&!resources.some(a=>a.left>0&&Math.hypot(x-a.x,z-a.z)<r+1.7)&&!entities.some(a=>!a.dead&&defs[a.type].r&&Math.hypot(x-a.x,z-a.z)<r+defs[a.type].r+1)}
function build(worker,type,x,z){let d=defs[type],t=worker.team;if(players[t].money<d.cost||requirement(type,t)||!validBuild(type,x,z,t))return false;players[t].money-=d.cost;let b=spawn(type,t,x,z,false);b.hp=d.hp*.15;for(let u of entities)if(!defs[u.type].r&&!defs[u.type].flying&&dist(u,b)<d.r+1){let a=Math.atan2(u.x-x,u.z-z);u.x=x+Math.sin(a)*(d.r+1.6);u.z=z+Math.cos(a)*(d.r+1.6);u.path=null}worker.order={kind:'build',target:b};worker.orders=[];return true}
function issue(list,kind,point,target,queue=false){let troops=list.filter(e=>!defs[e.type].r);let n=troops.length,side=Math.ceil(Math.sqrt(n));list.forEach((e,i)=>{if(defs[e.type].r){e.rally={...point,kind:kind==='attackmove'?'attackmove':'move'};return}let p={x:point.x,z:point.z};if(!target&&n>1){p.x+=((i%side)-(side-1)/2)*2.2;p.z+=(Math.floor(i/side)-(side-1)/2)*2.2;p.x=Math.max(1,Math.min(SIZE-1,p.x));p.z=Math.max(1,Math.min(SIZE-1,p.z))}let o={kind,...p,target};if(queue)e.orders.push(o);else{e.order=o;e.orders=[];e.target=null}});if(list.some(e=>e.team===0)){effects.push({kind:'order',x:point.x,z:point.z,life:1,color:kind==='attack'?[1,.3,.25]:[.3,1,.8]});sound(600,.045)}}
function blocked(x,z,e){if(x<.6||z<.6||x>SIZE-.6||z>SIZE-.6)return true;if(defs[e.type].flying)return false;for(let r of rocks)if(Math.hypot(x-r.x,z-r.z)<r.r+.65)return true;for(let b of buildings)if(b!==e&&Math.hypot(x-b.x,z-b.z)<defs[b.type].r+.7)return true;return false}
let buildings=[],buckets=new Map();function bucketKey(x,z){return Math.floor(x/12)+','+Math.floor(z/12)}function rebuildBuckets(){buildings=entities.filter(e=>!e.dead&&defs[e.type].r);buckets.clear();for(let e of entities){if(e.dead)continue;let k=bucketKey(e.x,e.z);if(!buckets.has(k))buckets.set(k,[]);buckets.get(k).push(e)}}
function nearby(e,r){let a=[],x0=Math.floor((e.x-r)/12),x1=Math.floor((e.x+r)/12),z0=Math.floor((e.z-r)/12),z1=Math.floor((e.z+r)/12);for(let z=z0;z<=z1;z++)for(let x=x0;x<=x1;x++){let b=buckets.get(x+','+z);if(b)a.push(...b)}return a}
function findPath(e,x,z,stop){const G=72,S=2;let grid=new Uint8Array(G*G);for(let zz=0;zz<G;zz++)for(let xx=0;xx<G;xx++)if(blocked((xx+.5)*S,(zz+.5)*S,e))grid[zz*G+xx]=1;let sx=Math.max(0,Math.min(G-1,Math.floor(e.x/S))),sz=Math.max(0,Math.min(G-1,Math.floor(e.z/S))),start=sz*G+sx;grid[start]=0;let goal=-1,gd=Infinity;for(let zz=Math.max(0,Math.floor((z-stop-4)/S));zz<Math.min(G,Math.ceil((z+stop+4)/S));zz++)for(let xx=Math.max(0,Math.floor((x-stop-4)/S));xx<Math.min(G,Math.ceil((x+stop+4)/S));xx++){let dd=Math.hypot((xx+.5)*S-x,(zz+.5)*S-z);if(!grid[zz*G+xx]&&dd<gd){gd=dd;goal=zz*G+xx}}if(goal<0)return [];let costs=new Float32Array(G*G);costs.fill(Infinity);costs[start]=0;let prev=new Int32Array(G*G);prev.fill(-1);let closed=new Uint8Array(G*G),open=[start],gx=goal%G,gz=Math.floor(goal/G);for(let iterations=0;open.length&&iterations<4500;iterations++){let bi=0,bf=Infinity;for(let i=0;i<open.length;i++){let k=open[i],f=costs[k]+Math.hypot(k%G-gx,Math.floor(k/G)-gz);if(f<bf){bf=f;bi=i}}let k=open.splice(bi,1)[0];if(k===goal){let path=[];while(k!==start&&k>=0){path.push({x:(k%G+.5)*S,z:(Math.floor(k/G)+.5)*S});k=prev[k]}return path.reverse()}closed[k]=1;let cx=k%G,cz=Math.floor(k/G);for(let dz=-1;dz<=1;dz++)for(let dx=-1;dx<=1;dx++){if(!dx&&!dz)continue;let nx=cx+dx,nz=cz+dz,n=nz*G+nx;if(nx<0||nz<0||nx>=G||nz>=G||grid[n]||closed[n])continue;if(dx&&dz&&(grid[cz*G+nx]||grid[nz*G+cx]))continue;let cost=costs[k]+(dx&&dz?1.414:1);if(cost<costs[n]){costs[n]=cost;prev[n]=k;if(!open.includes(n))open.push(n)}}}return []}
function move(e,x,z,dt,stop=.8){if(Math.hypot(x-e.x,z-e.z)<stop){e.path=null;return true}if(!defs[e.type].flying){let changed=!e.goal||Math.hypot(e.goal.x-x,e.goal.z-z)>4;if(changed){e.path=null;e.goal={x,z}}if(!e.path&&(!e.pathTime||time-e.pathTime>2)){let obstruct=false,dd=Math.hypot(x-e.x,z-e.z);for(let k=2;k<dd-stop;k+=2)if(blocked(e.x+(x-e.x)*k/dd,e.z+(z-e.z)*k/dd,e)){obstruct=true;break}if(obstruct){e.path=findPath(e,x,z,stop);e.pathTime=time}}if(e.path?.length){while(e.path.length&&dist(e,e.path[0])<1.3)e.path.shift();if(e.path.length){x=e.path[0].x;z=e.path[0].z;stop=.4}else e.path=null}if(e.stuck>1.5){e.path=null;e.pathTime=0}}let dx=x-e.x,dz=z-e.z,d=Math.hypot(dx,dz);if(d<stop)return false;let speed=defs[e.type].speed*dt;dx/=d;dz/=d;e.angle=Math.atan2(dx,dz);if(!blocked(e.x+dx*speed,e.z+dz*speed,e)){e.x+=dx*speed;e.z+=dz*speed;e.stuck=0}else{e.stuck=(e.stuck||0)+dt;let sign=e.id%2?1:-1;let found=false;for(let a of [sign*.7,-sign*.7,sign*1.4,-sign*1.4,sign*2.1]){let xx=dx*Math.cos(a)-dz*Math.sin(a),zz=dx*Math.sin(a)+dz*Math.cos(a);if(!blocked(e.x+xx*speed,e.z+zz*speed,e)){e.x+=xx*speed;e.z+=zz*speed;found=true;break}}if(!found&&e.stuck>1){e.x+=Math.sin(e.id+time)*.05;e.z+=Math.cos(e.id+time)*.05}}if(!defs[e.type].flying){for(let u of nearby(e,2)){if(u===e||defs[u.type].r||defs[u.type].flying||u.dead)continue;let dd=dist(e,u);if(dd<1.15&&dd>.01){let push=(1.15-dd)*.2,xx=e.x+(e.x-u.x)/dd*push,zz=e.z+(e.z-u.z)/dd*push;if(!blocked(xx,zz,e)){e.x=xx;e.z=zz}}}}return false}
function canAttack(e,t){let d=defs[e.type];return d.dmg&&(!defs[t.type].flying||d.airTarget)&&!t.dead&&t.team!==e.team&&visible(t,e.team)}
function damage(e,t){let d=defs[e.type],td=defs[t.type],mult=d.weapon==='kinetic'?(td.armor==='heavy'?.55:1.25):(td.armor==='heavy'?1.3:.9);let amount=d.dmg*mult*(players[e.team].up.weapons?1.25:1)*(players[t.team].up.armor?.8:1);t.hp-=amount;effects.push({kind:d.range<3?'hit':'shot',x:e.x,z:e.z,y:d.flying?5:1.6,tx:t.x,tz:t.z,ty:td.flying?5:1,life:.22,color:d.weapon==='plasma'?[.55,.75,1]:[1,.8,.25]});if(t.hp<=0){t.dead=true;for(let q of t.queue)if(upgrades[q.type])players[t.team].research[q.type]=false;effects.push({kind:'boom',x:t.x,z:t.z,life:1,color:teamColor[t.team],size:td.r||1.5});if(t.team===0&&td.r)notify(td.name+' destroyed!');if(td.r)sound(90,.2)}else if(t.team===0&&td.r&&(!t.warn||time-t.warn>15)){t.warn=time;notify('Under attack: '+td.name);sound(210,.2)}}
function tick(dt){time+=dt;fogTimer-=dt;if(fogTimer<=0){updateFog();fogTimer=.3}rebuildBuckets();for(let e of entities){if(e.dead)continue;let d=defs[e.type],p=players[e.team];e.cool-=dt;if(e.build<1)continue;if(e.queue.length){let q=e.queue[0],qd=defs[q.type]||upgrades[q.type];if(!defs[q.type]||used(e.team)<=cap(e.team)){q.progress+=dt;if(q.progress>=qd.time){e.queue.shift();if(upgrades[q.type]){p.up[q.type]=true;p.research[q.type]=false}else{let a=(e.id+time)*2.1,rr=d.r+2,u=spawn(q.type,e.team,e.x+Math.sin(a)*rr,e.z+Math.cos(a)*rr);for(let k=0;k<60&&blocked(u.x,u.z,u);k++){a+=.8;rr=d.r+2+Math.floor(k/12)*1.8;u.x=Math.max(1,Math.min(SIZE-1,e.x+Math.sin(a)*rr));u.z=Math.max(1,Math.min(SIZE-1,e.z+Math.cos(a)*rr))}u.order={kind:e.rally.kind||'move',x:e.rally.x,z:e.rally.z};if(q.type==='worker')autoGather(u)}if(e.team===0)sound(880,.08)}}}
if(d.r&&e.type!=='turret')continue;let o=e.order;if(d.dmg&&e.type!=='worker'){let target=e.target;if(!target||!canAttack(e,target)||dist(e,target)>d.sight+3)target=null;if(o?.kind==='attack'&&o.target&&!o.target.dead&&visible(o.target,e.team)&&canAttack(e,o.target))target=o.target;if(!target&&(o?.kind!=='move'))target=nearest(e,nearby(e,d.sight),a=>canAttack(e,a)&&dist(e,a)<(o?.kind==='hold'?d.range:d.sight));e.target=target;if(target){let range=d.range+(defs[target.type].r||.4);if(dist(e,target)<=range){if(e.cool<=0){damage(e,target);e.cool=d.rate}continue}else if(!d.r&&o?.kind!=='hold'){move(e,target.x,target.z,dt,range*.9);continue}}}
if(d.r)continue;if(!o){if(e.orders.length)e.order=e.orders.shift();continue}if(o.kind==='gather'){let r=o.target;if(e.carry>=15||r.left<=0&&e.carry>0){let h=nearest(e,buildings,b=>b.team===e.team&&b.build>=1&&['hq','depot'].includes(b.type));if(h&&move(e,h.x,h.z,dt,defs[h.type].r+1.2)){p.money+=e.carry;if(e.team===0)effects.push({kind:'income',x:e.x,z:e.z,life:1.2,amount:e.carry});e.carry=0;if(r.left<=0)autoGather(e)}}else if(r.left<=0){autoGather(e)}else if(move(e,r.x,r.z,dt,2.2)){e.gather+=dt*(p.up.harvest?1.5:1);if(e.gather>=.18){e.gather=0;let n=Math.min(1,r.left);r.left-=n;e.carry+=n}}}else if(o.kind==='return'){let h=o.target||nearest(e,buildings,b=>b.team===e.team&&b.build>=1&&['hq','depot'].includes(b.type));if(h&&move(e,h.x,h.z,dt,defs[h.type].r+1.2)){p.money+=e.carry;e.carry=0;autoGather(e)}}else if(o.kind==='build'){let b=o.target;if(b.dead||b.build>=1){e.order=null;autoGather(e)}else if(move(e,b.x,b.z,dt,defs[b.type].r+1.4)){let bd=defs[b.type];b.build=Math.min(1,b.build+dt/bd.time);b.hp=Math.min(bd.hp,b.hp+bd.hp*.85*dt/bd.time);if(b.build>=1&&e.team===0){notify(bd.name+' online');sound(740,.12)}}}else if(o.kind==='move'||o.kind==='attackmove'){if(move(e,o.x,o.z,dt)){e.order=e.orders.shift()||null}}else if(o.kind==='attack'&&(!o.target||o.target.dead||!visible(o.target,e.team))){e.order=e.orders.shift()||null}}
for(let f of effects)f.life-=dt;effects=effects.filter(f=>f.life>0);entities=entities.filter(e=>!e.dead);selection=selection.filter(e=>!e.dead);aiTimer-=dt;if(aiTimer<=0){thinkAI();aiTimer=1.8}for(let t=0;t<2;t++)if(!entities.some(e=>e.team===t&&e.type==='hq'))finish(t===1);}
function aiPlace(w,type,center){for(let i=0;i<45;i++){let a=rand()*Math.PI*2,r=8+rand()*16,x=center.x+Math.sin(a)*r,z=center.z+Math.cos(a)*r;if(validBuild(type,x,z,1))return build(w,type,x,z)}return false}
let nextRaid=100,scoutSent=false;
function thinkAI(){let p=players[1],own=entities.filter(e=>e.team===1),workers=own.filter(e=>e.type==='worker'),hqs=own.filter(e=>e.type==='hq'),army=own.filter(e=>defs[e.type].supply&&e.type!=='worker'),complete=own.filter(e=>e.build>=1),w=workers.find(e=>e.order?.kind!=='build'&&e.order?.kind!=='move');let base=hqs[0];if(!base)return;
for(let e of workers)if(!e.order)autoGather(e);for(let h of hqs)if(h.queue.length<1&&workers.length<Math.min(28,8+time/22)&&p.money>=55)enqueue(h,'worker');if(w){let need=null;if(cap(1)-used(1)<7&&!own.some(e=>e.type==='supply'&&e.build<1))need='supply';else if(!own.some(e=>e.type==='barracks'))need='barracks';else if(time>85&&!own.some(e=>e.type==='lab'))need='lab';else if(time>125&&!own.some(e=>e.type==='air'))need='air';else if(time>165&&own.filter(e=>e.type==='barracks').length<Math.min(6,1+Math.floor(time/100)))need='barracks';if(need&&p.money>=defs[need].cost)aiPlace(w,need,base);else if(time>180&&p.money>450&&hqs.length<1+Math.floor(time/160)){let fields=resources.filter(r=>r.left>500&&known[1][cell(r.x,r.z)]&&!hqs.some(h=>dist(h,r)<24));let r=nearest(base,fields);if(r)aiPlace(w,'hq',r)}else if(time>130&&p.money>500&&own.filter(e=>e.type==='turret').length<hqs.length*2)aiPlace(w,'turret',hqs[hqs.length-1])}
for(let b of complete){if(b.type==='barracks'&&b.queue.length<2){let choice=has(1,'lab')&&rand()<.28?'tank':rand()<.6?'lancer':'blade';enqueue(b,choice)}if(b.type==='air'&&b.queue.length<2)enqueue(b,'flyer');if(b.type==='lab'&&!b.queue.length&&p.money>350){let u=Object.keys(upgrades).find(k=>!p.up[k]&&!p.research[k]);if(u)enqueue(b,u)}}
if(time>42&&!scoutSent&&army.length){let s=army[0];issue([s],'move',{x:77,z:73});s.orders.push({kind:'move',x:31,z:72},{kind:'hold'});scoutSent=true}let threat=nearest(base,entities,e=>e.team===0&&visible(e,1)&&hqs.some(h=>dist(h,e)<32));if(threat){issue(army,'attack',{x:threat.x,z:threat.z},threat)}else if(time>nextRaid&&army.length>=5){let targets=[...memories[1].values()],target=targets.find(e=>e.type==='hq')||targets[0];let routes=[{x:78,z:73},{x:61,z:119},{x:31,z:72},{x:64,z:26},{x:17,z:34}];let dest=target||routes[Math.floor(time/60)%routes.length];issue(army,'attackmove',dest);nextRaid=time+55+rand()*25}else for(let u of army)if(!u.order)u.order={kind:'attackmove',x:base.x-14,z:base.z-12};}
</script>
<script>
const vs='attribute vec3 p;attribute vec3 c;varying vec3 v;void main(){gl_Position=vec4(p,1.0);v=c;}',fs='precision mediump float;varying vec3 v;void main(){gl_FragColor=vec4(v,1.0);}';function shader(type,src){let s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);return s}let program=gl.createProgram();gl.attachShader(program,shader(gl.VERTEX_SHADER,vs));gl.attachShader(program,shader(gl.FRAGMENT_SHADER,fs));gl.linkProgram(program);gl.useProgram(program);let vb=gl.createBuffer(),pb=gl.getAttribLocation(program,'p'),cb=gl.getAttribLocation(program,'c');gl.enableVertexAttribArray(pb);gl.enableVertexAttribArray(cb);gl.enable(gl.DEPTH_TEST);let verts=[],W=innerWidth,H=innerHeight;
function resize(){W=innerWidth;H=innerHeight;canvas.width=W*devicePixelRatio;canvas.height=H*devicePixelRatio;gl.viewport(0,0,canvas.width,canvas.height)}addEventListener('resize',resize);resize();
function project(x,y,z){let dx=x-cam.x,dz=z-cam.z,c=Math.cos(cam.yaw),s=Math.sin(cam.yaw),rx=dx*c-dz*s,rz=dx*s+dz*c;let scale=H/cam.zoom;return {x:W/2+rx*scale,y:H*.43+(rz*.57-y*.82)*scale,depth:-(rz*.82+y*.57)/180}}
function unproject(x,y){let scale=H/cam.zoom,rx=(x-W/2)/scale,rz=(y-H*.43)/scale/.57,c=Math.cos(cam.yaw),s=Math.sin(cam.yaw);return{x:Math.max(0,Math.min(SIZE,cam.x+rx*c+rz*s)),z:Math.max(0,Math.min(SIZE,cam.z-rx*s+rz*c))}}
function vtx(p,c){let q=project(...p);verts.push(q.x/W*2-1,1-q.y/H*2,q.depth,...c)}function tri(a,b,c,col){vtx(a,col);vtx(b,col);vtx(c,col)}function quad(a,b,c,d,col){tri(a,b,c,col);tri(a,c,d,col)}function tint(c,f){return c.map(v=>v*f)}
function box(x,y,z,w,h,d,c,angle=0){let co=Math.cos(angle),si=Math.sin(angle);function p(a,b,k){return[x+a*co+k*si,y+b,z-a*si+k*co]}let a=p(-w/2,0,-d/2),b=p(w/2,0,-d/2),cc=p(w/2,0,d/2),dd=p(-w/2,0,d/2),e=p(-w/2,h,-d/2),f=p(w/2,h,-d/2),g=p(w/2,h,d/2),hh=p(-w/2,h,d/2);quad(e,f,g,hh,tint(c,1.12));quad(a,b,f,e,tint(c,.65));quad(b,cc,g,f,tint(c,.8));quad(cc,dd,hh,g,tint(c,.87));quad(dd,a,e,hh,tint(c,.7))}
function pyramid(x,y,z,r,h,c){let a=[x-r,y,z-r],b=[x+r,y,z-r],d=[x-r,y,z+r],cc=[x+r,y,z+r],top=[x,y+h,z];tri(a,b,top,tint(c,.7));tri(b,cc,top,c);tri(cc,d,top,tint(c,1.15));tri(d,a,top,tint(c,.8))}
function ring(x,z,r,col,y=.12){let thick=.15;for(let i=0;i<24;i++){let a=i*Math.PI/12,b=(i+1)*Math.PI/12;quad([x+Math.sin(a)*r,y,z+Math.cos(a)*r],[x+Math.sin(b)*r,y,z+Math.cos(b)*r],[x+Math.sin(b)*(r-thick),y,z+Math.cos(b)*(r-thick)],[x+Math.sin(a)*(r-thick),y,z+Math.cos(a)*(r-thick)],col)}}
function beam(x,y,z,tx,ty,tz,col,width=.12){let dx=tx-x,dz=tz-z,l=Math.hypot(dx,dz)||1,nx=-dz/l*width,nz=dx/l*width;quad([x+nx,y,z+nz],[tx+nx,ty,tz+nz],[tx-nx,ty,tz-nz],[x-nx,y,z-nz],col)}
function drawEntity(e,ghost=false,preview=false){let d=defs[e.type],c=ghost?[.19,.24,.3]:teamColor[e.team],base=ghost?[.10,.14,.18]:[.18,.25,.3],x=e.x,z=e.z,b=e.build||.1,angle=e.angle||0;let hh=d.flying?5+Math.sin(time*3+e.id)*.2:0;if(d.r){let r=d.r;box(x,.1,z,r*2,.6,r*2,base);let h=(e.type==='hq'?5:3)*b;if(e.type==='hq'){box(x,.6,z,5,h,5,base);box(x,h+.7,z,3,.5,3,c);pyramid(x,h+1,z,1.3,2*b,c);for(let s of [-1,1])box(x+s*3,.6,z,1.1,2*b,4,c)}else if(e.type==='supply'){box(x,.6,z,2,1.2*b,2,base);pyramid(x,1.8*b,z,1.4,4*b,c)}else if(e.type==='barracks'){box(x,.6,z,5,2*b,4,base);for(let s of [-1,1])box(x+s*1.5,2.6*b,z,1,1.3*b,3,c);box(x,.7,z+2.1,2,1.1*b,.3,c)}else if(e.type==='air'){box(x,.6,z,5,.6,5,c);ring(x,z,2,[.75,.85,.9],1.25);box(x+2,1,z-2,1,3*b,1,base)}else if(e.type==='lab'){box(x,.6,z,3.5,2*b,3.5,base);pyramid(x,2.6*b,z,1.8,2*b,[.5,.55,.9]);box(x,4.6*b,z,.5,.7,.5,c)}else if(e.type==='turret'){box(x,.6,z,2.2,2*b,2.2,base);box(x,2.6*b,z,1.3,.8,1.3,c);let target=e.target,a=target?Math.atan2(target.x-x,target.z-z):0;box(x+Math.sin(a),3*b,z+Math.cos(a),.5,.5,2.5,c,a)}else{box(x,.6,z,3.5,1.8*b,3.5,base);box(x,2.4*b,z,3.8,.5,3.8,c)}if(b<1){ring(x,z,r+1,[1,.7,.25]);for(let s of [-1,1])box(x+s*r,.7,z,.2,4*b,.2,[1,.7,.25])}}
else if(e.type==='flyer'){box(x,hh,z,1.1,.5,2.8,base,angle);box(x,hh+.3,z,3.5,.25,1,c,angle);pyramid(x,hh+.5,z,.55,.5,c);ring(x,z,.65,[.12,.24,.27])}else if(e.type==='tank'){box(x,.2,z,2.8,1.1,3.2,base,angle);box(x,1.3,z,1.9,.7,1.7,c,angle);box(x+Math.sin(angle)*1.1,1.65,z+Math.cos(angle)*1.1,.4,.35,2.5,[.5,.65,.7],angle)}else if(e.type==='worker'){box(x,.3,z,1.2,.7,1.5,c,angle);box(x,1,z,.6,.4,.6,[.7,.82,.88]);if(e.carry)box(x,1.4,z,.6,.4,.6,[.55,.45,1])}else{box(x,.1,z,1,.5,1,base,angle);box(x,.6,z,.85,1,.6,c,angle);box(x,1.6,z,.65,.5,.6,[.7,.84,.9]);if(e.type==='blade')beam(x+.5,1,z,x+.9,2.1,z,[.6,1,.95],.16);else box(x+.6,1.2,z+.2,.25,.25,1.2,[.6,.7,.85],angle)}
if(!ghost&&!preview){let p=project(x,hh+(d.r?7:2.5),z);screens.set(e.id,{x:p.x,y:project(x,hh+1,z).y,r:Math.max(d.r||1,1)*H/cam.zoom});if(selection.includes(e))ring(x,z,(d.r||1)+.7,[.4,1,.85]);if(e.hp<d.hp||selection.includes(e)){let y=hh+(d.r?7:3),w=d.r?5:2;box(x,y,z,w,.13,.18,[.12,.15,.2]);box(x-w/2+w*e.hp/d.hp/2,y+.03,z,w*e.hp/d.hp,.15,.2,e.hp<d.hp*.3?[1,.3,.2]:[.2,1,.65])}}}
function render(){gl.clearColor(.025,.045,.07,1);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);verts=[];screens.clear();for(let z=0;z<N;z++)for(let x=0;x<N;x++){let i=z*N+x,k=known[0][i],v=vision[0][i];let f=v?1:k?.38:.075;let c=tint((x+z)%2?[.14,.23,.27]:[.145,.24,.28],f);quad([x*CELL,0,z*CELL],[(x+1)*CELL,0,z*CELL],[(x+1)*CELL,0,(z+1)*CELL],[x*CELL,0,(z+1)*CELL],c)}
for(let r of rocks)if(known[0][cell(r.x,r.z)]){let f=vision[0][cell(r.x,r.z)]?1:.32;pyramid(r.x,.05,r.z,r.r,r.h,tint([.25,.32,.36],f));pyramid(r.x+.6,r.h*.3,r.z-.5,r.r*.5,r.h*.9,tint([.29,.37,.42],f))}for(let r of resources)if(r.left>0&&known[0][cell(r.x,r.z)]){let f=vision[0][cell(r.x,r.z)]?1:.32;let h=1.2+r.left/1600;pyramid(r.x,.05,r.z,.95,h,tint([.41,.37,.85],f));pyramid(r.x+.65,.05,r.z+.5,.5,h*.7,tint([.62,.51,.94],f))}
for(let m of memories[0].values())if(!vision[0][cell(m.x,m.z)])drawEntity(m,true);for(let e of entities)if(visible(e))drawEntity(e);for(let e of selection)if(e.order?.x)ring(e.order.x,e.order.z,.6,[.25,.6,.5]);if(placement){let p=unproject(mouse.x,mouse.y),ok=validBuild(placement,p.x,p.z,0);drawEntity({type:placement,team:0,...p,build:1},false,true);ring(p.x,p.z,defs[placement].r+1,ok?[.3,1,.6]:[1,.2,.2])}
for(let f of effects){if(!vision[0][cell(f.x,f.z)]&&f.kind!=='order')continue;if(f.kind==='shot')beam(f.x,f.y,f.z,f.tx,f.ty,f.tz,f.color,.13);else if(f.kind==='order')ring(f.x,f.z,1+(1-f.life)*2,f.color);else if(f.kind==='boom')pyramid(f.x,.2,f.z,(f.size||1)*f.life,3*(1-f.life)+1,tint(f.color,f.life));else if(f.kind==='hit')pyramid(f.tx,.5,f.tz,.5,2,f.color);else if(f.kind==='income')pyramid(f.x,2+(1.2-f.life)*2,f.z,.25,.5,[.65,1,.8])}
gl.bindBuffer(gl.ARRAY_BUFFER,vb);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(verts),gl.DYNAMIC_DRAW);gl.vertexAttribPointer(pb,3,gl.FLOAT,false,24,0);gl.vertexAttribPointer(cb,3,gl.FLOAT,false,24,12);gl.drawArrays(gl.TRIANGLES,0,verts.length/6);drawMini();}
const mc=$('minimap').getContext('2d');function drawMini(){let sx=208/SIZE,sz=160/SIZE;mc.fillStyle='#03070b';mc.fillRect(0,0,208,160);for(let z=0;z<N;z++)for(let x=0;x<N;x++){let i=z*N+x;if(!known[0][i])continue;mc.fillStyle=vision[0][i]?'#233b42':'#111d26';mc.fillRect(x*CELL*sx,z*CELL*sz,CELL*sx+.5,CELL*sz+.5)}for(let r of resources)if(r.left>0&&known[0][cell(r.x,r.z)]){mc.fillStyle='#8277cd';mc.fillRect(r.x*sx-1,r.z*sz-1,2,2)}for(let m of memories[0].values())if(!vision[0][cell(m.x,m.z)]){mc.fillStyle='#593e43';mc.fillRect(m.x*sx-2,m.z*sz-2,4,4)}for(let e of entities)if(visible(e)){mc.fillStyle=e.team?'#ff6a59':'#5ae6be';let r=defs[e.type].r?2:1;mc.fillRect(e.x*sx-r,e.z*sz-r,r*2,r*2)}let ps=[[0,70],[W,70],[W,H-215],[0,H-215]].map(([x,y])=>unproject(x,y));mc.beginPath();ps.forEach((p,i)=>i?mc.lineTo(p.x*sx,p.z*sz):mc.moveTo(p.x*sx,p.z*sz));mc.closePath();mc.strokeStyle='#bcdedb99';mc.stroke();}
function refreshUI(){ $('money').textContent=Math.floor(players[0]?.money||0);$('supply').textContent=used(0)+' / '+cap(0);$('supply').style.color=used(0)>cap(0)?'#ff8a65':'#6cf5d1';$('clock').textContent=String(Math.floor(time/60)).padStart(2,'0')+':'+String(Math.floor(time%60)).padStart(2,'0');let e=selection[0];if(!e){$('selection').innerHTML='<h3>MERIDIAN EXPEDITION</h3><p>Select a Fabricator to construct.<br>Select a production facility to train.<br>Scout to locate the rival expedition.</p>';$('buttons').innerHTML='';$('queues').innerHTML='';return}let d=defs[e.type],counts={};for(let u of selection)counts[defs[u.type].name]=(counts[defs[u.type].name]||0)+1;$('selection').innerHTML='<h3>'+(selection.length===1?d.name.toUpperCase():selection.length+' UNITS SELECTED')+'</h3><p>'+Object.entries(counts).map(([k,v])=>v+' × '+k).join(' · ')+'</p><p>Integrity '+Math.round(selection.reduce((s,u)=>s+u.hp,0))+' / '+selection.reduce((s,u)=>s+defs[u.type].hp,0)+(selection.length===1?'<br>'+d.armor+' armor · '+(d.weapon||'infrastructure')+(d.dmg?' · '+d.dmg+' damage':''):'')+'</p>'+(e.build<1?'<p>Construction '+Math.floor(e.build*100)+'% — Fabricator required</p>':'')+(e.carry?'<p>Carrying '+e.carry+' aether</p>':'');$('buttons').innerHTML='';$('queues').innerHTML='';function btn(label,sub,fn,reason=''){let b=document.createElement('button');b.innerHTML=label+'<small>'+sub+'</small>';b.onclick=fn;b.title=reason||sub;if(reason){b.style.opacity='.45';b.onclick=()=>notify(reason)}$('buttons').appendChild(b)}
if(e.team!==0)return;if(!d.r){btn('Attack move [A]','Engage on route',()=>{mode='attackmove';notify('Attack-move: click a destination')});btn('Stop [S]','Clear all orders',()=>command('stop'));btn('Hold [H]','Defend this position',()=>command('hold'));if(selection.some(u=>u.type==='worker'))for(let type of ['hq','depot','supply','barracks','air','lab','turret']){let b=defs[type],reason=requirement(type,0)|| (players[0].money<b.cost?'Needs '+b.cost+' aether':'');btn(b.name,'◈ '+b.cost+' · '+b.time+'s',()=>{placement=type;mode=null;notify('Place '+b.name+' on visible, open terrain. Esc cancels.')},reason)}}else if(e.build>=1){for(let type of d.produce||[]){let u=defs[type],reason=requirement(type,0)||(players[0].money<u.cost?'Needs '+u.cost+' aether':'')||(used(0)+u.supply>cap(0)?'Needs more supply — build a Pylon':'');btn(u.name,'◈ '+u.cost+' · '+u.supply+' supply · '+u.time+'s',()=>enqueue(e,type),reason)}if(e.type==='lab')for(let [k,u]of Object.entries(upgrades))btn(u.name,players[0].up[k]?'Complete':'◈ '+u.cost+' · '+u.time+'s',()=>enqueue(e,k),players[0].up[k]?'Already researched':players[0].research[k]?'Research in progress':players[0].money<u.cost?'Insufficient aether':'');if(!d.produce&&e.type!=='lab')$('buttons').innerHTML='<p>'+({hq:'Returns harvested aether.',depot:'Workers can return aether here.',supply:'Provides +20 supply. Build freely to grow your army.',turret:'Plasma defense. Targets ground and air.'}[e.type]||'')+'</p>';let qwrap=document.createElement('div');qwrap.className='queue';e.queue.forEach((q,i)=>{let b=document.createElement('button'),qd=defs[q.type]||upgrades[q.type];b.textContent=qd.name+' '+(i===0?Math.floor(q.progress/qd.time*100)+'%':'queued')+' ×';b.title='Cancel: refund 75% and release reserved supply';b.onclick=()=>{players[0].money+=qd.cost*.75;if(upgrades[q.type])players[0].research[q.type]=false;e.queue.splice(i,1);refreshUI()};qwrap.appendChild(b)});$('queues').appendChild(qwrap);if(e.queue.length&&used(0)>cap(0))$('queues').append('Supply blocked — add Pylons');if(d.produce)$('queues').append(' Right-click terrain to set rally.');}}
function command(kind){for(let e of selection)if(!defs[e.type].r){e.order=kind==='hold'?{kind:'hold'}:null;e.orders=[];e.target=null}sound();refreshUI()}
function pick(x,y){let best=null,dd=Infinity;for(let e of entities){if(!visible(e))continue;let p=screens.get(e.id);if(!p)continue;let d=Math.hypot(p.x-x,p.y-y);if(d<Math.max(12,p.r+10)&&d<dd){best=e;dd=d}}return best}
function contextual(p,target,shift){if(!selection.length)return;if(target&&target.team===1)issue(selection,'attack',p,target,shift);else if(target&&target.team===0&&['hq','depot'].includes(target.type)&&selection.some(e=>e.type==='worker'&&e.carry)){issue(selection,'return',p,target,shift)}else{let r=nearest(p,resources,a=>a.left>0&&known[0][cell(a.x,a.z)]);if(r&&dist(p,r)<3.5&&selection.some(e=>e.type==='worker')){issue(selection.filter(e=>e.type==='worker'),'gather',p,r,shift);issue(selection.filter(e=>e.type!=='worker'),'move',p,null,shift)}else if(target&&target.build<1&&target.team===0&&selection.some(e=>e.type==='worker'))issue(selection.filter(e=>e.type==='worker'),'build',p,target,shift);else issue(selection,'move',p,null,shift)}}
canvas.oncontextmenu=e=>e.preventDefault();canvas.onmousedown=e=>{if(!playing||paused)return;mouse={x:e.clientX,y:e.clientY};if(e.button===2){placement=null;mode=null;contextual(unproject(mouse.x,mouse.y),pick(mouse.x,mouse.y),e.shiftKey);return}if(e.button===1){drag={rotate:true,x:e.clientX};e.preventDefault();return}if(e.button!==0)return;if(placement){let w=selection.find(u=>u.type==='worker'&&u.team===0),p=unproject(mouse.x,mouse.y);if(w&&build(w,placement,p.x,p.z)){placement=null;refreshUI()}else notify('Cannot build here: visible clear ground and enough aether required');return}if(mode){issue(selection,mode,unproject(mouse.x,mouse.y),null,e.shiftKey);mode=null;return}drag={x:e.clientX,y:e.clientY,shift:e.shiftKey};};addEventListener('mousemove',e=>{mouse={x:e.clientX,y:e.clientY};if(drag?.rotate){cam.yaw+=(e.clientX-drag.x)*.008;drag.x=e.clientX}else if(drag){let box=$('drag');box.style.display='block';box.style.left=Math.min(drag.x,e.clientX)+'px';box.style.top=Math.min(drag.y,e.clientY)+'px';box.style.width=Math.abs(drag.x-e.clientX)+'px';box.style.height=Math.abs(drag.y-e.clientY)+'px'}});addEventListener('mouseup',e=>{if(!drag)return;if(!drag.rotate){let hits=[];if(Math.hypot(e.clientX-drag.x,e.clientY-drag.y)<6){let a=pick(e.clientX,e.clientY);if(a)hits=[a]}else{for(let a of entities){let p=screens.get(a.id);if(a.team===0&&!defs[a.type].r&&p&&p.x>=Math.min(drag.x,e.clientX)&&p.x<=Math.max(drag.x,e.clientX)&&p.y>=Math.min(drag.y,e.clientY)&&p.y<=Math.max(drag.y,e.clientY))hits.push(a)}}selection=drag.shift?[...new Set([...selection,...hits])]:hits;refreshUI()}drag=null;$('drag').style.display='none'});canvas.ondblclick=e=>{let a=pick(e.clientX,e.clientY);if(a&&a.team===0){selection=entities.filter(u=>u.team===0&&u.type===a.type&&screens.has(u.id)&&Math.abs(screens.get(u.id).x-W/2)<W/2&&screens.get(u.id).y>60&&screens.get(u.id).y<H-210);refreshUI()}};canvas.onwheel=e=>{e.preventDefault();cam.zoom=Math.max(32,Math.min(120,cam.zoom+e.deltaY*.04))};
$('minimap').oncontextmenu=e=>e.preventDefault();$('minimap').onmousedown=e=>{if(!playing||paused)return;let r=e.currentTarget.getBoundingClientRect(),p={x:(e.clientX-r.left)/r.width*SIZE,z:(e.clientY-r.top)/r.height*SIZE};if(e.button===2)contextual(p,nearest(p,entities,a=>visible(a)&&dist(p,a)<4),e.shiftKey);else if(mode){issue(selection,mode,p,null,e.shiftKey);mode=null}else{cam.x=p.x;cam.z=p.z}};
addEventListener('keydown',e=>{if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' ','F1'].includes(e.key))e.preventDefault();let k=e.key.toLowerCase();keys[k]=true;if(!playing)return;if(k==='escape'){if(placement||mode){placement=null;mode=null}else togglePause();return}if(paused)return;if(k==='a'&&!e.ctrlKey){mode='attackmove';notify('Attack-move: click a destination. Arrow keys pan.')}if(k==='s'&&!e.ctrlKey)command('stop');if(k==='h')command('hold');if(k==='f1'){selection=entities.filter(u=>u.team===0&&u.type==='worker'&&!u.order);refreshUI()}if(/^[1-9]$/.test(k)){if(e.ctrlKey){e.preventDefault();groups[k]=selection.map(u=>u.id);notify('Control group '+k+' assigned')}else{selection=(groups[k]||[]).map(id=>entities.find(u=>u.id===id)).filter(Boolean);refreshUI()}}});addEventListener('keyup',e=>delete keys[e.key.toLowerCase()]);addEventListener('blur',()=>{keys={};if(playing&&!paused&&!ended)togglePause()});
function start(){init();nextRaid=100;scoutSent=false;playing=true;$('overlay').style.display='none';notify('Landing secure. Fabricators are gathering. Build a Foundry and Pylons.');sound(660,.2)}
function title(){ $('intro').innerHTML='<div class="eyebrow">09 / THE AETHER FRONTIER</div><h1>FRONTIER<br>WAR</h1><h2>A WORLD UNCLAIMED. AN EXPEDITION AT RISK.</h2><div class="columns"><div><h3>BUILD YOUR FRONTIER</h3><p>Command the teal Meridian expedition. Fabricators collect finite aether. Raise Foundries, train armies, and establish new Nexuses near distant fields. Pylons grow your supply without a population cap.</p></div><div><h3>DISCOVER YOUR RIVAL</h3><p>The red Ashen Compact develops beyond your sensors. Scout the unknown. <strong>Destroy every enemy Nexus to win.</strong> Protect yours — losing them all means defeat. Expect first raids after a safe opening.</p></div></div><p>Left click / drag to select · Right click to order · A then click to attack-move<br>Arrows or WASD to pan · Wheel to zoom · Q / E to rotate · ? for field manual</p><br><button id="start">DEPLOY EXPEDITION →</button>'; $('start').onclick=start;}
function togglePause(){if(ended||!playing)return;paused=!paused;$('overlay').style.display=paused?'flex':'none';if(paused){$('intro').innerHTML='<div class="eyebrow">MISSION SUSPENDED</div><h1>PAUSED</h1><p>Your expedition awaits. All simulation is paused.</p><br><button id="resume">RESUME →</button><button id="restart" class="secondary">RESTART</button>';$('resume').onclick=togglePause;$('restart').onclick=start}}
function finish(win){ended=true;playing=false;$('overlay').style.display='flex';$('intro').innerHTML='<div class="eyebrow">EXPEDITION REPORT / '+$('clock').textContent+'</div><h1>'+(win?'FRONTIER<br>SECURED':'EXPEDITION<br>LOST')+'</h1><h2>'+(win?'The rival network has fallen. Meridian holds the frontier.':'All Meridian Nexuses have been destroyed.')+'</h2><p>Your expedition fielded '+entities.filter(e=>e.team===0&&!defs[e.type].r).length+' surviving units. Explore new strategies in another skirmish.</p><br><button id="again">DEPLOY AGAIN →</button>';$('again').onclick=start;sound(win?900:120,.4)}
$('pause').onclick=togglePause;$('helpBtn').onclick=()=>{$('help').style.display=$('help').style.display==='block'?'none':'block'};$('mute').onclick=()=>{muted=!muted;$('mute').textContent=muted?'♪ OFF':'♪ ON'};
function frame(now){let dt=Math.min(.05,(now-last)/1000||.016);last=now;if(playing&&!paused){let vx=(keys.arrowright||keys.d?1:0)-(keys.arrowleft?1:0),vz=(keys.arrowdown||keys.s?1:0)-(keys.arrowup||keys.w?1:0);let speed=cam.zoom*.65*dt,c=Math.cos(cam.yaw),s=Math.sin(cam.yaw);cam.x=Math.max(0,Math.min(SIZE,cam.x+(vx*c+vz*s)*speed));cam.z=Math.max(0,Math.min(SIZE,cam.z+(-vx*s+vz*c)*speed));cam.yaw+=((keys.e?1:0)-(keys.q?1:0))*dt;tick(dt);uiTimer-=dt;if(uiTimer<=0){refreshUI();uiTimer=.4}noticeTimer-=dt;if(noticeTimer<0)$('notice').textContent=placement?'PLACEMENT MODE · Left click to construct · Esc to cancel':mode?'ATTACK MOVE · Click destination':'MERIDIAN / '+entities.filter(e=>e.team===0&&!defs[e.type].r).length+' active units · Scout the unknown';}render();requestAnimationFrame(frame)}
init();title();requestAnimationFrame(frame);
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6.1-sol","provider":"openrouter","persona":"full","sessionId":"c66eb195-8ae5-4220-9afa-c81260f99dd8","tokensIn":361223,"tokensOut":18320,"tokensTotal":379543,"cost":0.29268700000000003,"turns":15,"toolCalls":14,"failedToolCalls":0,"timestamp":"2026-09-29T20:37:52.825Z"} -->