{"connections":[{"sourceLocalID":2,"sourcePortID":"outlet_output","targetLocalID":0,"targetPortID":"inlet_input"}],"hideGrid":false,"name":"Neon Crawl","nodes":[{"config":{"chrome":"titleBar","closeIsDismiss":true,"height":860,"html":"<!doctype html><html lang='en'><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>Neon Crawl</title><style>:root{color-scheme:dark;font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#ecf4ff;background:#080e1a}*{box-sizing:border-box}body{margin:0;overflow:hidden}canvas{display:block;width:100vw;height:100vh;touch-action:none}button,select,input{font:inherit}button,select{border:1px solid #ffffff24;border-radius:12px;background:#182337;color:#eaf3ff;padding:10px 14px}button{cursor:pointer;font-weight:700}button:hover{background:#24364e}button:disabled{opacity:.4;cursor:default}button:focus-visible,select:focus-visible,input:focus-visible{outline:2px solid #61e8f0;outline-offset:4px}.top{position:absolute;left:24px;right:24px;top:22px;display:flex;gap:16px;align-items:center;justify-content:space-between}.brand{font-size:18px;font-weight:850;letter-spacing:-.7px}.brand small{display:block;font-size:10px;letter-spacing:3px;color:#8799b3;margin-top:4px}.tools{display:flex;gap:8px;align-items:center}.tag{font:11px ui-monospace,monospace;letter-spacing:1px;color:#92aec8}.score{position:absolute;top:91px;left:24px;right:24px;display:flex;justify-content:space-between;pointer-events:none}.card{width:220px;padding:14px 16px;border:1px solid #ffffff18;border-radius:16px;background:#0d1728dc;backdrop-filter:blur(12px)}.card b{font-size:12px;letter-spacing:1px}.card span{float:right;font:12px ui-monospace,monospace}.bar{margin-top:11px;height:5px;background:#ffffff10;border-radius:10px;overflow:hidden}.bar i{display:block;height:100%;width:100%;background:#62e5ef;transition:width .4s}.enemy .bar i{background:#ff9972}.wind{position:absolute;top:91px;left:50%;transform:translateX(-50%);text-align:center;background:#0d1728d9;padding:10px 20px;border:1px solid #ffffff18;border-radius:16px;min-width:190px}.wind small{font-size:9px;letter-spacing:2px;color:#8fa8c2}.wind strong{display:block;font:18px ui-monospace,monospace;margin-top:5px}.wind em{display:block;font-size:10px;color:#8fa8c2;font-style:normal;margin-top:4px}.intro{position:absolute;left:50%;top:41%;transform:translate(-50%,-50%);text-align:center;pointer-events:none;text-shadow:0 4px 28px #000}.intro small{font:11px ui-monospace,monospace;letter-spacing:5px;color:#72e2eb}.intro h1{font-size:clamp(30px,5vw,68px);margin:12px 0;letter-spacing:-3px}.intro p{color:#bed0e5;font-size:13px}.bottom{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);width:min(790px,calc(100% - 36px));border:1px solid #ffffff22;border-radius:22px;background:#0c1627ed;box-shadow:0 18px 80px #0006;backdrop-filter:blur(16px);padding:17px 22px}.status{display:flex;justify-content:space-between;gap:12px;margin-bottom:15px;align-items:center}.status b{font-size:12px;color:#63e5ef}.status span{font-size:11px;color:#9aadc6;text-align:right}.controls{display:grid;grid-template-columns:1fr 1fr 150px;gap:26px;align-items:center}.control label{display:flex;justify-content:space-between;font-size:10px;letter-spacing:2px;color:#9cb2c9}.control output{font:20px ui-monospace,monospace;letter-spacing:-1px;color:#fff}input[type=range]{width:100%;accent-color:#67e2ed;margin:12px 0 4px;cursor:pointer}.fire{height:57px;background:#65e5e9;color:#07232f;border:0;font-size:13px;letter-spacing:1px}.fire:hover{background:#a0fbff}.hint{text-align:center;color:#748ba6;font-size:10px;margin:12px 0 0}.modal{position:absolute;inset:0;background:#050b16a8;backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center}.modal[hidden],.intro[hidden]{display:none}.sheet{width:min(420px,90vw);padding:35px;border:1px solid #ffffff24;border-radius:24px;background:#101d31;box-shadow:0 30px 100px #0008;text-align:center}.sheet small{color:#63e5ef;letter-spacing:3px;font-size:10px}.sheet h2{font-size:33px;letter-spacing:-1px}.sheet p{color:#a7bdd4;line-height:1.7;font-size:13px}.sheet button{width:100%;margin-top:12px}.error{position:absolute;inset:0;display:grid;place-items:center;padding:30px;text-align:center;background:#0a1220;z-index:20}.error[hidden]{display:none}@media(max-width:720px){.top{left:14px;right:14px;top:14px}.brand{font-size:15px}.brand small{font-size:8px;letter-spacing:2px}.tools{gap:5px}.tools button,.tools select{font-size:10px;padding:8px}.tag{display:none}.score{left:14px;right:14px;top:75px}.card{width:140px;padding:11px}.wind{top:144px;min-width:150px;padding:8px}.wind strong{font-size:14px}.controls{gap:13px;grid-template-columns:1fr 1fr 95px}.bottom{padding:15px}.fire{font-size:11px}.status span{font-size:10px}.hint{font-size:9px}.intro h1{letter-spacing:-1px}}@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto}}</style></head><body><canvas id='scene' aria-label='3D destructible city artillery game'></canvas><header class='top'><div class='brand'>NEON / CRAWL<small>ROOFTOP ARTILLERY CLUB</small></div><div class='tools'><span class='tag' id='sector'>SECTOR 01</span><select id='mode' aria-label='Game mode'><option value='solo'>Solo vs Computer</option><option value='duo'>Two Players</option></select><button id='newCity'>New City</button><button id='save'>Save HTML</button></div></header><div class='score'><div class='card'><b style='color:#68e6ef'>MINT</b><span id='hp0'>100 HP</span><div class='bar'><i id='bar0'></i></div></div><div class='card enemy'><b style='color:#ffab87'>EMBER</b><span id='hp1'>100 HP</span><div class='bar'><i id='bar1'></i></div></div></div><div class='wind'><small>WIND ACCELERATION</small><strong id='wind'>→ 0.0 m/s²</strong><em id='windNote'>Changes each turn</em></div><div class='intro' id='intro'><small id='introSmall'>SECTOR 01</small><h1 id='cityName'>Neon Harbor</h1><p>New skyline. New footholds. Same old grudge.</p></div><section class='bottom'><div class='status'><b id='turn'>Surveying the skyline</b><span id='message' aria-live='polite'>Preparing a new city…</span></div><div class='controls'><div class='control'><label for='angle'>ANGLE <output id='angleValue'>45°</output></label><input id='angle' type='range' min='5' max='175' value='45' step='1'></div><div class='control'><label for='power'>POWER <output id='powerValue'>65%</output></label><input id='power' type='range' min='10' max='100' value='65' step='1'></div><button class='fire' id='fire'>FIRE ↗</button></div><p class='hint'>← → angle · ↑ ↓ power · Space fire · Dots show only the start of the arc · Angles are relative to facing your rival</p></section><div class='modal' id='modal' hidden><div class='sheet'><small id='resultTag'>SECTOR CLEARED</small><h2 id='resultTitle'>Mint takes the skyline</h2><p id='resultText'></p><button class='fire' id='next'>Next Skyline →</button></div></div><div class='error' id='error' hidden></div><script>(()=>{'use strict';const $=id=>document.getElementById(id),canvas=$('scene'),gl=canvas.getContext('webgl',{antialias:true,alpha:false,powerPreference:'high-performance'});if(!gl){$('error').hidden=false;$('error').textContent='This game needs WebGL. Save this HTML and open it in a browser with hardware acceleration enabled.';return;}const vertex=`attribute vec3 aPosition;attribute vec3 aNormal;attribute vec4 aColor;uniform mat4 uVP;varying vec3 vPosition;varying vec3 vNormal;varying vec4 vColor;void main(){vPosition=aPosition;vNormal=aNormal;vColor=aColor;gl_Position=uVP*vec4(aPosition,1.0);}`;const fragment=`precision mediump float;varying vec3 vPosition;varying vec3 vNormal;varying vec4 vColor;uniform vec3 uEye;uniform vec3 uFog;float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}void main(){vec3 n=normalize(vNormal);float light=0.40+0.60*max(dot(n,normalize(vec3(-0.5,0.9,0.65))),0.0);vec3 c=vColor.rgb*light;if(vColor.a>0.5&&abs(n.y)<0.3){float axis=abs(n.z)>0.5?vPosition.x:vPosition.z;vec2 p=vec2(axis,vPosition.y);vec2 f=fract(p);float window=step(0.22,f.x)*step(f.x,0.78)*step(0.28,f.y)*step(f.y,0.77);float lit=step(0.42,hash(floor(p)+vColor.r*71.0));vec3 glass=mix(vec3(0.025,0.08,0.13),mix(vec3(0.32,0.75,0.83),vec3(1.0,0.65,0.30),hash(floor(p)*0.7)),lit);c=mix(c,glass,window*0.83);c*=0.82+0.18*step(0.055,f.y);}float fog=smoothstep(65.0,155.0,length(uEye-vPosition));gl_FragColor=vec4(mix(c,uFog,fog*0.86),1.0);}`;function shader(type,src){const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))throw Error(gl.getShaderInfoLog(s));return s;}let program;try{program=gl.createProgram();gl.attachShader(program,shader(gl.VERTEX_SHADER,vertex));gl.attachShader(program,shader(gl.FRAGMENT_SHADER,fragment));gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))throw Error(gl.getProgramInfoLog(program));}catch(e){$('error').hidden=false;$('error').textContent='The 3D renderer could not start: '+e.message;return;}gl.useProgram(program);gl.enable(gl.DEPTH_TEST);const posLoc=gl.getAttribLocation(program,'aPosition'),normalLoc=gl.getAttribLocation(program,'aNormal'),colorLoc=gl.getAttribLocation(program,'aColor'),vpLoc=gl.getUniformLocation(program,'uVP'),eyeLoc=gl.getUniformLocation(program,'uEye'),fogLoc=gl.getUniformLocation(program,'uFog');const cityBuffer=gl.createBuffer(),groundBuffer=gl.createBuffer(),dynamicBuffer=gl.createBuffer();let cityCount=0,groundCount=0;const W=76,H=32,D=9,voxels=new Uint8Array(W*H*D),faceData=[[[1,0,0],[[1,0,0],[1,1,0],[1,1,1],[1,0,1]]],[[-1,0,0],[[0,0,1],[0,1,1],[0,1,0],[0,0,0]]],[[0,1,0],[[0,1,1],[1,1,1],[1,1,0],[0,1,0]]],[[0,-1,0],[[0,0,0],[1,0,0],[1,0,1],[0,0,1]]],[[0,0,1],[[1,0,1],[1,1,1],[0,1,1],[0,0,1]]],[[0,0,-1],[[0,0,0],[0,1,0],[1,1,0],[1,0,0]]]];const themes=[{name:'Neon Harbor',fog:[.035,.065,.12],colors:[[.15,.28,.37],[.22,.30,.42],[.20,.22,.35],[.16,.34,.36]]},{name:'Amber Heights',fog:[.10,.065,.095],colors:[[.38,.25,.23],[.31,.28,.34],[.43,.31,.25],[.29,.22,.35]]},{name:'Glacier District',fog:[.035,.10,.14],colors:[[.21,.36,.43],[.28,.38,.44],[.19,.30,.39],[.28,.32,.46]]},{name:'Violet Exchange',fog:[.09,.045,.15],colors:[[.30,.23,.43],[.23,.25,.41],[.34,.22,.35],[.20,.31,.40]]}];let theme=themes[0],level=1,seed=1,rng=Math.random,buildings=[],crawlers=[],phase='intro',phaseTime=0,active=0,wind=0,round=1,rocket=null,particles=[],trail=[],shake=0,clock=0,lastTime=0,accumulator=0,viewWidth=1,viewHeight=1,aiDelay=0,shotCount=0;const reducedMotion=matchMedia('(prefers-reduced-motion: reduce)').matches;function randomSeed(){seed=(Math.random()*4294967295)>>>0;let a=seed;rng=()=>{a+=0x6D2B79F5;let t=a;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return((t^(t>>>14))>>>0)/4294967296;};}function index(x,y,z){return(x*H+y)*D+z;}function cell(x,y,z){return x>=0&&x<W&&y>=0&&y<H&&z>=0&&z<D?voxels[index(x,y,z)]:0;}function pointSolid(x,y,z=0){if(y<0)return true;return!!cell(Math.floor(x+W/2),Math.floor(y),Math.floor(z+D/2));}function vertexPush(v,p,n,c,m=0){v.push(p[0],p[1],p[2],n[0],n[1],n[2],c[0],c[1],c[2],m);}function quad(v,q,n,c,m=0){for(const i of[0,1,2,0,2,3])vertexPush(v,q[i],n,c,m);}function box(v,x,y,z,sx,sy,sz,c,m=0){for(const[n,q]of faceData)quad(v,q.map(p=>[x+p[0]*sx,y+p[1]*sy,z+p[2]*sz]),n,c,m);}const sphereUnit=[];for(let j=0;j<5;j++)for(let i=0;i<8;i++){const p=(a,b)=>{const t=a/5*Math.PI,f=b/8*Math.PI*2;return[Math.sin(t)*Math.cos(f),Math.cos(t),Math.sin(t)*Math.sin(f)];};const q=[p(j,i),p(j+1,i),p(j+1,i+1),p(j,i+1)];for(const k of[0,1,2,0,2,3])sphereUnit.push(q[k]);}function sphere(v,x,y,z,r,c,stretch=1){for(const n of sphereUnit)vertexPush(v,[x+n[0]*r,y+n[1]*r*stretch,z+n[2]*r],n,c);}function upload(buffer,vertices){gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(vertices),gl.STATIC_DRAW);return vertices.length/10;}function rebuildCity(){const v=[];for(let x=0;x<W;x++)for(let y=0;y<H;y++)for(let z=0;z<D;z++){const id=cell(x,y,z);if(!id)continue;const c=theme.colors[(id-1)%theme.colors.length];for(const[n,q]of faceData)if(!cell(x+n[0],y+n[1],z+n[2]))quad(v,q.map(p=>[x-W/2+p[0],y+p[1],z-D/2+p[2]]),n,c,1);}cityCount=upload(cityBuffer,v);}function buildGround(){const v=[];box(v,-60,-1,-40,120,1,80,[.08,.12,.18]);box(v,-48,.01,7,96,.025,4,[.055,.075,.11]);box(v,-48,.01,-11,96,.025,4,[.055,.075,.11]);for(let x=-46;x<47;x+=4){box(v,x,.055,8.9,1.8,.015,.12,[.64,.57,.35]);box(v,x,.055,-9.1,1.8,.015,.12,[.64,.57,.35]);}for(let x=-48;x<48;x+=6){box(v,x,.02,6.35,.4,.04,.4,[.19,.64,.68]);box(v,x,.02,-6.7,.4,.04,.4,[.35,.30,.52]);}for(let i=0;i<26;i++){const x=-64+i*5,h=2+rng()*14;box(v,x,0,-28-rng()*8,3+rng()*2,h,3,[.075,.11,.18],1);}groundCount=upload(groundBuffer,v);}function surface(x,maxY=H){const ix=Math.floor(x+W/2),iz=Math.floor(D/2);for(let y=Math.min(H-1,Math.floor(maxY));y>=0;y--)if(cell(ix,y,iz))return y+1;return 0;}function generate(){randomSeed();theme=themes[(level-1)%themes.length];voxels.fill(0);buildings=[];let x=3,id=1;while(x<W-7){const width=4+Math.floor(rng()*4),height=7+Math.floor(rng()*15),depth=5+2*Math.floor(rng()*3),z0=Math.floor((D-depth)/2);for(let a=x;a<Math.min(W-2,x+width);a++)for(let y=0;y<height;y++)for(let z=z0;z<z0+depth;z++)voxels[index(a,y,z)]=id;const roofX=x+1+Math.floor(rng()*Math.max(1,width-2));if(rng()>.35)for(let y=height;y<height+2;y++)for(let a=x+1;a<x+width-1;a++)for(let z=3;z<6;z++)voxels[index(a,y,z)]=id;buildings.push({x,width,height,roofX});x+=width+1+Math.floor(rng()*2);id++;}const left=buildings[Math.floor(rng()*Math.min(3,buildings.length))],right=buildings[buildings.length-1-Math.floor(rng()*Math.min(3,buildings.length))];crawlers=[left,right].map((b,i)=>{const x=b.roofX-W/2+.5;return{x,y:surface(x)+.7,hp:100,color:i?[1,.45,.32]:[.30,.86,.70],helmet:i?[.94,.34,.19]:[.18,.62,.77]};});rebuildCity();buildGround();active=0;round=1;shotCount=0;rocket=null;particles=[];trail=[];shake=0;phase='intro';phaseTime=0;accumulator=0;wind=(rng()*2-1)*3.8;$('modal').hidden=true;$('intro').hidden=false;$('introSmall').textContent='SECTOR '+String(level).padStart(2,'0')+' / '+seed.toString(16).toUpperCase();$('cityName').textContent=theme.name;$('sector').textContent='SECTOR '+String(level).padStart(2,'0');$('angle').value=45;$('power').value=65;updateControls();status('Surveying the skyline','The camera settles into your firing view.');updateHUD();}function playerTurn(){return phase==='aim'&&(active===0||$('mode').value==='duo');}function updateControls(){$('angleValue').textContent=$('angle').value+'°';$('powerValue').textContent=$('power').value+'%';}function updateHUD(){for(let i=0;i<2;i++){$('hp'+i).textContent=Math.ceil(crawlers[i].hp)+' HP';$('bar'+i).style.width=crawlers[i].hp+'%';}$('wind').textContent=(wind<0?'← ':'→ ')+Math.abs(wind).toFixed(1)+' m/s²';$('windNote').textContent=Math.abs(wind)<.8?'Light breeze':Math.abs(wind)<2.4?'Watch the crosswind':'Strong wind: compensate';$('fire').disabled=!playerTurn();$('angle').disabled=!playerTurn();$('power').disabled=!playerTurn();}function status(title,message){$('turn').textContent=title;$('turn').style.color=active?'#ffab87':'#68e6ef';$('message').textContent=message;}function beginTurn(){phase='aim';phaseTime=0;rocket=null;trail=[];wind=(rng()*2-1)*3.8;$('angle').value=45;updateControls();if(active===1&&$('mode').value==='solo'){aiDelay=.9;status('Ember is lining up a shot','Wind affects both teams equally.');}else status((active?'Ember':'Mint')+' · Your turn','Round '+round+' · Aim, set power, and launch.');updateHUD();}function shotState(angle,power,who=active){const w=crawlers[who],target=crawlers[1-who],dir=Math.sign(target.x-w.x)||1,theta=angle*Math.PI/180,speed=10+power*.36,dx=Math.cos(theta)*dir,dy=Math.sin(theta);return{x:w.x+dx*1.0,y:w.y+.22+dy*1.0,z:0,vx:dx*speed,vy:dy*speed,t:0,owner:who};}function hitsCity(p){return pointSolid(p.x,p.y)||pointSolid(p.x-.16,p.y)||pointSolid(p.x+.16,p.y)||pointSolid(p.x,p.y-.16);}function hitsCrawler(p){return crawlers.some((w,i)=>w.hp>0&&(i!==p.owner||p.t>.20)&&Math.hypot(p.x-w.x,p.y-w.y)<.88);}function advance(p,dt){p.vx+=wind*dt;p.vy-=9.8*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.t+=dt;}function fire(angle=+$('angle').value,power=+$('power').value,computer=false){if(phase!=='aim'||(!computer&&!playerTurn()))return;rocket=shotState(angle,power);shotCount++;trail=[];phase='flight';phaseTime=0;accumulator=0;status((active?'Ember':'Mint')+' · Rocket away','Watch the wind bend the shot.');updateHUD();}function computerAim(){const target=crawlers[0],self=crawlers[1];let best={score:Infinity,angle:45,power:65};for(let k=0;k<150;k++){const angle=12+rng()*72,power=25+rng()*75,p=shotState(angle,power,1);let closest=Infinity;for(let j=0;j<1150;j++){advance(p,1/100);const d=Math.hypot(p.x-target.x,p.y-target.y);closest=Math.min(closest,d);if(hitsCity(p)||hitsCrawler(p)){const selfDistance=Math.hypot(p.x-self.x,p.y-self.y),score=d+(selfDistance<6?(6-selfDistance)*4:0);if(score<best.score)best={score,angle,power};break;}if(Math.abs(p.x)>W/2+13||p.y<0||p.t>11)break;}if(closest<.7)break;}const error=Math.max(.35,2.0-level*.18);fire(best.angle+(rng()-.5)*error,best.power+(rng()-.5)*error,true);}function particleBurst(x,y){for(let i=0;i<54;i++){const a=rng()*Math.PI*2,s=2+rng()*10;particles.push({x,y,z:(rng()-.5)*2,vx:Math.cos(a)*s,vy:Math.sin(a)*s+3,vz:(rng()-.5)*8,life:.5+rng()*1.1,max:1.6,r:.07+rng()*.20,c:i%3?[1,.45,.12]:[.35,.40,.46]});}}function explode(x,y){const radius=3.45;for(let a=Math.max(0,Math.floor(x+W/2-radius));a<=Math.min(W-1,Math.ceil(x+W/2+radius));a++)for(let b=Math.max(0,Math.floor(y-radius));b<=Math.min(H-1,Math.ceil(y+radius));b++)for(let z=0;z<D;z++)if(Math.hypot(a-W/2+.5-x,b+.5-y,z-D/2+.5)<radius)voxels[index(a,b,z)]=0;let total=0;for(const w of crawlers){const d=Math.hypot(w.x-x,w.y-y),damage=d<6.3?Math.round(70*(1-d/6.3)):0;w.hp=Math.max(0,w.hp-damage);total+=damage;const newY=surface(w.x,w.y-.3)+.7,fall=Math.max(0,w.y-newY);w.y=newY;if(fall>1.7){const hurt=Math.round((fall-1.7)*6);w.hp=Math.max(0,w.hp-hurt);total+=hurt;}}rebuildCity();particleBurst(x,y);shake=reducedMotion?0:.48;rocket=null;phase='blast';phaseTime=0;status('Impact',total?'Blast and falling damage: '+total+' HP.':'City damaged. Both players escaped the blast.');updateHUD();}function finishShot(){if(crawlers.some(w=>w.hp<=0)){phase='over';const draw=crawlers.every(w=>w.hp<=0),winner=crawlers[0].hp>0?'Mint':'Ember',won=!draw&&crawlers[0].hp>0;$('resultTag').textContent=draw?'MUTUAL DESTRUCTION':won||$('mode').value==='duo'?'SECTOR CLEARED':'SECTOR LOST';$('resultTitle').textContent=draw?'Nobody owns this roof':winner+' takes the skyline';$('resultText').textContent=shotCount+' rockets fired across '+theme.name+'. '+(won||$('mode').value==='duo'?'The next sector brings a fresh city, new placements, and another duel.':'Try a fresh layout of this sector. Use the wind and falling damage to your advantage.');$('next').textContent=won||$('mode').value==='duo'?'Next Skyline →':'Retry Sector ↻';$('modal').hidden=false;updateHUD();return;}active=1-active;if(active===0)round++;beginTurn();}function perspective(fov,aspect,near,far){const f=1/Math.tan(fov/2),nf=1/(near-far);return new Float32Array([f/aspect,0,0,0,0,f,0,0,0,0,(far+near)*nf,-1,0,0,2*far*near*nf,0]);}function normalize(v){const n=Math.hypot(...v)||1;return v.map(x=>x/n);}function cross(a,b){return[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];}function dot(a,b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}function lookAt(eye,target){const z=normalize(eye.map((x,i)=>x-target[i])),x=normalize(cross([0,1,0],z)),y=cross(z,x);return new Float32Array([x[0],y[0],z[0],0,x[1],y[1],z[1],0,x[2],y[2],z[2],0,-dot(x,eye),-dot(y,eye),-dot(z,eye),1]);}function multiply(a,b){const out=new Float32Array(16);for(let c=0;c<4;c++)for(let r=0;r<4;r++)for(let k=0;k<4;k++)out[c*4+r]+=a[k*4+r]*b[c*4+k];return out;}function drawBuffer(buffer,count){gl.bindBuffer(gl.ARRAY_BUFFER,buffer);gl.enableVertexAttribArray(posLoc);gl.enableVertexAttribArray(normalLoc);gl.enableVertexAttribArray(colorLoc);gl.vertexAttribPointer(posLoc,3,gl.FLOAT,false,40,0);gl.vertexAttribPointer(normalLoc,3,gl.FLOAT,false,40,12);gl.vertexAttribPointer(colorLoc,4,gl.FLOAT,false,40,24);gl.drawArrays(gl.TRIANGLES,0,count);}function drawCrawler(v,w,i){if(w.hp<=0)return;const face=Math.sign(crawlers[1-i].x-w.x)||1,bob=phase==='flight'?0:Math.sin(clock*2+i)*.035;sphere(v,w.x-face*.35,w.y-.38,0,.40,w.color,.62);sphere(v,w.x-face*.16,w.y-.14,0,.36,w.color);sphere(v,w.x,w.y+.15+bob,0,.43,w.color,1.07);sphere(v,w.x,w.y+.48+bob,-.025,.44,w.helmet,.40);sphere(v,w.x-face*.13,w.y+.22+bob,.355,.115,[.97,.98,.93]);sphere(v,w.x+face*.15,w.y+.22+bob,.355,.115,[.97,.98,.93]);sphere(v,w.x-face*.13+face*.025,w.y+.22+bob,.451,.049,[.035,.045,.06]);sphere(v,w.x+face*.15+face*.025,w.y+.22+bob,.451,.049,[.035,.045,.06]);box(v,w.x-.13,w.y-.01+bob,.39,.26,.045,.025,[.13,.08,.09]);if(i===active&&phase!=='intro'&&phase!=='over'){const y=w.y+1.6+Math.sin(clock*3)*.09;quad(v,[[w.x-.28,y+.35,0],[w.x+.28,y+.35,0],[w.x,y,0],[w.x,y,0]],[0,0,1],i?[1,.55,.35]:[.35,.95,1]);}}function render(){const dpr=Math.min(devicePixelRatio||1,1.7),cw=Math.round(canvas.clientWidth*dpr),ch=Math.round(canvas.clientHeight*dpr);if(canvas.width!==cw||canvas.height!==ch){canvas.width=cw;canvas.height=ch;}viewWidth=cw;viewHeight=ch;gl.viewport(0,0,cw,ch);gl.clearColor(...theme.fog,1);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);const aspect=cw/ch,distance=Math.max(77,46/(Math.tan(.62/2)*aspect)),duration=reducedMotion?.6:4.5,t=Math.min(1,phaseTime/duration),ease=1-Math.pow(1-t,3),orbit=phase==='intro'&&!reducedMotion?(1-ease)*Math.PI*2:0;let eye=[Math.sin(orbit)*distance,23+(phase==='intro'?(1-ease)*10:0),Math.cos(orbit)*distance];if(shake>0){eye[0]+=Math.sin(clock*85)*shake;eye[1]+=Math.cos(clock*73)*shake;}const vp=multiply(perspective(.62,aspect,.1,320),lookAt(eye,[0,9,0]));gl.uniformMatrix4fv(vpLoc,false,vp);gl.uniform3fv(eyeLoc,eye);gl.uniform3fv(fogLoc,theme.fog);drawBuffer(groundBuffer,groundCount);drawBuffer(cityBuffer,cityCount);const v=[];crawlers.forEach((w,i)=>drawCrawler(v,w,i));if(playerTurn()){const p=shotState(+$('angle').value,+$('power').value);for(let j=0;j<14;j++){advance(p,.036);if(hitsCity(p)||hitsCrawler(p))break;if(j%1===0)sphere(v,p.x,p.y,.05,.065,[.56,.93,1]);}}if(rocket){sphere(v,rocket.x,rocket.y,0,.18,[.87,.93,.97],1.2);const speed=Math.hypot(rocket.vx,rocket.vy);sphere(v,rocket.x-rocket.vx/speed*.28,rocket.y-rocket.vy/speed*.28,0,.14,[1,.55,.16]);}for(let i=0;i<trail.length;i++){const p=trail[i],s=.025+.07*i/trail.length;sphere(v,p.x,p.y,0,s,[.23+.28*i/trail.length,.30+.25*i/trail.length,.38+.22*i/trail.length]);}for(const p of particles){const r=p.r*Math.min(1,p.life*2);box(v,p.x-r,p.y-r,p.z-r,r*2,r*2,r*2,p.c);}gl.bindBuffer(gl.ARRAY_BUFFER,dynamicBuffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(v),gl.DYNAMIC_DRAW);drawBuffer(dynamicBuffer,v.length/10);}function tick(now){requestAnimationFrame(tick);const dt=lastTime?Math.min(.04,(now-lastTime)/1000):0;lastTime=now;clock+=dt;phaseTime+=dt;shake=Math.max(0,shake-dt);for(const p of particles){p.life-=dt;p.vy-=9*dt;p.x+=p.vx*dt;p.y+=p.vy*dt;p.z+=p.vz*dt;if(p.y<.1){p.y=.1;p.vy=Math.abs(p.vy)*.25;p.vx*=.94;}}particles=particles.filter(p=>p.life>0);if(phase==='intro'&&phaseTime>(reducedMotion?.6:4.5)){$('intro').hidden=true;beginTurn();}else if(phase==='aim'&&active===1&&$('mode').value==='solo'){aiDelay-=dt;if(aiDelay<=0)computerAim();}else if(phase==='flight'&&rocket){accumulator+=dt;while(accumulator>=1/120&&rocket){accumulator-=1/120;advance(rocket,1/120);if(hitsCity(rocket)||hitsCrawler(rocket)){explode(rocket.x,Math.max(0,rocket.y));break;}if(Math.abs(rocket.x)>W/2+14||rocket.y>100||rocket.t>12){rocket=null;phase='blast';phaseTime=.65;status('Rocket out of bounds','No damage. The turn passes.');break;}}if(rocket){trail.push({x:rocket.x,y:rocket.y});if(trail.length>25)trail.shift();}}else if(phase==='blast'&&phaseTime>1.3)finishShot();render();}for(const id of['angle','power'])$(id).addEventListener('input',updateControls);$('fire').addEventListener('click',()=>fire());$('newCity').addEventListener('click',generate);$('mode').addEventListener('change',()=>{level=1;generate();});$('next').addEventListener('click',()=>{if(crawlers[0].hp>0||$('mode').value==='duo')level++;generate();});$('save').addEventListener('click',()=>{const source='<!doctype html>'+document.documentElement.outerHTML,blob=new Blob([source],{type:'text/html'}),url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='neon-crawl.html';document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000);});document.addEventListener('keydown',e=>{if(!playerTurn()||['INPUT','SELECT'].includes(e.target.tagName))return;if(e.code==='Space'){e.preventDefault();fire();return;}const changes={ArrowLeft:['angle',-1],ArrowRight:['angle',1],ArrowUp:['power',1],ArrowDown:['power',-1]};if(changes[e.key]){e.preventDefault();const[id,d]=changes[e.key],el=$(id);el.value=Math.max(+el.min,Math.min(+el.max,+el.value+d*(e.shiftKey?5:1)));updateControls();}});canvas.addEventListener('webglcontextlost',e=>{e.preventDefault();$('error').hidden=false;$('error').textContent='The graphics context was interrupted. Close and run the game again to restart.';});generate();requestAnimationFrame(tick);})();</script></body></html>","timeoutMinutes":0,"title":"Neon Crawl","width":1280,"windowMode":"window"},"customName":"Neon Crawl","isDisabled":false,"localID":0,"typeID":"action.html_view","x":450,"y":260},{"config":{"_height":450,"_width":460,"text":"**Neon Crawl is a 3D rooftop artillery game that opens in its own window when you run this flow.**\n\n**What it does**\n- A complete game in one window. No network, no files, nothing to install.\n- Play against the computer, or choose Two Players and take turns on one Mac.\n- The wind changes every turn and buildings break apart where shots land.\n\n**Set it up**\n- Nothing to connect. Run the flow to play.\n- Left and Right set the angle, Up and Down set the power, Space fires. Hold Shift to move in steps of 5.\n- Close the window to stop playing.\n\n**Make it yours**\n- Change Width and Height on **Neon Crawl** for a bigger or smaller window.\n- Set Window to Full-screen takeover on **Neon Crawl** to fill the screen."},"isDisabled":false,"localID":1,"typeID":"note","x":100,"y":-290},{"config":{"inputType":"text","textInput":"Play"},"customName":"Launch Game","isDisabled":false,"localID":2,"typeID":"trigger.input","x":100,"y":200}],"version":1}