RTS
Qwen 3.8 Flash · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 50,317 bytes · SHA-256 3f90ba9c791a
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>FRONTIER WAR — Helion Expedition vs Korvathi Dominion</title>
<style>
:root{
--cy:#3fe8d8; --cy2:#1b8f8a; --am:#ffc247; --en:#ff5140; --en2:#8a1f16;
--bg:#05090c; --pn:#0b1418cc; --ln:#1e3b40; --tx:#cfece8;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{width:100%;height:100%;overflow:hidden;background:var(--bg);color:var(--tx);
font:12px/1.35 "Segoe UI",Roboto,Helvetica,Arial,sans-serif;user-select:none;-webkit-user-select:none}
#app{position:fixed;inset:0}
canvas#gl,canvas#fx{position:absolute;inset:0;display:block}
canvas#fx{pointer-events:none}
#hud{position:absolute;inset:0;pointer-events:none}
.panel{pointer-events:auto;background:linear-gradient(180deg,#0e1c21ee,#071013ee);
border:1px solid var(--ln);box-shadow:0 0 0 1px #0009, inset 0 1px 0 #2c5b6244, 0 6px 22px #0008;
clip-path:polygon(0 8px,8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%)}
/* top bar */
#topbar{position:absolute;top:8px;left:50%;transform:translateX(-50%);display:flex;gap:14px;align-items:center;
padding:6px 14px}
.stat{display:flex;align-items:baseline;gap:5px;letter-spacing:.08em;font-size:11px;color:#7fb8b4}
.stat b{font-size:16px;color:var(--cy);font-weight:700;text-shadow:0 0 8px #3fe8d855}
#hSup.warn b{color:var(--en);animation:blink .8s infinite}
@keyframes blink{50%{opacity:.35}}
#topbtns{display:flex;gap:6px}
button{pointer-events:auto;cursor:pointer;font:inherit;color:var(--tx);background:#12262c;border:1px solid var(--ln);
padding:4px 9px;letter-spacing:.06em}
button:hover{background:#1b3d46;border-color:var(--cy2)}
button:disabled{opacity:.45;cursor:default}
button.prime{background:#145a55;border-color:var(--cy);color:#dffcf8}
/* minimap */
#mmwrap{position:absolute;top:8px;right:8px;width:224px;height:224px;padding:4px}
#mm{width:216px;height:216px;display:block;image-rendering:pixelated;background:#000;cursor:crosshair;pointer-events:auto}
#mmlegend{display:flex;justify-content:space-between;font-size:9px;color:#6d9d99;padding:2px 3px 0;letter-spacing:.06em}
/* log */
#log{position:absolute;top:52px;left:50%;transform:translateX(-50%);width:460px;text-align:center;pointer-events:none}
#log div{margin:3px 0;padding:3px 8px;background:#000a;border-left:2px solid var(--cy);display:inline-block;
font-size:11px;letter-spacing:.04em;animation:fadeout .4s}
#log div.bad{border-color:var(--en);color:#ffc0b8}
#log div.good{border-color:var(--am);color:#ffe6b0}
@keyframes fadeout{from{opacity:0;transform:translateY(-6px)}}
/* selection panel */
#selpanel{position:absolute;left:8px;bottom:8px;width:322px;min-height:118px;max-height:230px;padding:8px;overflow:hidden}
#selhead{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--ln);padding-bottom:4px;margin-bottom:6px}
#selhead span{letter-spacing:.14em;font-size:10px;color:#79b5b1}
#selgrid{display:flex;flex-wrap:wrap;gap:4px;max-height:150px;overflow:auto}
.selchip{display:flex;align-items:center;gap:6px;padding:3px 6px;background:#0d1b20;border:1px solid #1c383f;cursor:pointer}
.selchip:hover{border-color:var(--cy)}
.selchip i{width:14px;height:14px;display:block;flex:0 0 14px;clip-path:polygon(50% 0,100% 50%,50% 100%,0 50%)}
.selchip b{font-size:11px}
.selchip em{font-style:normal;font-size:10px;color:#6fa5a1}
/* command card */
#cmdcard{position:absolute;right:8px;bottom:8px;width:340px;padding:8px}
#cmdtabs{display:flex;gap:4px;margin-bottom:6px}
#cmdtabs button{flex:1;font-size:10px;letter-spacing:.1em;padding:3px 0}
#cmdbody{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;max-height:194px;overflow:auto;padding-right:2px}
.ccbtn{position:relative;padding:6px 6px 6px 8px;min-height:56px;background:#0f2027;border:1px solid #23454c;cursor:pointer;
display:flex;flex-direction:column;justify-content:space-between;overflow:hidden}
.ccbtn:hover{background:#173340;border-color:var(--cy)}
.ccbtn.off{opacity:.55;cursor:default;background:#101a1d}
.ccbtn.off:hover{border-color:#23454c}
.ccbtn.act{border-color:var(--am);background:#26200f}
.ccbtn .n{font-size:10px;letter-spacing:.03em;color:#dff3f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ccbtn .c{font-size:10px;color:var(--am)}
.ccbtn .k{position:absolute;top:2px;right:3px;font-size:9px;color:#5f9a95}
.ccbtn .bar{position:absolute;left:0;bottom:0;height:3px;background:var(--cy);width:0%}
.ccbtn .x{position:absolute;top:1px;right:1px;font-size:11px;color:#ff9a8f;padding:0 3px;pointer-events:none}
#tip{position:absolute;pointer-events:none;background:#04100fee;border:1px solid var(--cy2);padding:6px 8px;max-width:280px;
font-size:11px;display:none;z-index:40;box-shadow:0 4px 16px #000c}
#tip h4{font-size:11px;color:var(--cy);letter-spacing:.08em;margin-bottom:3px}
#tip .r{color:#ff9a8f}
/* screens */
.screen{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:auto;
background:radial-gradient(120% 90% at 50% 40%,#0a1b20ee,#01050799 70%,#000e)}
.screen.hidden{display:none}
.card{width:min(900px,92vw);max-height:88vh;overflow:auto;padding:22px 26px}
.card h1{font-size:38px;letter-spacing:.16em;color:var(--cy);text-shadow:0 0 24px #3fe8d855;line-height:1}
.card h1 small{display:block;font-size:12px;letter-spacing:.34em;color:#79b5b1;margin-top:8px}
.card h2{font-size:14px;letter-spacing:.12em;color:var(--am);margin:16px 0 6px}
.cols{display:grid;grid-template-columns:1fr 1fr 1fr;gap:18px;margin-top:10px}
.cols ul{list-style:none}
.cols li{margin:4px 0;padding-left:14px;position:relative;font-size:11.5px;color:#b9ded9}
.cols li:before{content:"▸";position:absolute;left:0;color:var(--cy2)}
.cols b{color:#fff}
.keys{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:3px 12px}
.keys div{font-size:11px;color:#9fd0cb}
.keys kbd{background:#16333a;border:1px solid #2b5b62;padding:0 5px;font:10px monospace;color:#e8fffb;margin-right:5px}
#btnrow{display:flex;gap:10px;margin-top:18px;align-items:center}
#btnrow button{font-size:15px;padding:9px 22px;letter-spacing:.14em}
.factionline{display:flex;gap:16px;margin-top:12px;font-size:11px}
.factionline div{padding:8px 12px;border:1px solid var(--ln);flex:1}
.f1{color:var(--cy)}.f2{color:var(--en)}
#endtitle{font-size:44px;letter-spacing:.2em}
.win{color:var(--cy)}.lose{color:var(--en)}
#loadnote{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#000;color:var(--cy);
letter-spacing:.2em;font-size:13px;z-index:60}
</style>
</head>
<body>
<div id="app">
<canvas id="gl"></canvas>
<canvas id="fx"></canvas>
<div id="hud">
<div id="topbar" class="panel">
<div class="stat" id="hOreBox">ORE <b id="hOre">0</b></div>
<div class="stat" id="hSup">SUPPLY <b id="hSupV">0/0</b></div>
<div class="stat">UNITS <b id="hPop">0</b></div>
<div class="stat">T <b id="hTime">0:00</b></div>
<div id="topbtns">
<button id="bHelp" title="F1">HELP</button>
<button id="bMute" title="M">SFX ON</button>
<button id="bPause" title="Esc">PAUSE</button>
</div>
</div>
<div id="log"></div>
<div id="mmwrap" class="panel"><canvas id="mm" width="216" height="216"></canvas>
<div id="mmlegend"><span>LMB camera · RMB order</span><span>SHIFT+LMB pin</span></div>
</div>
<div id="selpanel" class="panel"><div id="selhead"><span>NO SELECTION</span><em id="selsum"></em></div><div id="selgrid"></div></div>
<div id="cmdcard" class="panel"><div id="cmdtabs"></div><div id="cmdbody"></div></div>
<div id="tip"></div>
</div>
<div id="title" class="screen">
<div class="card panel">
<h1>FRONTIER WAR<small>CLAIM OF THE SEVENTH WORLD · REAL-TIME STRATEGY</small></h1>
<div class="factionline">
<div><b class="f1">HELION EXPEDITION</b> — you. One Command Nexus, four Dozer drones, 400 ore. Build, harvest <b>ore</b>, expand, field an army.</div>
<div><b class="f2">KORVATHI DOMINION</b> — rival colonists on the far side of the valley. They harvest, expand, research and attack with the same rules you have.</div>
</div>
<div class="cols">
<div><h2>OBJECTIVE</h2><ul>
<li><b>Win:</b> destroy every enemy <b>Command Nexus</b>.</li>
<li><b>Lose:</b> your last Nexus falls.</li>
<li>You may build extra Nexus as expansions.</li>
<li>First ~80 seconds both sides are developing — scout and build in peace.</li>
</ul></div>
<div><h2>THE LOOP</h2><ul>
<li>Dozers harvest <b>ore</b> from finite spire fields and return it to a Nexus or Supply Depot.</li>
<li><b>Supply</b> is reusable capacity: Depots raise the cap. Production pauses if you run out.</li>
<li>Foundry = ground units · Aerie = aircraft · Cipher Lab = upgrades · Sentry Pylon = guns.</li>
<li>Expand to distant ore fields: long trips cost output, drop-offs fix that.</li>
</ul></div>
<div><h2>CONTROLS</h2><div class="keys">
<div><kbd>LMB</kbd>select / box</div><div><kbd>SHIFT</kbd>add to sel</div>
<div><kbd>RMB</kbd>move/attack/gather</div><div><kbd>A</kbd>attack-move</div>
<div><kbd>S</kbd>stop <kbd>H</kbd>hold</div><div><kbd>W</kbd>patrol</div>
<div><kbd>1-9</kbd>ctrl groups</div><div><kbd>WASD</kbd>pan camera</div>
<div><kbd>Q</kbd><kbd>E</kbd>rotate</div><div><kbd>WHEEL</kbd>zoom</div>
<div><kbd>MMB</kbd>drag pan</div><div><kbd>F1</kbd>help <kbd>M</kbd>mute</div>
<div><kbd>T</kbd>select all Dozers</div><div><kbd>Y</kbd>select all combat</div>
<div><kbd>ESC</kbd>pause</div><div><kbd>SPACE</kbd>jump to contact</div>
</div></div>
</div>
<div id="btnrow"><button class="prime" id="bStart">DEPLOY TO FRONTIER</button>
<button id="bStartFast">SKIP TO LATE WAR (debug)</button>
<span style="font-size:11px;color:#6d9d99">Edge-scroll on by default. Audio is generated live — no downloads.</span></div>
</div>
</div>
<div id="pause" class="screen hidden"><div class="card panel" style="width:auto;text-align:center">
<h1 style="font-size:26px">PAUSED</h1>
<div id="btnrow" style="justify-content:center">
<button class="prime" id="bResume">RESUME</button><button id="bHelp2">HELP</button><button id="bRestart">RESTART MATCH</button>
</div></div></div>
<div id="help" class="screen hidden"><div class="card panel">
<h1 style="font-size:22px">FIELD MANUAL</h1>
<div class="cols">
<div><h2>COUNTERS</h2><ul>
<li><b>Kinetic</b> (Vanguard) shreds <b>Light</b> armour, bounces off <b>Heavy</b>.</li>
<li><b>Energy</b> (Pulse Lancer, pylons) burns <b>Light</b> and <b>Shielded</b> — your answer to aircraft.</li>
<li><b>Explosive</b> (Juggernaut) splash damage melts <b>Heavy</b> armour and crowds.</li>
<li>Stingers hit ground only: bring Lancers or pylons or they feast.</li>
</ul></div>
<div><h2>ECONOMY</h2><ul>
<li>Ore fields run dry. 8 fields are on this map — take the contested middle.</li>
<li>Dozer costs 50, carries 14 (21 with Extractor Boost).</li>
<li>Depot: 80 ore → +8 supply and a nearby drop-off.</li>
<li>Cancel a queued item for a 75% refund; supply is released instantly.</li>
</ul></div>
<div><h2>FOG OF WAR</h2><ul>
<li>Black = never seen. Dim = explored, out of sight. Lit = currently seen.</li>
<li>Enemy units vanish when they leave your vision; old building outlines stay until you look again.</li>
<li>Kites (air scouts) see 24 — cheap early intel, big later.</li>
</ul></div>
</div>
<div id="btnrow" style="justify-content:center"><button class="prime" id="bHelpClose">CLOSE</button></div>
</div></div>
<div id="end" class="screen hidden"><div class="card panel" style="text-align:center;width:auto;min-width:520px">
<h1 id="endtitle">VICTORY</h1><div id="endbody" style="margin-top:10px;font-size:12px;color:#a9d8d3"></div>
<div id="btnrow" style="justify-content:center"><button class="prime" id="bRestart2">NEW MATCH</button></div></div></div>
<div id="loadnote">GENERATING FRONTIER WORLD…</div>
</div>
<script>
"use strict";
/* ================= FRONTIER WAR ================= */
/* ---- config ---- */
var CFG={W:400,CELL:4,GW:100,GH:100,FOG:200,START_ORE:400,CARRY:14,REFUND:0.75,GRACE:80};
var GW=CFG.GW,GH=CFG.GH,CELL=CFG.CELL,FOGN=CFG.FOG,FOGC=CFG.W/FOGN;
var HPX=CFG.W/CELL, HPZ=CFG.GH/CELL; /* path grid dims */
/* ---- math/util ---- */
function clamp(v,a,b){return v<a?a:(v>b?b:v)}
function lerp(a,b,t){return a+(b-a)*t}
function dist2(ax,az,bx,bz){var dx=ax-bx,dz=az-bz;return dx*dx+dz*dz}
function d01(ax,az,bx,bz){return Math.sqrt(dist2(ax,az,bx,bz))}
function smoothstep(t){return t*t*(3-2*t)}
var _sd=987654321;
function rnd(){_sd=(_sd*1103515245+12345)>>>0;return _sd/4294967296}
function rr(a,b){return a+rnd()*(b-a)}
function ri(a,b){return Math.floor(a+rnd()*(b-a+0.9999))}
function hash2(x,y){var n=(x*374761393+y*668265263)|0;n=(n^(n>>13))*1274126177|0;n=(n^(n>>16))>>>0;return n/4294967296}
function vnoise(x,y){var xi=Math.floor(x),yi=Math.floor(y),xf=x-xi,yf=y-yi;
var u=smoothstep(xf),v=smoothstep(yf);
return lerp(lerp(hash2(xi,yi),hash2(xi+1,yi),u),lerp(hash2(xi,yi+1),hash2(xi+1,yi+1),u),v)}
function fbm(x,y,o){var s=0,a=.5,f=1;for(var i=0;i<(o||4);i++){s+=a*vnoise(x*f,y*f);f*=2.07;a*=.5}return s}
/* ---- mat4 ---- */
function m4(){return new Float32Array(16)}
function m4id(o){o[0]=1;o[1]=0;o[2]=0;o[3]=0;o[4]=0;o[5]=1;o[6]=0;o[7]=0;o[8]=0;o[9]=0;o[10]=1;o[11]=0;o[12]=0;o[13]=0;o[14]=0;o[15]=1;return o}
function m4mul(o,a,b){for(var i=0;i<4;i++){var b0=b[i*4],b1=b[i*4+1],b2=b[i*4+2],b3=b[i*4+3];
o[i*4]=a[0]*b0+a[4]*b1+a[8]*b2+a[12]*b3;
o[i*4+1]=a[1]*b0+a[5]*b1+a[9]*b2+a[13]*b3;
o[i*4+2]=a[2]*b0+a[6]*b1+a[10]*b2+a[14]*b3;
o[i*4+3]=a[3]*b0+a[7]*b1+a[11]*b2+a[15]*b3}return o}
function m4persp(o,fovy,asp,zn,zf){var f=1/Math.tan(fovy/2),nf=1/(zn-zf);
o[0]=f/asp;o[1]=0;o[2]=0;o[3]=0;o[4]=0;o[5]=f;o[6]=0;o[7]=0;o[8]=0;o[9]=0;o[10]=(zf+zn)*nf;o[11]=-1;o[12]=0;o[13]=0;o[14]=2*zf*zn*nf;o[15]=0;return o}
function m4lookAt(o,ex,ey,ez,cx,cy,cz,ux,uy,uz){
var zx=ex-cx,zy=ey-cy,zz=ez-cz,l=Math.sqrt(zx*zx+zy*zy+zz*zz)||1;zx/=l;zy/=l;zz/=l;
var xx=uy*zz-uz*zy,xy=uz*zx-ux*zz,xz=ux*zy-uy*zx;l=Math.sqrt(xx*xx+xy*xy+xz*xz)||1;xx/=l;xy/=l;xz/=l;
var yx=zy*xz-zz*xy,yy=zz*xx-zx*xz,yz=zx*xy-zy*xx;
o[0]=xx;o[1]=yx;o[2]=zx;o[3]=0;o[4]=xy;o[5]=yy;o[6]=zy;o[7]=0;o[8]=xz;o[9]=yz;o[10]=zz;o[11]=0;
o[12]=-(xx*ex+xy*ey+xz*ez);o[13]=-(yx*ex+yy*ey+yz*ez);o[14]=-(zx*ex+zy*ey+zz*ez);o[15]=1;return o}
function m4inv(o,a){var a00=a[0],a01=a[1],a02=a[2],a03=a[3],a10=a[4],a11=a[5],a12=a[6],a13=a[7],
a20=a[8],a21=a[9],a22=a[10],a23=a[11],a30=a[12],a31=a[13],a32=a[14],a33=a[15];
var b00=a00*a11-a01*a10,b01=a00*a12-a02*a10,b02=a00*a13-a03*a10,b03=a01*a12-a02*a11,b04=a01*a13-a03*a11,
b05=a02*a13-a03*a12,b06=a20*a31-a21*a30,b07=a20*a32-a22*a30,b08=a20*a33-a23*a30,b09=a21*a32-a22*a31,
b10=a21*a33-a23*a31,b11=a22*a33-a23*a32;
var det=b00*b11-b01*b10+b02*b09+b03*b08-b04*b07+b05*b06; if(!det)return m4id(o); det=1/det;
o[0]=(a11*b11-a12*b10+a13*b09)*det;o[1]=(a02*b10-a01*b11-a03*b09)*det;o[2]=(a31*b05-a32*b04+a33*b03)*det;
o[3]=(a22*b04-a21*b05-a23*b03)*det;o[4]=(a12*b08-a10*b11-a13*b07)*det;o[5]=(a00*b11-a02*b08+a03*b07)*det;
o[6]=(a30*b04-a32*b06-a33*b02)*det;o[7]=(a22*b06-a20*b04+a23*b02)*det;o[8]=(a10*b10-a11*b08+a13*b06)*det;
o[9]=(a01*b08-a00*b10-a03*b06)*det;o[10]=(a32*b01-a30*b05+a33*b00)*det;o[11]=(a20*b05-a22*b01-a23*b00)*det;
o[12]=(a11*b07-a10*b09-a12*b05)*det;o[13]=(a00*b09-a01*b07+a02*b05)*det;o[14]=(a30*b03-a31*b02+a32*b04)*det;
o[15]=(a21*b02-a20*b04-a22*b03)*det;return o}
function m4xform(o,m,x,y,z,w){var rx=m[0]*x+m[4]*y+m[8]*z+m[12]*w,ry=m[1]*x+m[5]*y+m[9]*z+m[13]*w,
rz=m[2]*x+m[6]*y+m[10]*z+m[14]*w,rw=m[3]*x+m[7]*y+m[11]*z+m[15]*w;
if(rw){o[0]=rx/rw;o[1]=ry/rw;o[2]=rz/rw;o[3]=rw}else{o[0]=rx;o[1]=ry;o[2]=rz;o[3]=1}return o}
/* DOM helpers */
function $(s){return document.querySelector(s)}
function el(tag,cls,html){var e=document.createElement(tag);if(cls)e.className=cls;if(html!=null)e.innerHTML=html;return e}
function fmtTime(t){var m=Math.floor(t/60),s=Math.floor(t%60);return m+":"+(s<10?"0":"")+s}
function logMsg(txt,kind){var d=el("div",kind||"",txt);$("#log").appendChild(d);
var items=$("#log").children; while(items.length>4) $("#log").removeChild(items[0]);
setTimeout(function(){ if(d.parentNode) d.parentNode.removeChild(d) },4200)}
</script>
<script>
/* ==================== RENDERER ==================== */
var glcv=$("#gl"), fxcv=$("#fx"), gl=null, fx=null, VW=0, VH=0, DPR=1;
function initGL(){
var opts={antialias:false,alpha:false,depth:true,powerPreference:"high-performance"};
gl=glcv.getContext("webgl2",opts)||glcv.getContext("webgl",opts)||glcv.getContext("experimental-webgl",opts);
if(!gl) return false;
var ext1=gl.getExtension("OES_element_index_uint"), extI=gl.getExtension("ANGLE_instanced_arrays");
R.div=gl.instancedAttribDivisor?function(l,d){gl.instancedAttribDivisor(l,d)}:
(extI?function(l,d){extI.vertexAttribDivisorANGLE(l,d)}:null);
R.drawI=gl.drawArraysInstanced?function(m,f,c,n){gl.drawArraysInstanced(m,f,c,n)}:
(extI?function(m,f,c,n){extI.drawArraysInstancedANGLE(m,f,c,n)}:null);
R.drawEI=gl.drawElementsInstanced?function(m,c,t,o,n){gl.drawElementsInstanced(m,c,t,o,n)}:
(extI?function(m,c,t,o,n){extI.drawElementsInstancedANGLE(m,c,t,o,n)}:null);
if(!R.div||!R.drawEI) return false;
fx=fxcv.getContext("2d");
return true;
}
var R={div:null,drawI:null,drawEI:null};
function sh(type,src){var s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);
if(!gl.getShaderParameter(s,gl.COMPILE_STATUS)){logMsg("SHADER: "+gl.getShaderInfoLog(s),"bad");console.error(gl.getShaderInfoLog(s),src)}
return s}
function prog(vs,fs,attrs,iattrs){
var p=gl.createProgram();gl.attachShader(p,sh(gl.VERTEX_SHADER,vs));gl.attachShader(p,sh(gl.FRAGMENT_SHADER,fs));
gl.linkProgram(p);
if(!gl.getProgramParameter(p,gl.LINK_STATUS))logMsg("LINK: "+gl.getProgramInfoLog(p),"bad");
var o={p:p,u:{},a:{}};
var nu=gl.getProgramParameter(p,gl.ACTIVE_UNIFORMS);
for(var i=0;i<nu;i++){var inf=gl.getActiveUniform(p,i);o.u[inf.name]=gl.getUniformLocation(p,inf.name)}
for(var k in attrs)o.a[k]=gl.getAttribLocation(p,attrs[k]);
return o;
}
var PREC="#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n";
/* ---- instanced solid pass ---- */
var VS_INST=[
"attribute vec3 aPos;attribute vec3 aNor;attribute vec4 iX;attribute vec4 iS;attribute vec4 iC;",
"uniform mat4 uVP;uniform vec3 uOff;uniform vec3 uScale;uniform vec4 uCol;",
"varying vec3 vN;varying vec4 vC;varying vec3 vW;",
"void main(){vec3 p=aPos*uScale*iS.xyz+uOff*iS.xyz;",
"float c=cos(iX.w),s=sin(iX.w);",
"vec3 r=vec3(c*p.x+s*p.z,p.y,-s*p.x+c*p.z);",
"vec3 w=r+iX.xyz;",
"vN=vec3(aNor.x*c+aNor.z*s,aNor.y,-aNor.x*s+aNor.z*c);",
"vC=vec4(uCol.rgb*iC.rgb*(0.55+0.45*iC.w),uCol.a*iS.w);",
"vW=w;gl_Position=uVP*vec4(w,1.0);}"].join("\n"),
FS_INST=[PREC,
"varying vec3 vN;varying vec4 vC;varying vec3 vW;",
"uniform vec3 uLd;uniform vec3 uAmb;uniform vec3 uCam;uniform float uEmis;",
"void main(){vec3 n=normalize(vN);float d=max(dot(n,uLd),0.0);",
"float f=max(dot(n,normalize(uCam-vW)),0.0);",
"vec3 c=vC.rgb*(uAmb+uLdCol(d))+vec3(0.10)*f*f;",
"c+=vC.rgb*uEmis;",
"gl_FragColor=vec4(c,vC.a);}",""].join("\n").replace("uLdCol(d)","vec3(0.85)*d+vec3(0.25)*pow(d,3.0)");
/* ---- terrain ---- */
var VS_TER=["attribute vec3 aPos;attribute vec3 aNor;attribute vec3 aCol;","uniform mat4 uVP;uniform float uW;",
"varying vec3 vN;varying vec3 vC;varying vec2 vF;varying vec3 vW;",
"void main(){vF=aPos.xz/uW;vN=aNor;vC=aCol;vW=aPos;gl_Position=uVP*vec4(aPos,1.0);}"].join("\n"),
FS_TER=[PREC,"varying vec3 vN;varying vec3 vC;varying vec2 vF;varying vec3 vW;",
"uniform sampler2D uFog;uniform vec3 uLd;uniform vec3 uAmb;",
"void main(){vec4 f=texture2D(uFog,vF);float ex=f.r,vi=f.g;",
"float d=max(dot(normalize(vN),uLd),0.0);",
"vec3 lit=vC*(uAmb+vec3(0.92,0.95,0.9)*d);",
"float band=abs(fract(vW.y*0.75)-0.5)*2.0;",
"lit+=vec3(0.03,0.10,0.11)*smoothstep(0.86,1.0,band)*vi;",
"vec3 known=vC*0.17+vec3(0.012,0.035,0.045);",
"vec3 dark=vec3(0.004,0.008,0.012);",
"vec3 c=mix(dark,known,smoothstep(0.02,0.55,ex));",
"c=mix(c,lit,smoothstep(0.35,0.92,vi));",
"gl_FragColor=vec4(c,1.0);}"].join("\n");
/* ---- billboards (bars, glows, rings, ground decals) ---- */
var VS_BB=["attribute vec2 aQ;attribute vec4 bP;attribute vec4 bS;attribute vec4 bT;",
"uniform mat4 uVP;uniform vec3 uRight;uniform vec3 uUp;uniform float uMode;",
"varying vec2 vQ;varying vec4 vS;varying vec4 vT;",
"void main(){vec3 w;",
"if(uMode>2.5){w=bP.xyz+vec3(aQ.x*bS.x,0.0,aQ.y*bS.x);}","else{w=bP.xyz+uRight*(aQ.x*bS.x)+uUp*(aQ.y*bS.y);}",
"vQ=aQ;vS=bS;vT=bT;gl_Position=uVP*vec4(w,1.0);}"].join("\n"),
FS_BB=[PREC,"varying vec2 vQ;varying vec4 vS;varying vec4 vT;uniform float uMode;",
"void main(){float r=vT.z;",
"if(uMode<0.5){float t=(vQ.x+1.0)*0.5;",
" vec3 col=(t<r)?vec3(vS.z,vS.w,vT.x):vec3(0.02,0.03,0.04);",
" if(abs(vQ.y)>0.72) col=vec3(0.55,0.75,0.8)*0.55+col*0.25;",
" gl_FragColor=vec4(col,vT.y);}",
"else if(uMode<1.5){float d=length(vQ);float a=smoothstep(1.0,0.0,d);",
" gl_FragColor=vec4(vS.z,vS.w,vT.x,a*a*vT.y);}",
"else if(uMode<2.5){float d=length(vQ);float a=smoothstep(0.06,0.0,abs(d-0.78));",
" gl_FragColor=vec4(vS.z,vS.w,vT.x,a*vT.y);}",
"else{float d=length(vQ);float a=smoothstep(1.0,0.72,d)*0.55+smoothstep(0.05,0.0,abs(d-0.88))*0.9;",
" gl_FragColor=vec4(vS.z,vS.w,vT.x,a*vT.y*0.5);}}"].join("\n");
/* ================= GEOMETRY ================= */
function MB(){this.p=[];this.n=[];this.i=[]}
MB.prototype.v=function(x,y,z,nx,ny,nz){this.p.push(x,y,z);this.n.push(nx,ny,nz);return this.p.length/3-1};
MB.prototype.quad=function(a,b,c,d){this.i.push(a,b,c,a,c,d)};
MB.prototype.tri=function(a,b,c){this.i.push(a,b,c)};
MB.prototype.box=function(w,h,d,ox,oy,oz){ox=ox||0;oy=oy||0;oz=oz||0;
var x=w/2,y=h/2,z=d/2,V=this;
var f=[[[x,-y,-z],[x,y,-z],[x,y,z],[x,-y,z],[1,0,0]],[[-x,-y,z],[-x,y,z],[-x,y,-z],[-x,-y,-z],[-1,0,0]],
[[-x,-y,z],[x,-y,z],[x,y,z],[-x,y,z],[0,1,0]],[[-x,-y,-z],[-x,y,-z],[x,y,-z],[x,-y,-z],[0,-1,0]],
[[-x,y,-z],[x,y,-z],[x,y,z],[-x,y,z],[0,0,1]],[[x,y,-z],[-x,y,-z],[-x,y,z],[x,y,z],[0,0,-1]]];
for(var i=0;i<6;i++){var q=f[i],ids=[];
for(var k=0;k<4;k++)ids.push(V.v(q[k][0]+ox,q[k][1]+oy,q[k][2]+oz,q[4][0],q[4][1],q[4][2]));
V.quad(ids[0],ids[1],ids[2],ids[3])}
return this};
MB.prototype.cyl=function(r1,r2,h,seg,ox,oy,oz,flat){ox=ox||0;oy=oy||0;oz=oz||0;
var V=this,base=V.p.length/3;
for(var i=0;i<seg;i++){var a0=i/seg*6.28318,a1=(i+1)/seg*6.28318;
var c0=Math.cos(a0),s0=Math.sin(a0),c1=Math.cos(a1),s1=Math.sin(a1);
var nx=(c0+c1)/2,nz=(s0+s1)/2,l=Math.sqrt(nx*nx+nz*nz)||1;
var p0=V.v(ox+c0*r1,oy,h/2,0,1,0),p1=V.v(ox+c1*r1,oy+r1*0,h/2,0,1,0);
var v0=V.v(ox+c0*r1,oy,-h/2,nx/l,0,nz/l),v1=V.v(ox+c1*r1,oy,-h/2,nx/l,0,nz/l);
var t0=V.v(ox+c0*r1,oy,h/2,nx/l,0,nz/l),t1=V.v(ox+c1*r1,oy,h/2,nx/l,0,nz/l);
V.quad(v0,v1,t1,t0);
if(r2>0){var u0=V.v(ox+c0*r2,oy,h/2,c0*0.6,0.8,c1*0.0),u1=V.v(ox+c1*r2,oy,h/2,c1*0.6,0.8,s1*0.0);
V.tri(u0,u1,t1);V.tri(u0,t1,t0)}
if(r1>0){var w0=V.v(ox+c0*r1,oy,-h/2,c0*0.6,-0.8,s0*0.0),w1=V.v(ox+c1*r1,oy,-h/2,c1*0.6,-0.8,s1*0.0);
V.tri(w0,w1,v0)}
}
return this};
MB.prototype.cone=function(r,h,seg,ox,oy,oz){ox=ox||0;oy=oy||0;oz=oz||0;var V=this;
var ap=V.v(ox,oy+h,oz,0,1,0);
for(var i=0;i<seg;i++){var a0=i/seg*6.28318,a1=(i+1)/seg*6.28318;
var v0=V.v(ox+Math.cos(a0)*r,oy,oz+Math.sin(a0)*r,Math.cos(a0)*0.6,0.5,Math.sin(a0)*0.6);
var v1=V.v(ox+Math.cos(a1)*r,oy,oz+Math.sin(a1)*r,Math.cos(a1)*0.6,0.5,Math.sin(a1)*0.6);
V.tri(v0,v1,ap)}
return this};
MB.prototype.octa=function(rx,ry,rz,ox,oy,oz){ox=ox||0;oy=oy||0;oz=oz||0;var V=this;
var px=V.v(ox+rx,oy,oz,1,0,0),nx=V.v(ox-rx,oy,oz,-1,0,0),py=V.v(ox,oy+ry,oz,0,1,0),ny=V.v(ox,oy-ry,oz,0,-1,0),
pz=V.v(ox,oy,oz+rz,0,0,1),nz=V.v(ox,oy,oz-rz,0,0,-1);
V.tri(px,py,pz);V.tri(pz,py,nx);V.tri(nx,py,nz);V.tri(nz,py,px);
V.tri(px,nz,ny);V.tri(nz,pz,ny);V.tri(pz,nx,ny);V.tri(nx,px,ny);return V};
MB.prototype.wedge=function(w,h,d,ox,oy,oz){ox=ox||0;oy=oy||0;oz=oz||0;var V=this;
var a=V.v(ox-w/2,oy-h/2,oz-d/2,0,-1,0),b=V.v(ox+w/2,oy-h/2,oz-d/2,0,-1,0),
c=V.v(ox+w/2,oy-h/2,oz+d/2,0,-1,0),dd=V.v(ox-w/2,oy-h/2,oz+d/2,0,-1,0);
var t=V.v(ox-w/2,oy+h/2,oz-d/2,-1,0.5,0);
var t2=V.v(ox+w/2,oy+h/2,oz-d/2,1,0.5,0);
V.quad(a,b,dd,c);V.tri(a,dd,t);V.tri(b,t2,c);V.quad(t,t2,c,dd);
V.tri(a,t,b);V.tri(dd,c,t2);V.tri(a,t2,t);V.tri(a,dd,t2);return V};
MB.prototype.finish=function(){
var vb=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,vb);
var arr=[],P=this.p,N=this.n;
for(var i=0;i<P.length/3;i++){arr.push(P[i*3],P[i*3+1],P[i*3+2],N[i*3],N[i*3+1],N[i*3+2])}
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(arr),gl.STATIC_DRAW);
var ib=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ib);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint32Array(this.i),gl.STATIC_DRAW);
return {vb:vb,ib:ib,count:this.i.length};
};
var MESH={};
function buildMeshes(){
var b;
b=new MB();b.box(1,1,1);MESH.box=b.finish();
b=new MB();b.box(1,0.6,1);MESH.lobox=b.finish();
b=new MB();b.cyl(0.5,0.42,1,8);MESH.cyl=b.finish();
b=new MB();b.cyl(0.5,0.0,1,6);MESH.cone=b.finish();
b=new MB();b.cyl(0.35,0.5,1,10);MESH.tube=b.finish();
b=new MB();b.octa(0.5,0.5,0.5);MESH.octa=b.finish();
b=new MB();b.cone(0.5,1,5);MESH.spire=b.finish();
b=new MB();b.cone(0.5,1,4);MESH.spire4=b.finish();
b=new MB();b.octa(0.7,0.4,0.7);MESH.rock=b.finish();
b=new MB();b.wedge(1,1,1);MESH.wedge=b.finish();
b=new MB();b.box(1,0.15,0.25);MESH.gun=b.finish();
b=new MB();b.box(0.3,1,0.3);MESH.pole=b.finish();
b=new MB();b.cyl(0.6,0.3,0.5,6);MESH.rotor=b.finish();
b=new MB();b.box(1,0.35,0.6);MESH.plate=b.finish();
b=new MB();b.box(0.5,0.5,1.6);MESH.wing=b.finish();
}
/* ---- instance group buffers ---- */
function Group(){this.buf=gl.createBuffer();this.data=new Float32Array(2048*12);this.n=0;this.cap=2048}
Group.prototype.reset=function(){this.n=0};
Group.prototype.push=function(px,py,pz,yaw,sx,sy,sz,al,r,g,b,e){
if(this.n>=this.cap){this.cap*=2;this.data=new Float32Array(this.cap*12)}
var d=this.data,i=this.n*12;
d[i]=px;d[i+1]=py;d[i+2]=pz;d[i+3]=yaw;d[i+4]=sx;d[i+5]=sy;d[i+6]=sz;d[i+7]=al;
d[i+8]=r;d[i+9]=g;d[i+10]=b;d[i+11]=e;this.n++};
Group.prototype.upload=function(){gl.bindBuffer(gl.ARRAY_BUFFER,this.buf);
gl.bufferData(gl.ARRAY_BUFFER,this.data.subarray(0,this.n*12),gl.DYNAMIC_DRAW)};
var BBGroups=[];
function makeBBGroup(){
var g={buf:gl.createBuffer(),data:new Float32Array(4096*12),n:0,cap:4096};
g.reset=function(){g.n=0};
g.push=function(px,py,pz,hw,hh,r,gr,bl,a,ra,ex){var i=g.n*12,d=g.data;
d[i]=px;d[i+1]=py;d[i+2]=pz;d[i+3]=1;
d[i+4]=hw;d[i+5]=hh;d[i+6]=r;d[i+7]=gr;
d[i+8]=bl;d[i+9]=a;d[i+10]=ra;d[i+11]=ex;g.n++};
g.upload=function(){gl.bindBuffer(gl.ARRAY_BUFFER,g.buf);
gl.bufferData(gl.ARRAY_BUFFER,g.data.subarray(0,g.n*12),gl.DYNAMIC_DRAW)};
return g}
/* ---- pass setup ---- */
var P_INST,P_TER,P_BB,QUAD,FOGT,FOGTDATA,groups={},bbBars,bbFx,bbGround;
function initRender(){
P_INST=prog(VS_INST,FS_INST,["aPos","aNor","iX","iS","iC"]);
P_TER=prog(VS_TER,FS_TER,["aPos","aNor","aCol"]);
P_BB=prog(VS_BB,FS_BB,["aQ","bP","bS","bT"]);
var b=new MB();b.p=[-1,-1,0,1,-1,0,1,1,0,-1,1,0];b.n=[0,1,0,0,1,0,0,1,0,0,1,0];b.i=[0,1,2,0,2,3];QUAD=b.finish();
FOGT=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,FOGT);
FOGTDATA=new Uint8Array(FOGN*FOGN*4);
gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,FOGN,FOGN,0,gl.RGBA,gl.UNSIGNED_BYTE,FOGTDATA);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
bbBars=makeBBGroup();bbFx=makeBBGroup();bbGround=makeBBGroup();
gl.enable(gl.DEPTH_TEST);gl.enable(gl.CULL_FACE);gl.cullFace(gl.BACK);
gl.clearColor(0.004,0.008,0.012,1);
}
function bindInstAttrs(g){
gl.bindBuffer(gl.ARRAY_BUFFER,g.buf);
var S=48;
gl.enableVertexAttribArray(P_INST.a.aPos);gl.vertexAttribPointer(P_INST.a.aPos,3,gl.FLOAT,false,S,0);R.div(P_INST.a.aPos,0);
gl.enableVertexAttribArray(P_INST.a.aNor);gl.vertexAttribPointer(P_INST.a.aNor,3,gl.FLOAT,false,S,12);R.div(P_INST.a.aNor,0);
gl.bindBuffer(gl.ARRAY_BUFFER,g.buf);
gl.enableVertexAttribArray(P_INST.a.iX);gl.vertexAttribPointer(P_INST.a.iX,4,gl.FLOAT,false,S,0);R.div(P_INST.a.iX,1);
gl.enableVertexAttribArray(P_INST.a.iS);gl.vertexAttribPointer(P_INST.a.iS,4,gl.FLOAT,false,S,16);R.div(P_INST.a.iS,1);
gl.enableVertexAttribArray(P_INST.a.iC);gl.vertexAttribPointer(P_INST.a.iC,4,gl.FLOAT,false,S,32);R.div(P_INST.a.iC,1);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,null);
}
function bindMesh(m){
gl.bindBuffer(gl.ARRAY_BUFFER,m.vb);
gl.enableVertexAttribArray(P_INST.a.aPos);gl.vertexAttribPointer(P_INST.a.aPos,3,gl.FLOAT,false,24,0);R.div(P_INST.a.aPos,0);
gl.enableVertexAttribArray(P_INST.a.aNor);gl.vertexAttribPointer(P_INST.a.aNor,3,gl.FLOAT,false,24,12);R.div(P_INST.a.aNor,0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,m.ib);
}
/* draw one mesh-part for all instances in group g with part offset/scale/colour */
function drawPart(g,mesh,off,scl,col,mode){
if(!g.n)return;
gl.uniform3fv(P_INST.u.uOff,off);gl.uniform3fv(P_INST.u.uScale,scl);gl.uniform4f(P_INST.u.uCol,col[0],col[1],col[2],col[3]===undefined?1:col[3]);
gl.uniform1f(P_INST.u.uEmis,mode||0);
bindMesh(mesh);bindInstAttrs(g);
R.drawEI(gl.TRIANGLES,mesh.count,gl.UNSIGNED_INT,0,g.n);
}
function bindBBA(g){
gl.bindBuffer(gl.ARRAY_BUFFER,QUAD.vb);
gl.enableVertexAttribArray(P_BB.a.aQ);gl.vertexAttribPointer(P_BB.a.aQ,2,gl.FLOAT,false,12,0);R.div(P_BB.a.aQ,0);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,QUAD.ib);
gl.bindBuffer(gl.ARRAY_BUFFER,g.buf);
gl.enableVertexAttribArray(P_BB.a.bP);gl.vertexAttribPointer(P_BB.a.bP,4,gl.FLOAT,false,48,0);R.div(P_BB.a.bP,1);
gl.enableVertexAttribArray(P_BB.a.bS);gl.vertexAttribPointer(P_BB.a.bS,4,gl.FLOAT,false,48,16);R.div(P_BB.a.bS,1);
gl.enableVertexAttribArray(P_BB.a.bT);gl.vertexAttribPointer(P_BB.a.bT,4,gl.FLOAT,false,48,32);R.div(P_BB.a.bT,1);
}
function drawBB(g,mode,additive){
if(!g.n)return;
gl.uniform1f(P_BB.u.uMode,mode);
if(additive){gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE);gl.depthMask(false)}
else{gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.depthMask(false)}
bindBBA(g);
R.drawI(gl.TRIANGLES,0,4,g.n);
gl.depthMask(true);gl.disable(gl.BLEND);
}
</script>
<script>
/* ==================== WORLD ==================== */
var HRES=201,HSTEP=CFG.W/(HRES-1);
var HM=new Float32Array(HRES*HRES);
var BLK=new Uint8Array(GW*GH); /* 1 = ground blocked */
var ROCKC=new Uint8Array(GW*GH); /* rock/cliff marker */
var oreFields=[],rocks=[],decor=[];
var SPAWN=[{x:70,z:70},{x:330,z:330}];
var STARTS=[[150,150],[250,250]]; /* ore fields near each start */
function hIdx(x,z){var i=clamp(Math.round(x/HSTEP),0,HRES-1),j=clamp(Math.round(z/HSTEP),0,HRES-1);return j*HRES+i}
function heightAt(x,z){
var fx=x/HSTEP,fz=z/HSTEP,i=Math.floor(fx),j=Math.floor(fz);
i=clamp(i,0,HRES-2);j=clamp(j,0,HRES-2);
var tx=clamp(fx-i,0,1),tz=clamp(fz-j,0,1);
var a=HM[j*HRES+i],b=HM[j*HRES+i+1],c=HM[(j+1)*HRES+i],d=HM[(j+1)*HRES+i+1];
return lerp(lerp(a,b,tx),lerp(c,d,tx),tz);
}
function cellOf(x,z){return {i:clamp(Math.floor(x/CELL),0,GW-1),j:clamp(Math.floor(z/CELL),0,GH-1)}}
function blkAt(i,j){if(i<0||j<0||i>=GW||j>=GH)return 1;return BLK[j*GW+i]}
function blkWorld(x,z){var c=cellOf(x,z);return blkAt(c.i,c.j)}
var TERRAIN=null;
function genWorld(){
oreFields.length=0;rocks.length=0;decor.length=0;
/* --- height --- */
for(var j=0;j<HRES;j++)for(var i=0;i<HRES;i++){
var x=i*HSTEP,z=j*HSTEP;
var n=fbm(x*0.0055,z*0.0055,5);
var ridge=Math.abs(fbm(x*0.0031+40,z*0.0031-17,3)*2-1);
var h=(n-0.5)*13+ (1-ridge)*7 - 2;
/* bowl: rim of map rises */
var dx=(x-CFG.W/2)/(CFG.W/2),dz=(z-CFG.W/2)/(CFG.W/2);
h+=Math.pow(clamp(Math.sqrt(dx*dx+dz*dz),0,1),3)*16;
HM[j*HRES+i]=h;
}
/* --- block mask: cliffs + ridges + craters --- */
for(var jj=0;jj<GH;jj++)for(var ii=0;ii<GW;ii++){
var wx=ii*CELL+CELL/2,wz=jj*CELL+CELL/2;
var gx=(heightAt(wx+CELL,wz)-heightAt(wx-CELL,wz)),gz=(heightAt(wx,wz+CELL)-heightAt(wx,wz-CELL));
var slope=Math.sqrt(gx*gx+gz*gz)/(2*CELL);
var rv=fbm(wx*0.009+120,wz*0.009-60,3);
var b=0;
if(slope>0.62)b=1;
if(rv>0.635)b=1;
if(wx<8||wz<8||wx>CFG.W-8||wz>CFG.W-8)b=1;
BLK[jj*GW+ii]=b;ROCKC[jj*GW+ii]=b?1:0;
}
/* craters (impassable lakes) */
var lakes=[{x:200,z:120,r:26},{x:120,z:280,r:22}];
for(var L=0;L<lakes.length;L++){var lk=lakes[L];
for(var j2=0;j2<GH;j2++)for(var i2=0;i2<GW;i2++){
var d=d01(i2*CELL+2,j2*CELL+2,lk.x,lk.z);
if(d<lk.r){BLK[j2*GW+i2]=1;ROCKC[j2*GW+i2]=2;
var f=1-d/lk.r;HM[hIdx(i2*CELL+2,j2*CELL+2)]=lerp(HM[hIdx(i2*CELL+2,j2*CELL+2)],-4.5,f*f*0.9)}
}
}
/* --- ore fields --- */
var spots=[[70,70],[330,330],[200,60],[60,200],[340,200],[200,340],[150,150],[250,250],[200,200],[90,320],[320,90]];
var used=0;
for(var s=0;s<spots.length&&oreFields.length<8;s++){
var sx=spots[s][0]+rr(-10,10),sz=spots[s][1]+rr(-10,10);
var tooClose=false;
for(var q=0;q<oreFields.length;q++)if(d01(oreFields[q].x,oreFields[q].z,sx,sz)<52)tooClose=true;
for(var sp=0;sp<2;sp++)if(d01(SPAWN[sp].x,SPAWN[sp].z,sx,sz)<44)tooClose=true;
if(tooClose)continue;
var rad=rr(8,11),amt=ri(1600,3200);
var cry=[];
for(var c=0;c<14;c++){
var a=rnd()*6.283,r2=Math.sqrt(rnd())*rad;
var cx=sx+Math.cos(a)*r2,cz=sz+Math.sin(a)*r2;
cry.push({x:cx,z:cz,h:rr(2.6,5.2),w:rr(1.1,2.0),yaw:rnd()*6.28});
}
oreFields.push({x:sx,z:sz,r:rad,amount:amt,max:amt,cry:cry,id:oreFields.length,alive:true});
}
/* guarantee two starter fields */
for(var st=0;st<2;st++){
var f=oreFields[st];if(!f)continue;
var dx=f.x-SPAWN[st].x,dz=f.z-SPAWN[st].z,d=Math.sqrt(dx*dx+dz*dz);
if(d>70){f.x=SPAWN[st].x+dx/d*58;f.z=SPAWN[st].z+dz/d*58;
for(var cc=0;cc<f.cry.length;cc++){f.cry[cc].x=f.x+rr(-f.r,f.r);f.cry[cc].z=f.z+rr(-f.r,f.r)}}
}
/* --- clear spawn plates + corridors between POIs --- */
for(var p=0;p<2;p++)clearPlate(SPAWN[p].x,SPAWN[p].z,20);
for(var o=0;o<oreFields.length;o++)clearPlate(oreFields[o].x,oreFields[o].z,oreFields[o].r+5);
var nodes=[[SPAWN[0].x,SPAWN[0].z],[SPAWN[1].x,SPAWN[1].z]];
for(var o2=0;o2<oreFields.length;o2++)nodes.push([oreFields[o2].x,oreFields[o2].z]);
for(var a1=0;a1<nodes.length;a1++)for(var b1=a1+1;b1<nodes.length;b1++){
var A=nodes[a1],B=nodes[b1];var dd=d01(A[0],A[1],B[0],B[1]);
if(dd<74||rnd()<0.5)carve(A[0],A[1],B[0],B[1]);
}
buildTerrainMesh();
/* rock instances on ridge cells */
for(var j3=0;j3<GH;j3++)for(var i3=0;i3<GW;i3++){
if(!BLK[j3*GW+i3])continue;
var wx2=i3*CELL+2,wz2=j3*CELL+2;
if(hash2(i3,j3)>0.30)continue;
if(ROCKC[j3*GW+i3]===2&&hash2(i3+7,j3+3)>0.18)continue;
rocks.push({x:wx2+rr(-1.4,1.4),z:wz2+rr(-1.4,1.4),s:rr(2.6,5.4),h:rr(1.8,6.5),yaw:rr(0,6.28),t:ROCKC[j3*GW+i3]});
}
}
function clearPlate(x,z,r){
var i0=Math.floor((x-r)/CELL),i1=Math.floor((x+r)/CELL),j0=Math.floor((z-r)/CELL),j1=Math.floor((z+r)/CELL);
for(var j=Math.max(0,j0);j<=Math.min(GH-1,j1);j++)for(var i=Math.max(0,i0);i<=Math.min(GW-1,i1);i++){
var wx=i*CELL+2,wz=j*CELL+2;
if(d01(wx,wz,x,z)<r){BLK[j*GW+i]=0;
var hh=heightAt(wx,wz),t=1-d01(wx,wz,x,z)/r;
HM[hIdx(wx,wz)]=lerp(hh,clamp(hh,-1,2),t*0.7)}
}
}
function carve(x0,z0,x1,z1){
var d=d01(x0,z0,x1,z1),n=Math.ceil(d/4);
for(var k=0;k<=n;k++){
var t=k/n,x=lerp(x0,x1,t),z=lerp(z0,z1,t);
var c=cellOf(x,z);
for(var dj=-1;dj<=1;dj++)for(var di=-1;di<=1;di++){
var i=c.i+di,j=c.j+dj;if(i<1||j<1||i>=GW-1||j>=GH-1)continue;
BLK[j*GW+i]=0;
var wx=i*CELL+2,wz=j*CELL+2;
HM[hIdx(wx,wz)]=HM[hIdx(wx,wz)]*0.55+lerp(HM[hIdx(x0,z0)],HM[hIdx(x1,z1)],t)*0.45;
}
}
}
function buildTerrainMesh(){
var N=GW+1,pos=[],nor=[],col=[],idx=[];
for(var j=0;j<N;j++)for(var i=0;i<N;i++){
var x=i*CELL,z=j*CELL,y=heightAt(x,z);
pos.push(x,y,z);
var hx=(heightAt(x+CELL,z)-heightAt(x-CELL,z))/(2*CELL),hz=(heightAt(x,z+CELL)-heightAt(x,z-CELL))/(2*CELL);
var l=Math.sqrt(hx*hx+1+hz*hz)||1;nor.push(-hx/l,1/l,-hz/l);
var t=clamp((y+6)/26,0,1),rv=fbm(x*0.02,z*0.02,2);
var r,g,b;
if(y<-2.2){r=0.05;g=0.13;b=0.16}
else if(y<0.6){r=0.16;g=0.20;b=0.16}
else if(y<6){r=0.19+rv*0.06;g=0.24+rv*0.05;b=0.17}
else{r=0.26+rv*0.05;g=0.27;b=0.30}
/* dust-orange veins in lowlands */
if(rv>0.62&&y<6){r+=0.13;g+=0.05;b-=0.03}
col.push(r,g,b);
}
for(var j2=0;j2<N-1;j2++)for(var i2=0;i2<N-1;i2++){
var a=j2*N+i2,bb=a+1,c=a+N,d=c+1;idx.push(a,c,bb,bb,c,d);
}
var vb=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,vb);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(pos),gl.STATIC_DRAW);
var nb=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,nb);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(nor),gl.STATIC_DRAW);
var cb=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,cb);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(col),gl.STATIC_DRAW);
var ib=gl.createBuffer();gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,ib);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint32Array(idx),gl.STATIC_DRAW);
TERRAIN={vb:vb,nb:nb,cb:cb,ib:ib,count:idx.length};
}
/* ==================== FOG OF WAR ==================== */
var fog=[{exp:new Uint8Array(FOGN*FOGN),stamp:new Uint32Array(FOGN*FOGN)},
{exp:new Uint8Array(FOGN*FOGN),stamp:new Uint32Array(FOGN*FOGN)}];
var vgen=1;
function stampVision(f,x,z,r){
var g=fog[f],c0=Math.floor((x-r)/FOGC),c1=Math.floor((x+r)/FOGC),d0=Math.floor((z-r)/FOGC),d1=Math.floor((z+r)/FOGC);
var r2=r*r;
for(var j=Math.max(0,d0);j<=Math.min(FOGN-1,d1);j++){
var dz=(j+0.5)*FOGC-z;
for(var i=Math.max(0,c0);i<=Math.min(FOGN-1,c1);i++){
var dx=(i+0.5)*FOGC-x;
if(dx*dx+dz*dz<=r2){var k=j*FOGN+i;g.stamp[k]=vgen;g.exp[k]=1}
}
}
}
function seen(f,x,z){var i=clamp(Math.floor(x/FOGC),0,FOGN-1),j=clamp(Math.floor(z/FOGC),0,FOGN-1);
return fog[f].stamp[j*FOGN+i]===vgen}
function explored(f,x,z){var i=clamp(Math.floor(x/FOGC),0,FOGN-1),j=clamp(Math.floor(z/FOGC),0,FOGN-1);
return fog[f].exp[j*FOGN+i]===1}
function updateFogTex(){
var g=fog[0],e=g.exp,s=g.stamp,D=FOGTDATA;
for(var k=0;k<FOGN*FOGN;k++){
var v=(k%FOGN)|0,u=(k/FOGN)|0;
var vi=(s[k]===vgen)?255:0;
D[(u*FOGN+v)*4]=e[k]?255:0;D[(u*FOGN+v)*4+1]=vi;D[(u*FOGN+v)*4+2]=0;D[(u*FOGN+v)*4+3]=255;
}
gl.bindTexture(gl.TEXTURE_2D,FOGT);gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,FOGN,FOGN,gl.RGBA,gl.UNSIGNED_BYTE,FOGTDATA);
}
/* ==================== CAMERA / PICKING ==================== */
var CAM={x:200,z:200,yaw:0.0,dist:78,pitch:0.82,eye:[0,0,0],tgt:[0,0,0]};
var VP=m4(),PROJ=m4(),VIEW=m4(),IVP=m4(),_tmp=m4();
var view=[0,0,0],up=[0,1,0],fwd=[0,0,0];
function camUpdate(){
CAM.x=clamp(CAM.x,10,CFG.W-10);CAM.z=clamp(CAM.z,10,CFG.W-10);
CAM.dist=clamp(CAM.dist,22,230);
var cp=Math.cos(CAM.pitch),sp=Math.sin(CAM.pitch);
var ty=heightAt(CAM.x,CAM.z);
var ex=CAM.x+Math.sin(CAM.yaw)*cp*CAM.dist, ez=CAM.z+Math.cos(CAM.yaw)*cp*CAM.dist;
var ey=ty+sp*CAM.dist;
var minY=heightAt(ex,ez)+5;
if(ey<minY)ey=minY;
CAM.eye=[ex,ey,ez];CAM.tgt=[CAM.x,ty,CAM.z];
m4lookAt(VIEW,ex,ey,ez,CAM.x,ty,CAM.z,0,1,0);
m4persp(PROJ,0.72,VW/VH||1,2,900);
m4mul(VP,PROJ,VIEW);m4inv(IVP,VP);
var d=[CAM.x-ex,CAM.z-ez];var l=Math.sqrt(d[0]*d[0]+d[1]*d[1])||1;
fwd=[d[0]/l,0,d[1]/l];
view=[-d[0]/l,0,-d[1]/l];
up=[0,1,0];
}
function screenRay(sx,sy){
var nx=sx/VW*2-1,ny=1-sy/VH*2;
var p=[0,0,0,1];
m4xform(p,IVP,nx,ny,-1,1);var a=[p[0],p[1],p[2]];
m4xform(p,IVP,nx,ny,1,1);var b=[p[0],p[1],p[2]];
var dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2],l=Math.sqrt(dx*dx+dy*dy+dz*dz)||1;
return {o:a,d:[dx/l,dy/l,dz/l]};
}
function pickGround(sx,sy){
var r=screenRay(sx,sy);
if(r.d[1]>-0.02)return {x:CAM.x,z:CAM.z};
var t=(0-r.o[1])/r.d[1],x=r.o[0]+r.d[0]*t,z=r.o[2]+r.d[2]*t;
for(var k=0;k<5;k++){
var y=heightAt(x,z);
var tt=(y-r.o[1])/r.d[1];
x=r.o[0]+r.d[0]*tt;z=r.o[2]+r.d[2]*tt;
}
return {x:clamp(x,0,CFG.W),z:clamp(z,0,CFG.W),ok:true};
}
function worldToScreen(x,y,z){
var p=[0,0,0,1];m4xform(p,VP,x,y,z,1);
return {x:(p[0]*0.5+0.5)*VW,y:(1-(p[1]*0.5+0.5))*VH,w:p[3]};
}
</script>
<script>
/* ==================== DATA TABLES ==================== */
var FCOL=[[0.30,0.95,0.86],[1.0,0.34,0.24]];
var FCOL2=[[0.05,0.35,0.34],[0.35,0.08,0.05]];
var FNAME=["HELION EXPEDITION","KORVATHI DOMINION"];
var DK={kin:0,ene:1,exp:2}, AR={light:0,heavy:1,shield:2};
/* damage-type vs armour multiplier */
var MOD=[[1.00,0.50,0.90],[1.30,0.85,1.25],[1.05,1.45,0.70]];
var W=[1,1,1],D=[0.30,0.34,0.40],M=[0.66,0.70,0.74],G=[1.35,1.25,0.85],R0=[0.14,0.16,0.19];
/* ---- unit types ---- */
var UT={
drone:{n:["Dozer","Hauler"],k:"worker",cost:50,sup:1,time:9,hp:45,arm:AR.light,rad:0.95,spd:6.4,sight:12,fly:0,
dmg:0,rng:0,rate:1,dt:0,tg:0,parts:[["box",0,0.42,0,1.25,0.55,1.5,W],["box",0,0.9,-0.2,0.8,0.6,0.8,D],
["cyl",0,0.5,0.85,0.5,0.9,0.5,M],["box",0.7,0.75,0.35,0.25,0.5,0.7,M],["box",-0.7,0.75,0.35,0.25,0.5,0.7,M]]},
vanguard:{n:["Vanguard","Ripper"],k:"ground",cost:60,sup:1,time:7,hp:80,arm:AR.light,rad:0.85,spd:7.4,sight:13,fly:0,
dmg:10,dt:DK.kin,rng:2.2,rate:0.85,tg:1,parts:[["box",0,0.75,0,0.9,0.95,0.7,W],["octa",0,1.42,0,0.55,0.6,0.55,D],
["gun",0.55,0.95,0.35,0.35,0.35,1.9,M],["box",0.62,0.95,0,0.3,0.65,0.9,D],["box",-0.62,0.95,0,0.3,0.65,0.9,D],
["cyl",0.3,0.2,0,0.4,0.5,0.4,R0],["cyl",-0.3,0.2,0,0.4,0.5,0.4,R0]]},
lancer:{n:["Pulse Lancer","Spine Gunner"],k:"ground",cost:105,sup:2,time:11,hp:60,arm:AR.light,rad:0.85,spd:5.6,sight:16,fly:0,
dmg:12,dt:DK.ene,rng:8.5,rate:1.35,tg:3,parts:[["box",0,0.8,0,0.8,1.0,0.6,W],["octa",0,1.5,0.05,0.5,0.55,0.6,D],
["pole",0,1.05,0.9,0.26,0.26,3.2,M],["box",0,1.05,-0.45,0.7,0.7,0.5,G],["cyl",0.34,0.2,0,0.4,0.55,0.4,R0],
["cyl",-0.34,0.2,0,0.4,0.55,0.4,R0]]},
brute:{n:["Juggernaut","Gorewalker"],k:"ground",cost:230,sup:3,time:20,hp:340,arm:AR.heavy,rad:1.5,spd:3.3,sight:12,fly:0,
dmg:28,dt:DK.exp,rng:6.5,rate:2.1,spl:3.2,tg:1,parts:[["box",0,1.35,0,2.3,1.5,2.0,W],["box",0,2.35,-0.1,1.5,0.8,1.4,D],
["gun",0,1.5,1.0,0.7,0.7,2.6,M],["cyl",0.85,0.45,0,0.75,1.0,0.75,R0],["cyl",-0.85,0.45,0,0.75,1.0,0.75,R0],
["cyl",0.85,0.45,0.9,0.6,0.9,0.6,M],["cyl",-0.85,0.45,0.9,0.6,0.9,0.6,M],["box",1.15,1.9,0,0.35,1.3,1.6,D],
["box",-1.15,1.9,0,0.35,1.3,1.6,D]]},
stinger:{n:["Stinger","Hornet"],k:"air",cost:150,sup:2,time:13,hp:95,arm:AR.shield,rad:1.1,spd:9.5,sight:15,fly:1,alt:6.5,
dmg:15,dt:DK.ene,rng:6.0,rate:1.05,tg:1,parts:[["octa",0,0,0,1.5,0.7,2.2,W],["wing",0,0.1,0.6,0.9,0.3,1.0,D],
["wing",0,0.1,-0.9,0.9,0.3,1.0,D],["rotor",0.95,0.35,0,1.5,0.5,1.5,M],["rotor",-0.95,0.35,0,1.5,0.5,1.5,M],
["cone",0,0,-1.6,0.6,0.9,0.6,G]]},
kite:{n:["Kite","Gazer"],k:"air",cost:75,sup:1,time:8,hp:45,arm:AR.shield,rad:0.9,spd:12.5,sight:26,fly:1,alt:8.0,
dmg:0,dt:DK.ene,rng:0,rate:1,tg:0,parts:[["octa",0,0,0,1.3,0.5,1.3,W],["rotor",0,0.5,0,1.0,0.4,1.0,M],
["pole",0,-0.4,0,0.3,0.8,0.3,D],["cone",0,-0.2,0.7,0.45,0.5,0.45,G]]}
};
var UKEYS=["drone","vanguard","lancer","brute","stinger","kite"];
/* ---- building types ---- */
var BT={
hq:{n:["Command Nexus","Broodhold"],cost:500,time:48,hp:1400,arm:AR.heavy,fp:5,sup:12,sight:18,drop:1,
makes:["drone","vanguard","lancer","brute"],key:"B",
parts:[["box",0,2.2,0,17,4.4,17,W],["box",0,5.2,0,11,2.2,11,D],["cyl",0,7.6,0,7,3.2,7,M],
["pole",6,7,6,1.0,9,1.0,D],["pole",-6,7,-6,1.0,9,1.0,D],["cone",0,9.2,0,4.5,3.4,4.5,G],
["box",0,0.6,8.6,7,1.4,3.4,M],["box",8.6,0.6,0,3.4,1.4,7,M],["box",-8.6,0.6,0,3.4,1.4,7,M],["box",0,0.6,-8.6,7,1.4,3.4,M]]},
depot:{n:["Supply Depot","Hatchery"],cost:80,time:12,hp:420,arm:AR.heavy,fp:3,sup:8,sight:7,drop:1,key:"V",
parts:[["box",0,1.6,0,10,3.2,10,W],["cyl",2.6,4.4,2.6,3,3.4,3,M],["cyl",-2.6,4.4,-2.6,3,3.4,3,M],
["cyl",2.6,4.4,-2.6,2.4,3.0,2.4,D],["box",0,3.6,5.2,5,1.2,1.6,G]]},
foundry:{n:["Foundry","Forge"],cost:210,time:26,hp:760,arm:AR.heavy,fp:4,sup:0,sight:9,key:"F",
makes:["vanguard","lancer","brute"],
parts:[["box",0,2.6,0,14,5.2,12,W],["wedge",0,5.8,0,13,2.6,11,D],["cyl",-4.4,7.6,-3,2.2,4.0,2.2,M],
["cyl",-2.4,7.4,-3,1.8,3.6,1.8,M],["box",5.4,2.2,5.2,4,4.4,1.6,G],["box",0,1.0,6.4,6,2,1.4,R0]]},
aerie:{n:["Aerie","Roost"],cost:265,time:30,hp:640,arm:AR.heavy,fp:4,sup:0,sight:11,key:"N",
makes:["stinger","kite"],
parts:[["box",0,1.0,0,15,2,15,W],["cyl",0,3.4,0,4,3,4,D],["cone",0,5.0,0,3.4,3.2,3.4,G],
["pole",6.4,3,6.4,0.9,5,0.9,M],["pole",-6.4,3,-6.4,0.9,5,0.9,M],["box",0,2.4,7.4,8,0.6,2.2,G]]},
lab:{n:["Cipher Lab","Hex Spire"],cost:230,time:28,hp:560,arm:AR.heavy,fp:3,sup:0,sight:10,key:"G",
parts:[["cyl",0,2.6,0,10,5.2,10,W],["octa",0,6.6,0,7,4.6,7,M],["pole",0,8.6,0,0.9,4,0.9,D],
["cone",0,6.0,0,3,2.6,3,G],["box",4.4,2.2,0,1.4,3,6,D]]},
turret:{n:["Sentry Pylon","Thorn Turret"],cost:140,time:16,hp:480,arm:AR.heavy,fp:2,sup:0,sight:14,key:"T",
dmg:14,dt:DK.ene,rng:12.5,rate:1.0,tg:3,
parts:[["box",0,1.2,0,6,2.4,6,D],["pole",0,3.4,0,2.2,3.4,2.2,M],["octa",0,5.6,0,3.2,2.2,3.2,W],
["gun",0,5.6,1.6,0.9,0.9,3.6,M],["cone",0,6.8,0,1.6,1.6,1.6,G]]}
};
var BKEYS=["hq","depot","foundry","aerie","lab","turret"];
/* ---- upgrades ---- */
var UP=[
{key:"extract",n:"Extractor Boost",cost:250,time:40,d:"Dozer ore carry 14 → 21."},
{key:"plasma",n:"Plasma Shells",cost:300,time:45,d:"Pulse Lancers and Sentry Pylons deal +30% damage."},
{key:"ablative",n:"Ablative Plating",cost:280,time:45,d:"All units gain +22% maximum integrity."},
{key:"servo",n:"Overdrive Servos",cost:260,time:40,d:"Ground units move +25% faster."},
{key:"deepscan",n:"Deep Scan Array",cost:220,time:35,d:"Vision range +25% for everything you own."},
{key:"hardened",n:"Hardened Frames",cost:320,time:50,d:"Buildings gain +30% maximum integrity."}
];
/* ==================== GAME STATE ==================== */
var G,units=[],buildings=[],projs=[],fx=[],ghosts=[],byId={};
var OCC=new Uint8Array(GW*GH),OCCBY=new Int32Array(GW*GH);
function newFaction(f){return {f:f,ore:CFG.START_ORE,supCap:0,supUsed:0,res:{},
queue:[],prod:[],tech:[],alive:true,armyRally:{x:0,z:0},scoutPts:[],lastAtk:0,waves:0,plan:{},
known:{},threat:0}}
function resetState(){
units.length=0;buildings.length=0;projs.length=0;fx.length=0;ghosts.length=0;byId={};
for(var k=0;k<OCC.length;k++){OCC[k]=0;OCCBY[k]=-1}
G={state:"title",time:0,sel:[],groups:{},msg:0,over:false,winner:-1,frame:0,
f:[newFaction(0),newFaction(1)],placing:null,paused:false,speed:1,
selBox:null,orderMarks:[],cursor:{x:0,z:0,valid:false},edgeScroll:true,
camFollow:false,lastCombat:0,alertCd:0,kills:[0,0],built:[0,0]};
_sd=987654321;genWorld();
CAM.x=SPAWN[0].x;CAM.z=SPAWN[0].z;CAM.yaw=Math.atan2(SPAWN[1].x-SPAWN[0].x,SPAWN[1].z-SPAWN[0].z);
for(var f=0;f<2;f++){
var s=SPAWN[f];
addBuilding(f,"hq",s.x,s.z,true);
for(var i=0;i<4;i++)addUnit(f,"drone",s.x+rr(-9,9),s.z+rr(-9,9));
var g=fog[f];stampVision(f,s.x,s.z,30);
G.f[f].armyRally={x:s.x+(f===0?-14:14),z:s.z+(f===0?-14:14)};
}
vgen=1;updateFogTex();
}
var _id=1;
function addUnit(f,t,x,z){
var d=UT[t],u={id:_id++,kind:"u",f:f,t:t,x:x,z:z,y:0,alt:d.fly?d.alt:0,yaw:rr(0,6.28),
hp:d.hp,maxhp:d.hp,ord:"idle",ox:0,oz:0,tgt:null,path:null,pi:0,cd:rr(0,0.6),
carry:0,job:null,flash:0,sel:0,anim:rnd()*6.28,stuck:0,lastx:x,lastz:z,atkT:0,hold:false,
leash:null,dead:false,spawn:0};
applyTechUnit(u);
units.push(u);byId[u.id]=u;
G.f[f].supUsed+=d.sup;
return u;
}
function applyTechUnit(u){
var d=UT[u.t],r=G.f[u.f].res;
u.maxhp=Math.round(d.hp*(r.ablative?1.22:1));
if(u.hp>u.maxhp)u.hp=u.maxhp;
u.spd=d.spd*((!d.fly&&r.servo)?1.25:1);
u.sight=d.sight*(r.deepscan?1.25:1);
u.dmg=d.dmg*(r.plasma&&u.t==="lancer"?1.3:1);
}
function addBuilding(f,t,x,z,instant){
var d=BT[t],fp=d.fp,rad=fp*CELL/2;
var b={id:_id++,kind:"b",f:f,t:t,x:x,z:z,y:heightAt(x,z),yaw:(f?0.6:-0.6),
hp:d.hp*(r0(f)?1.3:1),maxhp:d.hp*(r0(f)?1.3:1),rad:rad,ord:"build",
done:!!instant,prog:instant?1:0,total:d.time,queue:[],research:null,rt:0,
cd:rr(0,1),workers:[],rally:null,sel:0,flash:0,cells:[],dead:false,kyaw:0};
b.maxhp=b.hp=Math.round(d.hp*(G.f[f].res.hardened?1.3:1));
buildings.push(b);byId[b.id]=b;
if(instant)markCells(b);else markCells(b);
G.built[f]++;
return b;
}
function r0(){return false}
function cellKey(i,j){return j*GW+i}
function markCells(b){
var fp=BT[b.t].fp,c=cellOf(b.x,b.z),i0=c.i-Math.floor(fp/2),j0=c.j-Math.floor(fp/2);
b.cells.length=0;
for(var j=j0;j<j0+fp;j++)for(var i=i0;i<i0+fp;i++){
if(i<0||j<0||i>=GW||j>=GH)continue;
OCC[j*GW+i]=1;OCCBY[j*GW+i]=b.id;b.cells.push(j*GW+i)}
}
function unmarkCells(b){for(var k=0;k<b.cells.length;k++){OCC[b.cells[k]]=0;OCCBY[b.cells[k]]=-1}}
function canPlace(x,z,fp){
if(x<fp*CELL||z<fp*CELL||x>CFG.W-fp*CELL||z>CFG.W-fp*CELL)return 0;
var c=cellOf(x,z),i0=c.i-Math.floor(fp/2),j0=c.j-Math.floor(fp/2);
var near=false;
for(var j=j0;j<j0+fp;j++)for(var i=i0;i<i0+fp;i++){
if(i<1||j<1||i>=GW-1||j>=GH-1)return 2;
if(OCC[j*GW+i])return 2;
if(BLK[j*GW+i])return 3;
var wx=i*CELL+2,wz=j*CELL+2;
for(var q=0;q<oreFields.length;q++){var of=oreFields[q];
if(of.alive&&d01(wx,wz,of.x,of.z)<of.r+1)return 4}
if(explored(0,wx,wz))near=true;
}
return near?1:5;
}
function supplyInfo(f){
var cap=0,used=G.f[f].supUsed;
for(var k=0;k<buildings.length;k++){var b=buildings[k];
if(b.f===f&&!b.dead&&b.done)cap+=BT[b.t].sup||0}
G.f[f].supCap=cap;return {used:used,cap:cap}
}
</script>