import {BIOMES, DEVICES, DT, H, TECH, VERSION, W, clamp, region} from './src/data.mjs'; import {pickOrganism, record} from './src/biology.mjs'; import {environment, generateRegion, routeBetween} from './src/world.mjs'; import {movePlayer} from './src/physics.mjs'; const $=id=>document.getElementById(id),SAVE_KEY='orr-phycofrontier-save-v1',canvas=$('world'),ctx=canvas.getContext('2d'),keys=new Set(),input={left:false,right:false,up:false,down:false,jump:false,jumpHeld:false}; let worldCache=null,worldCacheId='',modal='',paused=false,last=performance.now(),uiClock=0,toastTimer=0,audio=null; const esc=value=>String(value??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c])); const starterDevice=()=>({id:'device-001',type:'sampler',region:'estuary',status:'idle',progress:0,task:[],cycles:0,level:1}); function freshState(){return {version:VERSION,seed:'PF-ESTUARY-042',region:'estuary',time:0,cycles:0,player:{x:230,y:700,w:28,h:46,vx:0,vy:0,facing:1,grounded:false,coyote:0,airJump:false,crouched:false,swimming:false,climbing:false,animation:'idle'},discovered:['estuary'],scanned:[],samples:[],archive:[],devices:[starterDevice()],abilities:[],upgrades:[],outposts:['estuary'],shortcuts:[],logs:[{time:0,title:'Mission accepted',text:'Map the first habitat and bring an environmental sample back to the field sampler.'}],resources:{credits:120,data:0,materials:42,power:100,biomass:0},stats:{scanned:0,sampled:0,processed:0,characterized:0,regions:1},lastSite:null,objective:'Scan the shoreline habitat, then collect your first environmental sample.'};} function sanitize(raw){const base=freshState();if(!raw||raw.version!==VERSION||typeof raw.seed!=='string')return base;const s={...base,...raw,player:{...base.player,...raw.player},resources:{...base.resources,...raw.resources},stats:{...base.stats,...raw.stats}};for(const key of ['discovered','scanned','abilities','upgrades','outposts','shortcuts','logs','samples','archive','devices'])if(!Array.isArray(s[key]))s[key]=base[key];s.discovered=[...new Set(['estuary',...s.discovered.filter(id=>region(id))])];s.region=s.discovered.includes(s.region)?s.region:'estuary';s.devices=s.devices.filter(d=>DEVICES[d.type]).map(d=>({status:'idle',progress:0,task:[],cycles:0,level:1,...d}));return s;} function loadState(){try{return sanitize(JSON.parse(localStorage.getItem(SAVE_KEY)||'null'));}catch{return freshState();}} let state=loadState(); function saveState(){try{localStorage.setItem(SAVE_KEY,JSON.stringify(state));$('saveState').textContent='LOCAL SAVE · '+new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});}catch{$('saveState').textContent='SAVE UNAVAILABLE';}} function currentWorld(){if(worldCacheId!==state.region){worldCache=generateRegion(state.seed,state.region);worldCacheId=state.region;}return worldCache;} function dist(a,b){return Math.hypot((a.x||0)-(b.x||0),(a.y||0)-(b.y||0));} function nearestSite(limit=150){const p=state.player;return currentWorld().sites.map(s=>({...s,y:s.y})).sort((a,b)=>dist(p,a)-dist(p,b)).find(s=>dist(p,s)Math.abs(state.player.x-link.x)!state.scanned.includes(r.id)&&dist(state.player,r)node.classList.remove('show'),3600);} function log(title,text){state.logs.unshift({time:state.time,title,text});state.logs=state.logs.slice(0,40);showToast(text);} function speak(note=540,duration=.08){if(!audio)return;const o=audio.createOscillator(),g=audio.createGain();o.frequency.value=note;o.type='sine';g.gain.setValueAtTime(.08,audio.currentTime);g.gain.exponentialRampToValueAtTime(.001,audio.currentTime+duration);o.connect(g).connect(audio.destination);o.start();o.stop(audio.currentTime+duration);} function addSample(source,auto=false){const cap=state.abilities.includes('storage')?16:8;if(state.samples.filter(s=>s.status==='raw').length>=cap){showToast(`Sample pack full (${cap}). Process or return to an outpost.`);return false;}const w=currentWorld(),b=w.b,e=environment(b,state.time),site=source||w.sites[0],sample={id:`sample-${String(++state.stats.sampled).padStart(3,'0')}`,region:state.region,site:site.id,siteName:site.name,environment:e,collectedAt:state.time,status:'raw',rarity:site.rarity};state.samples.push(sample);state.lastSite=site.id;state.resources.materials+=auto?2:1;state.objective='Process the sample through a Droplet Sorter, then isolate a candidate.';log('Sample secured',`${site.name} is sealed in the field pack. ${e.temperature}°C · pH ${e.ph} · ${e.nutrients}% nutrients.`);speak(740,.13);return true;} function scan(){const w=currentWorld(),site=nearestSite(180);if(site&&!state.scanned.includes(site.id)){state.scanned.push(site.id);state.resources.data+=5;state.stats.scanned++;state.objective=`Sample site identified: ${site.name}. Move close and press E to collect it.`;log('Habitat scanned',`${site.name}: ${w.b.clue}`);speak(440,.2);}else if(site){showToast(`${site.name} already scanned. Press E to collect or look for another habitat.`);}else{const e=environment(w.b,state.time);showToast(`${w.b.name}: ${e.temperature}°C · ${e.salinity} PSU · pH ${e.ph} · ${e.light}% light`);}updateUI();} function interact(){const site=nearestSite(92);if(site){if(!state.scanned.includes(site.id)){scan();return;}if(addSample(site))updateUI();return;}const ore=nearestResource();if(ore){state.scanned.push(ore.id);state.resources.materials+=8;log('Mineral cache recovered','Refined materials can install devices and construct research outposts.');updateUI();return;}const link=nearestLink();if(link){const target=region(link.to);if(target.gate&&!state.abilities.includes(target.gate)){showToast(`Route sealed: ${target.gate} protection required.`);return;}if(!state.discovered.includes(link.to)){state.discovered.push(link.to);state.stats.regions++;state.resources.data+=8;log('New region charted',`${target.name} added to the living world map.`);}travelTo(link.to);return;}const outpost=currentWorld().outpost;if(Math.abs(state.player.x-outpost.x)<110&&Math.abs(state.player.y-outpost.y)<100){showPanel('devices');return;}showToast('Nothing here responds to the expedition key. Scan for biological activity.');} function travelTo(id){if(!region(id))return;const route=routeBetween(state,state.region,id,true);if(!route&&id!==state.region){showToast('No mapped route. Reach a connected frontier on foot first.');return;}const previous=state.region;state.region=id;worldCacheId='';const w=currentWorld();state.player.x=previous===id?state.player.x:(region(id).col>region(previous).col?90:region(id).colregion(previous).row?90:W/2);state.player.y=w.outpost.y-48;state.player.vx=0;state.player.vy=0;state.objective=`Scan ${w.b.name}. Every habitat holds a different biological signal.`;log('Entered habitat',`${w.b.name} · ${w.b.story}`);closePanel();updateUI();} function findTask(dev){const eligible=mode=>state.samples.filter(s=>s.region===state.region&&s.status===mode),d=DEVICES[dev.type];if(d.mode==='sample'||d.mode==='drone'||d.mode==='station')return [];if(d.mode==='sort')return eligible('raw').slice(0,d.capacity||1).map(s=>s.id);if(d.mode==='isolate')return eligible('candidate').slice(0,1).map(s=>s.id);if(d.mode==='analyze')return eligible('isolated').slice(0,1).map(s=>s.id);if(['evidence','adaptive','complete'].includes(d.mode))return eligible('raw').slice(0,1).map(s=>s.id);return [];} function deviceUnlocked(type){const d=DEVICES[type];return !d.unlock||state.abilities.includes(d.unlock);} function installDevice(type){const d=DEVICES[type];if(!deviceUnlocked(type)){showToast(`Research ${d.unlock} technology first.`);return;}if(state.resources.creditsd.id===id);if(!dev||dev.status==='processing')return;const d=DEVICES[dev.type];if(dev.region!==state.region){showToast('That device is remote. Open the map or build a station to manage it.');return;}if(state.resources.powers.id===state.lastSite)||currentWorld().sites[0],true);return;}if(d.mode==='drone'){for(let i=0;i<2;i++)addSample(currentWorld().sites[i%currentWorld().sites.length],true);state.resources.materials+=10;log('Drone returned','The autonomous sampler mapped a nearby microhabitat and brought back two sealed samples.');return;}if(d.mode==='station'){if(!state.outposts.includes(state.region)){state.outposts.push(state.region);state.resources.power=100;log('Outpost established',`${currentWorld().b.name} now supports remote processing and fast travel.`);}return;}for(const sid of tasks){const sample=state.samples.find(s=>s.id===sid);if(!sample)continue;if(d.mode==='sort'){sample.status='candidate';sample.specimen=pickOrganism(state,sample,dev.type==='adaptive'?.18:0);record(state,sample.specimen,'candidate',.35);state.resources.data+=3;state.objective='Candidate culture detected. Run the Single-cell Isolator.';}if(d.mode==='isolate'){const chance=clamp(d.isolation+(state.upgrades.includes('efficiency')?.12:0),0,1),roll=(Math.abs((sid+'isolate'+state.cycles).split('').reduce((a,c)=>a+c.charCodeAt(0),0))%100)/100;if(roll=1)completeDevice(dev);}} function buyTech(id){const t=TECH[id];if(!t||state.abilities.includes(id))return;if(t.requires&&!state.abilities.includes(t.requires)){showToast(`Requires ${TECH[t.requires].name}.`);return;}if(state.resources.creditscameraX+innerWidth+150)continue;const y=groundAt(w,d.x);ctx.save();ctx.translate(d.x,y);ctx.globalAlpha=.2+.5*d.variant;ctx.strokeStyle=b.color;ctx.fillStyle=b.color;ctx.lineWidth=2;if(b.flora==='reeds'||b.flora==='grass'){for(let i=0;i<3;i++){ctx.beginPath();ctx.moveTo(i*6-6,0);ctx.quadraticCurveTo(i*8-4,-d.size*.7,i*9-9,-d.size);ctx.stroke();}}else if(b.flora==='willow'||b.flora==='roots'){ctx.strokeStyle='#182b25';ctx.lineWidth=5;ctx.beginPath();ctx.moveTo(0,0);ctx.quadraticCurveTo(-d.size/3,-d.size,d.size/8,-d.size*1.25);ctx.stroke();for(let i=0;i<4;i++){ctx.fillStyle=b.color;ctx.beginPath();ctx.ellipse(-d.size*.2+i*d.size*.12,-d.size*(.7+(i%2)*.18),d.size*.16,d.size*.35,i,0,Math.PI*2);ctx.fill();}}else if(b.flora==='kelp'||b.flora==='vent'){ctx.beginPath();ctx.moveTo(0,0);ctx.bezierCurveTo(-d.size,-d.size*.4,d.size,-d.size*.75,Math.sin(time/2+d.phase)*d.size*.35,-d.size*1.4);ctx.stroke();ctx.lineWidth=1;ctx.strokeStyle='#d5f2c3';ctx.beginPath();ctx.moveTo(0,-d.size*.2);ctx.lineTo(Math.sin(d.phase)*d.size,-d.size*.9);ctx.stroke();}else if(b.flora==='crystal'||b.flora==='ice'){ctx.fillStyle=b.color;ctx.beginPath();ctx.moveTo(-d.size*.25,0);ctx.lineTo(-d.size*.05,-d.size);ctx.lineTo(d.size*.16,-d.size*.3);ctx.lineTo(d.size*.45,-d.size*1.15);ctx.lineTo(d.size*.5,0);ctx.closePath();ctx.fill();}else{ctx.strokeStyle='#90d7aa';ctx.strokeRect(-d.size*.3,-d.size*.55,d.size*.6,d.size*.55);ctx.beginPath();ctx.moveTo(-d.size*.2,-d.size*.55);ctx.lineTo(d.size*.22,0);ctx.moveTo(d.size*.22,-d.size*.55);ctx.lineTo(-d.size*.2,0);ctx.stroke();}ctx.restore();}ctx.globalAlpha=1;} function drawWorld(){const w=currentWorld(),b=w.b,p=state.player,dpr=canvas._dpr||1,vw=canvas.width/dpr,vh=canvas.height/dpr;ctx.setTransform(dpr,0,0,dpr,0,0);drawBackdrop(b,vw,vh,state.time);const camX=clamp(p.x-vw*.38,0,W-vw),camY=clamp(p.y-vh*.54,80,H-vh);ctx.save();ctx.translate(-camX,-camY);ctx.fillStyle='rgba(8,32,33,.65)';for(let y=0;y<3;y++){ctx.beginPath();ctx.moveTo(camX-50,300+y*130);for(let x=camX-50;xi?ctx.lineTo(g.x,g.y):ctx.moveTo(g.x,g.y));ctx.lineTo(W,H);ctx.lineTo(0,H);ctx.closePath();ctx.globalAlpha=.15;ctx.fill();ctx.globalAlpha=1;drawDecor(w,camX,state.time);for(const platform of w.platforms){ctx.fillStyle='#152b29';ctx.fillRect(platform.x,platform.y,platform.w,platform.h);ctx.strokeStyle='rgba(160,225,188,.45)';ctx.strokeRect(platform.x,platform.y,platform.w,platform.h);}for(const site of w.sites){const active=state.scanned.includes(site.id);ctx.save();ctx.translate(site.x,site.y);ctx.strokeStyle=active?'#9de8bb':'#ef9ea5';ctx.fillStyle=active?'rgba(157,232,187,.18)':'rgba(239,158,165,.13)';ctx.lineWidth=2;ctx.beginPath();ctx.arc(0,-18,18+Math.sin(state.time*3+site.x)*3,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.beginPath();ctx.moveTo(0,0);ctx.lineTo(0,-30);ctx.stroke();ctx.fillStyle='#f5f7dc';ctx.font='10px DM Mono';ctx.fillText(active?'SCANNED':'BIO-SITE',-25,-44);ctx.restore();}for(const link of w.links){ctx.save();ctx.translate(link.x,link.y);ctx.strokeStyle='rgba(236,193,131,.75)';ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo(0,20);ctx.lineTo(link.direction==='west'?-34:link.direction==='east'?34:0,link.direction==='up'?-26:link.direction==='down'?26:0);ctx.stroke();ctx.setLineDash([]);ctx.fillStyle='#ecc183';ctx.font='10px DM Mono';ctx.textAlign='center';ctx.fillText(region(link.to).name.toUpperCase(),0,-34);ctx.restore();}for(const ore of w.resources){if(state.scanned.includes(ore.id))continue;ctx.fillStyle='#d1a269';ctx.strokeStyle='#f3d395';ctx.beginPath();ctx.moveTo(ore.x-10,ore.y+10);ctx.lineTo(ore.x-5,ore.y-9);ctx.lineTo(ore.x+12,ore.y-15);ctx.lineTo(ore.x+17,ore.y+10);ctx.closePath();ctx.fill();ctx.stroke();}const out=w.outpost;ctx.save();ctx.translate(out.x,out.y);ctx.fillStyle='rgba(104,209,166,.15)';ctx.fillRect(-55,-125,110,125);ctx.strokeStyle='#9de8bb';ctx.strokeRect(-55,-125,110,125);ctx.fillStyle='#102b27';ctx.fillRect(-43,-95,86,95);ctx.fillStyle='#9de8bb';ctx.fillRect(-18,-75,36,18);ctx.fillStyle='#ecc183';ctx.beginPath();ctx.arc(0,-106,10+Math.sin(state.time*3),0,Math.PI*2);ctx.fill();ctx.fillStyle='#9de8bb';ctx.font='10px DM Mono';ctx.textAlign='center';ctx.fillText('OUTPOST // E',0,20);ctx.restore();for(const dev of state.devices.filter(d=>d.region===state.region&&d.type!=='station')){const dx=out.x+85+state.devices.indexOf(dev)*35;ctx.save();ctx.translate(dx,out.y);ctx.strokeStyle=dev.status==='processing'?'#83d8d2':'rgba(157,232,187,.7)';ctx.fillStyle='rgba(8,35,29,.9)';ctx.fillRect(-13,-48,26,48);ctx.strokeRect(-13,-48,26,48);ctx.fillStyle=dev.status==='processing'?'#83d8d2':'#d1a269';ctx.beginPath();ctx.arc(0,-60,7+Math.sin(state.time*5)*2,0,Math.PI*2);ctx.fill();ctx.strokeStyle='#83d8d2';ctx.beginPath();ctx.moveTo(-10,-25);ctx.bezierCurveTo(0,-10,10,-35,15,-15);ctx.stroke();ctx.restore();}const px=p.x+p.w/2,py=p.y+p.h/2;ctx.save();ctx.translate(px,py);ctx.scale(p.facing,1);const bob=p.animation==='run'?Math.sin(state.time*15)*2:p.animation==='swim'?Math.sin(state.time*6)*4:0;ctx.translate(0,bob);ctx.fillStyle='#122b28';ctx.strokeStyle='#a7e5c0';ctx.lineWidth=2;ctx.fillRect(-12,-8,24,26);ctx.strokeRect(-12,-8,24,26);ctx.fillStyle='#d8f1d2';ctx.beginPath();ctx.arc(0,-21,12,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#4cc8bc';ctx.fillRect(3,-24,9,5);ctx.fillStyle='#d7a967';ctx.fillRect(-15,0,8,5);ctx.fillStyle='#a7e5c0';ctx.fillRect(-16,20,10,5);ctx.fillRect(6,20,10,5);if(p.animation==='scan'){ctx.strokeStyle='rgba(157,232,187,.65)';ctx.beginPath();ctx.moveTo(12,-21);ctx.lineTo(125,-72);ctx.stroke();}ctx.restore();ctx.restore();} function renderMap(){const discovered=new Set(state.discovered);return `
${BIOMES.map(b=>{const seen=discovered.has(b.id),locked=b.gate&&!state.abilities.includes(b.gate);return ``;}).join('')}

This chart is generated from the persistent playable world, not a level selector. Walk to a frontier gate to reveal a connected habitat. Fast travel is available only between discovered regions with a mapped route.

${BIOMES.filter(b=>state.discovered.includes(b.id)).map(b=>`
${esc(b.name)}${state.outposts.includes(b.id)?'OUTPOST':state.scanned.some(id=>id.startsWith(b.id))?'SCANNED':'EXPEDITION'}
`).join('')}
`;} function deviceMarkup(dev){const d=DEVICES[dev.type],p=Math.round(dev.progress*100);return `
⬡

${esc(d.name)}

${esc(d.role)}

${dev.region.toUpperCase()} · ${dev.status==='processing'?`PROCESSING ${p}%`:'READY'} · ${d.power} POWER / CYCLE
${dev.status==='processing'?`
`:''}
${dev.status==='processing'?'':``}
`;} function renderDevices(){const available=Object.entries(DEVICES).filter(([id])=>deviceUnlocked(id)&&!state.devices.some(d=>d.type===id));return `
${state.devices.map(deviceMarkup).join('')}

Devices are functional instruments. They consume field power, accept only compatible samples, and remain attached to the habitat where they were installed.

${available.map(([id,d])=>`
⬡

${esc(d.name)}

${esc(d.role)}

${d.cost} RESEARCH · ${d.materials} MATERIALS · ${d.seconds||'SUPPORT'}s
`).join('')}
`;} function renderArchive(){return `

A field journal of fictional, simulated organisms. Evidence records preserve uncertainty; characterization requires an isolated culture and analysis.

${state.archive.length?state.archive.map(a=>`

${esc(a.name)}

${esc(a.rarity)} · ${esc(a.stage).toUpperCase()} · ${Math.round(a.confidence*100)}% CONFIDENCE

${esc(a.group)} · ${esc(a.morph)} morphology · ${esc(a.pigment)}. ${esc(a.note)}

${a.observations} observation${a.observations===1?'':'s'} · ${esc(a.application)}

`).join(''):'

No specimens yet. Scan the glowing field sites, secure a sample, then run it through the device chain.

'}
`;} function renderTech(){return `

Choose your expedition path. Technology costs research credits, data, and materials; prerequisites make routes through the world meaningful.

${Object.entries(TECH).map(([id,t])=>{const owned=state.abilities.includes(id),req=t.requires&&!state.abilities.includes(t.requires);return `
${esc(t.branch)}

${esc(t.name)}

${esc(t.description)}

${owned?'UNLOCKED':`${t.cost} CREDITS · ${t.data} DATA · ${t.materials} MATERIALS`}
${owned?'':``}
`;}).join('')}
`;} function renderJournal(){return `
${state.logs.map(l=>`
${esc(l.title)}

${esc(l.text)}

`).join('')}
${[['Regions charted',state.stats.regions],['Sites scanned',state.stats.scanned],['Samples secured',state.stats.sampled],['Processing cycles',state.stats.processed],['Cultures characterized',state.stats.characterized],['Outposts',state.outposts.length]].map(([a,b])=>`
${a}${b}
`).join('')}

All organism names and traits are fictional game data. The expedition illustrates scientific process; it is not a validated laboratory result.

`;} function showPanel(type){modal=type;$('modalShell').hidden=false;pausedInput();renderPanel();} function renderPanel(){if(!modal)return;const labels={map:['WORLD CARTOGRAPHY','Living world map'],devices:['MICROFLUIDIC NETWORK','Research devices'],archive:['FIELD JOURNAL','Specimen archive'],tech:['RESEARCH PROGRAM','Technology tree'],journal:['EXPEDITION RECORD','Mission journal']};$('panelEyebrow').textContent=labels[modal]?.[0]||'EXPEDITION SYSTEM';$('panelTitle').textContent=labels[modal]?.[1]||'Expedition';$('panelBody').innerHTML=modal==='map'?renderMap():modal==='devices'?renderDevices():modal==='archive'?renderArchive():modal==='tech'?renderTech():renderJournal();} function closePanel(){modal='';$('modalShell').hidden=true;} function pausedInput(){for(const k of Object.keys(input))input[k]=false;keys.clear();} function updateObjective(){const site=nearestSite(95),link=nearestLink(),ore=nearestResource();let text=state.objective;if(site)text=state.scanned.includes(site.id)?'Press E to collect the scanned environmental sample.':'Press F to scan this biological signal.';else if(ore)text='Press E to recover the mineral cache.';else if(link)text=`Press E to enter the ${region(link.to).name}.`;else if(Math.abs(state.player.x-currentWorld().outpost.x)<105)text='Field outpost: press E to open the microfluidic network.';$('objectiveText').textContent=text;} function updateUI(){const b=region(state.region),e=environment(b,state.time),cap=state.abilities.includes('storage')?16:8;$('regionName').textContent=b.name;$('regionType').textContent=`${b.type.toUpperCase()} · ${state.discovered.length.toString().padStart(2,'0')} HABITATS CHARTED`;$('credits').textContent=Math.floor(state.resources.credits);$('data').textContent=Math.floor(state.resources.data);$('materials').textContent=Math.floor(state.resources.materials);$('samples').textContent=`${state.samples.filter(s=>s.status==='raw').length} / ${cap}`;$('power').textContent=`${Math.floor(state.resources.power)}%`;$('archiveCount').textContent=state.archive.length;$('deviceCount').textContent=state.devices.length;$('techCount').textContent=state.abilities.length;$('logCount').textContent=state.logs.length;$('environmentText').textContent=`${e.temperature}°C · ${e.salinity} PSU · pH ${e.ph} · ${e.light}% LIGHT · ${e.pressure} ATM`;document.body.dataset.region=state.region;document.body.dataset.rawSamples=state.samples.filter(s=>s.status==='raw').length;document.body.dataset.archive=state.archive.length;updateObjective();if(modal==='devices')renderPanel();} function setKey(code,on){if(code==='ArrowLeft'||code==='KeyA')input.left=on;if(code==='ArrowRight'||code==='KeyD')input.right=on;if(code==='ArrowUp'||code==='KeyW')input.up=on;if(code==='ArrowDown'||code==='KeyS')input.down=on;if(code==='Space')input.jump=on;} window.addEventListener('keydown',e=>{if(['INPUT','SELECT','TEXTAREA'].includes(document.activeElement?.tagName))return;if(e.code==='Escape'){if(modal)closePanel();else togglePause();return;}if(e.code==='KeyM'){showPanel('map');return;}if(e.code==='KeyI'){showPanel('devices');return;}if(e.code==='KeyJ'){showPanel('archive');return;}if(e.code==='KeyT'){showPanel('tech');return;}if(e.code==='KeyP'){togglePause();return;}if(e.code==='KeyE'&&!e.repeat){interact();return;}if(e.code==='KeyF'&&!e.repeat){state.player.animation='scan';scan();setTimeout(()=>state.player.animation='idle',350);return;}setKey(e.code,true);if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Space'].includes(e.code))e.preventDefault();}); window.addEventListener('keyup',e=>setKey(e.code,false));window.addEventListener('blur',()=>{pausedInput();if(!modal)paused=true;}); document.querySelectorAll('[data-key]').forEach(button=>{const code={left:'ArrowLeft',right:'ArrowRight',jump:'Space'}[button.dataset.key];button.addEventListener('pointerdown',e=>{e.preventDefault();if(button.dataset.key==='interact')interact();else if(button.dataset.key==='scan')scan();else setKey(code,true);});button.addEventListener('pointerup',()=>code&&setKey(code,false));button.addEventListener('pointerleave',()=>code&&setKey(code,false));}); document.addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.panel){showPanel(b.dataset.panel);return;}if(b.dataset.region){travelTo(b.dataset.region);return;}if(b.dataset.installDevice){installDevice(b.dataset.installDevice);return;}if(b.dataset.runDevice){runDevice(b.dataset.runDevice);return;}if(b.dataset.buyTech){buyTech(b.dataset.buyTech);return;}const action=b.dataset.action;if(action==='scan')scan();if(action==='pause')togglePause();if(action==='resume')togglePause();if(action==='close-panel')closePanel();if(action==='save'){saveState();showToast('Progress saved to this browser.');}if(action==='reset'&&confirm('Erase the local PHYCOFRONTIER expedition?')){localStorage.removeItem(SAVE_KEY);location.reload();}}); function togglePause(){if(modal)return;paused=!paused;pausedInput();$('pauseShell').hidden=!paused;} window.addEventListener('resize',resize);window.addEventListener('pagehide',saveState);resize();window.__phycofrontier=globalThis.__phycofrontier={getState:()=>structuredClone(state),advance(seconds){for(let i=0;i.12){uiClock=0;updateUI();}requestAnimationFrame(loop);}requestAnimationFrame(loop);