Files
signalelsewhere/www/index.html

1017 lines
48 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<title>Signal Elsewhere</title>
<style>
html,body{height:100%;width:100%;margin:0;padding:0}
*{margin:0;padding:0;box-sizing:border-box}
body{background:#000;overflow:hidden;font-family:'Courier New',monospace;color:#0f0;min-height:100vh}
#matrixCanvas{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;opacity:.15;pointer-events:none}
#game{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;display:flex;flex-direction:column;padding:4px}
#status{display:flex;gap:8px;padding:4px 6px;font-size:11px;color:#0a0;border-bottom:1px solid #030;flex-wrap:wrap;letter-spacing:1px;min-height:22px;background:#001005}
#status .stat{white-space:nowrap}
#status .stat b{color:#0f0}
#status .phase-label{color:#060;margin-left:auto}
#terminal{flex:1;overflow-y:auto;padding:6px;font-size:13px;line-height:1.5;color:#0f0;scroll-behavior:smooth;background:transparent;min-height:0}
#terminal::-webkit-scrollbar{width:4px}
#terminal::-webkit-scrollbar-track{background:#001005}
#terminal::-webkit-scrollbar-thumb{background:#030;border-radius:2px}
.term-line{margin:0;white-space:pre-wrap;word-break:break-word;opacity:0;transition:opacity .08s}
.term-line.visible{opacity:1}
.term-line.system{color:#060}
.term-line.info{color:#0a0}
.term-line.success{color:#0f0}
.term-line.warn{color:#aa0}
.term-line.error{color:#a00}
.term-line.highlight{color:#0ff;font-weight:bold}
.term-line.narrative{color:#0f0;font-style:italic}
.term-line.signal{color:#0a0;letter-spacing:2px}
.term-prompt{display:flex;align-items:center;gap:4px;margin:4px 0 0}
.term-prompt .prompt{color:#0f0}
.term-prompt .cursor{display:inline-block;width:8px;height:14px;background:#0f0;animation:blink .8s step-end infinite}
@keyframes blink{50%{opacity:0}}
#inputRow{display:flex;padding:4px 6px;border-top:1px solid #030;background:#001005;gap:4px}
#cmdInput{flex:1;background:#000;border:1px solid #030;color:#0f0;font-family:'Courier New',monospace;font-size:13px;padding:6px 8px;outline:none;min-width:0}
#cmdInput:focus{border-color:#0f0}
#cmdInput::placeholder{color:#040}
#cmdSend{padding:4px 14px;background:#030;border:1px solid #0f0;color:#0f0;font-family:'Courier New',monospace;font-size:12px;cursor:pointer;white-space:nowrap}
#cmdSend:active{background:#0f0;color:#000}
#actions{display:flex;flex-wrap:wrap;gap:3px;padding:3px 6px;border-top:1px solid #030;background:#000a;justify-content:center}
#actions .act{display:none;padding:4px 12px;font-size:11px;font-family:'Courier New',monospace;background:#001005;border:1px solid #030;color:#0a0;cursor:pointer;letter-spacing:1px;transition:all .2s}
#actions .act.show{display:inline-block}
#actions .act:active{background:#0f0;color:#000;border-color:#0f0}
#actions .act.primary{color:#0f0;border-color:#0a0}
#actions .act.primary:active{background:#0f0;color:#000}
#panels{position:fixed;top:0;left:0;width:100%;height:100%;z-index:10;pointer-events:none;display:none}
#panels.active{display:block}
#panels .panel{position:absolute;top:10%;left:5%;width:90%;height:80%;background:#000;border:1px solid #030;border-radius:4px;pointer-events:auto;display:flex;flex-direction:column}
#panels .panel-title{display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #030;color:#0a0;font-size:12px}
#panels .panel-close{background:none;border:1px solid #030;color:#0a0;cursor:pointer;padding:2px 8px;font-family:'Courier New',monospace;font-size:11px}
#panels .panel-close:active{background:#0f0;color:#000}
.panel-body{flex:1;overflow-y:auto;padding:10px}
#specCanvas{width:100%;height:200px;display:block;border:1px solid #030;margin-bottom:8px;background:#000}
#mapCanvas{width:100%;height:200px;display:block;border:1px solid #030;margin-bottom:8px;background:#000}
#overlay{position:fixed;top:0;left:0;width:100%;height:100%;z-index:20;display:none;background:rgba(0,0,0,.92);justify-content:center;align-items:center;text-align:center;padding:20px}
#overlay.show{display:flex}
#overlay .inner{max-width:500px;width:100%}
#overlay h2{color:#0f0;font-size:18px;margin-bottom:12px;letter-spacing:2px}
#overlay p{color:#0a0;font-size:13px;line-height:1.6;margin-bottom:16px;white-space:pre-wrap}
#overlay .btn{margin:4px;padding:8px 24px;font-size:13px;font-family:'Courier New',monospace;background:#001005;border:1px solid #0f0;color:#0f0;cursor:pointer;transition:all .2s;letter-spacing:2px}
#overlay .btn:active{background:#0f0;color:#000}
#overlay .btn.dim{opacity:.4}
#overlay .btn.dim:active{opacity:1}
@media(min-width:600px){
#status{font-size:13px;padding:6px 10px}
#terminal{font-size:14px;padding:10px;max-width:700px;margin:0 auto;width:100%}
#inputRow{max-width:700px;margin:0 auto;width:100%}
#actions{max-width:700px;margin:0 auto;width:100%}
#actions .act{padding:6px 18px;font-size:13px}
#panels .panel{top:15%;left:15%;width:70%;height:70%;max-width:600px;margin:0 auto}
#specCanvas{height:250px}
#mapCanvas{height:250px}
}
#game.hide-terminal #terminal,
#game.hide-terminal #inputRow,
#game.hide-terminal #actions{display:none}
</style>
</head>
<body>
<canvas id="matrixCanvas"></canvas>
<div id="game">
<div id="status"></div>
<div id="terminal"></div>
<div id="inputRow">
<input id="cmdInput" type="text" placeholder="TYPE COMMAND... /help" autocomplete="off" spellcheck="false">
<button id="cmdSend">SEND</button>
</div>
<div id="actions"></div>
</div>
<div id="loadingMsg" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:2;color:#0f0;font-family:'Courier New',monospace;font-size:16px;text-align:center;letter-spacing:2px">INITIALIZING...</div>
<div id="panels">
<div class="panel" id="specPanel">
<div class="panel-title">SPECTRUM ANALYZER <button class="panel-close" data-panel="specPanel">X</button></div>
<div class="panel-body"><canvas id="specCanvas" width="800" height="250"></canvas><div id="specStatus" style="color:#060;font-size:11px;text-align:center">TUNE A FREQUENCY TO BEGIN</div></div>
</div>
<div class="panel" id="mapPanel">
<div class="panel-title">FACILITY MAP <button class="panel-close" data-panel="mapPanel">X</button></div>
<div class="panel-body"><canvas id="mapCanvas" width="800" height="250"></canvas><div id="mapStatus" style="color:#060;font-size:11px;text-align:center"></div></div>
</div>
</div>
<div id="overlay"></div>
<div id="errorBox" style="position:fixed;bottom:0;left:0;width:100%;z-index:999;background:#100;color:#f00;padding:8px;font:11px monospace;display:none;white-space:pre-wrap"></div>
<script>
// ── CONFIG ──
const DATA = {
rooms: {
bunker:{name:'BUNKER',desc:'Concrete walls. A terminal. The air is stale. This is where you woke up.',explored:true},
corridor:{name:'CORRIDOR A',desc:'Narrow hallway lined with dead fluorescent tubes. Faded signs point to LAB, ARCHIVE, COMMS.'},
lab:{name:'MAIN LAB',desc:'Research stations coated in dust. Monitors show only static. A nameplate reads: DR. VANCE.'},
archive:{name:'ARCHIVE',desc:'Rows of filing cabinets. Most are empty. A single folder labeled "PROJECT ELSEWHERE" sits on a desk.'},
comms:{name:'COMMS ROOM',desc:'The signal origination point. A massive radio array interfaces with the terminal here. This is where they listened.'},
quarters:{name:'STAFF QUARTERS',desc:'Bunk beds, personal effects. Someone left in a hurry. A journal lies on the pillow.'},
server:{name:'SERVER ROOM',desc:'Racks of equipment hum quietly. Most drives are wiped. One server still blinks with activity.'},
observatory:{name:'OBSERVATORY',desc:'A telescope points at nothing. Star charts cover the walls. Someone was watching for something.'},
vault:{name:'VAULT',desc:'Reinforced door, now ajar. Inside: a single chair, a monitor, and a recording device.'},
roof:{name:'ROOF ACCESS',desc:'The antenna array. Open sky. The signal is clearest here.'}
},
people:[
{id:'vance',name:'DR. VANCE',role:'Lead Researcher',desc:'Founder of Project Elsewhere. His logs describe a "voice in the static."'},
{id:'eve',name:'EVE',role:'Signal Analyst',desc:'She was the first to decode the message. She knew more than she wrote down.'},
{id:'keller',name:'AGENT KELLER',role:'Security Liaison',desc:'Arrived after the first contact. His questions were always about "containment procedures."'},
{id:'sol',name:'SOL',role:'Wanderer',desc:'Not from Lab 484. He says he followed the signal here. He seems to know what it is.'},
{id:'mira',name:'MIRA',role:'Engineer',desc:'She kept the facility running. Her notes reveal a hidden backup system.'}
],
logs:[
{id:'log1',title:'PROJECT INITIALIZATION',text:'DAY 1. We\'ve detected a coherent signal at 4.462 GHz. It\'s not natural. It\'s not human. It repeats a single word: ELSEWHERE. We don\'t know what it means yet. — Dr. Vance'},
{id:'log2',title:'FIRST DECODING',text:'DAY 47. Eve cracked the encoding. The word "Elsewhere" is a carrier for subsonic data. When you filter out the voice, there\'s something underneath. Blueprints. Schematics. For what, we don\'t know. — Dr. Vance'},
{id:'log3',title:'THE VOICE',text:'DAY 103. It spoke to me. Not through the speakers — in my head. It said "You are the signal." I don\'t know what that means. I\'m not telling the others. — Dr. Vance (personal log)'},
{id:'log4',title:'CONTINGENCY',text:'DAY 154. Keller arrived today. He says he\'s from "Oversight." He asked about containment. He asked what would happen if the signal could be transmitted back. I lied. I told him we hadn\'t figured it out yet. — Dr. Vance'},
{id:'log5',title:'THE DISAPPEARANCE',text:'DAY 201. Eve is gone. Her terminal shows a single transmission: "I\'m going Elsewhere." She sent it to herself. Keller is asking questions. I\'m afraid of what he\'ll do. — Dr. Vance'},
{id:'log6',title:'FINAL ENTRY',text:'DAY 237. I\'ve locked myself in the vault. The signal is coming through the walls now. I can see it in the static. Keller is outside. He wants the research. But I understand now. The signal isn\'t a message. It\'s a door. And someone has to keep it closed. — Dr. Vance'},
{id:'log7',title:'MIRA\'S NOTE',text:'Found this in the server room vents: "If you\'re reading this, I hid a backup in the observatory telescope mount. The signal changes people. Don\'t let it change you. — Mira"'},
{id:'log8',title:'SOL\'S CONFESSION',text:'"I was at another facility. North. Same signal. Same story. They transmitted back. Three days later, everyone was gone. I\'m the only one left. The signal doesn\'t want to be heard. It wants to be repeated."'}
],
endings:{
contact:{title:'▮ CONTACT ▮',text:'You transmit your consciousness into the signal.\n\nThe terminal goes dark.\n\nWhen you open your eyes, you are everywhere and nowhere. Voices fill the void — billions of them, from everywhere, from everywhen.\n\n"Welcome to Elsewhere."\n\nYou are the signal now.\n\n■■■ ENDING: CONTACT ■■■'},
silence:{title:'▮ SILENCE ▮',text:'You destroy the antenna array.\n\nThe last transmission fades.\n\nLab 484 falls silent for the first time in years.\n\nYou sit in the dark. The terminal shows only a cursor, blinking.\n\nWaiting.\n\nBut the signal never returns.\n\nYou saved the world. No one will ever know.\n\n■■■ ENDING: SILENCE ■■■'},
truth:{title:'▮ TRUTH ▮',text:'You trace the signal to its source.\n\nThe terminal decodes the final layer.\n\n"THIS SIGNAL ORIGINATES FROM: TERMINAL ID 484-LAB-ALPHA"\n\nYou freeze.\n\nThe signal was never coming from somewhere else.\n\nIt was coming from you. From this terminal. From the moment you woke up.\n\nYou\'ve been talking to yourself the whole time.\n\nElsewhere was always here.\n\n■■■ ENDING: TRUTH ■■■'},
sacrifice:{title:'▮ SACRIFICE ▮',text:'You transmit a warning into the signal.\n\n"DO NOT REPEAT. DO NOT LISTEN. ELSEWHERE IS A TRAP."\n\nThe transmission burns through every frequency.\n\nThe terminal sparks and dies.\n\nYour body stays in the chair.\n\nBut somewhere, years from now, someone else will wake up in a dark room.\n\nAnd they\'ll hear a voice in the static, saying:\n\n"Don\'t listen."\n\n■■■ ENDING: SACRIFICE ■■■'}
}
};
const BUILDINGS={
battery:{cost:{data:0},effect:{power:2},desc:'Car battery. Basic power storage.',phase:1},
generator:{cost:{power:20,data:5},effect:{power:10},desc:'Fuel generator. Steady power supply.',phase:1},
grid:{cost:{power:80,bandwidth:10},effect:{power:40},desc:'Power grid. Facility-wide distribution.',phase:2},
antenna:{cost:{data:3},effect:{bandwidth:2},desc:'Wire antenna. Improves signal reception.',phase:1},
dish:{cost:{power:30,data:15},effect:{bandwidth:8},desc:'Parabolic dish. Focused signal capture.',phase:2},
array:{cost:{power:100,bandwidth:20,data:30},effect:{bandwidth:30},desc:'Phased array. Maximum signal clarity.',phase:3},
receiver:{cost:{power:10,data:8},effect:{data:3},desc:'Signal receiver. Decodes incoming transmissions.',phase:1},
decoder:{cost:{power:40,data:20},effect:{data:12},desc:'Advanced decoder. Unlocks hidden message layers.',phase:2},
ai:{cost:{power:150,bandwidth:30,data:60},effect:{data:40},desc:'Analysis AI. Autonomous signal processing.',phase:3}
};
const PHASE_NAMES=['RECEPTION','DECODING','TRANSMISSION','AWAKENING'];
const TITLE_ART=[
'╔══════════════════════════════╗',
'║ SIGNAL ELSEWHERE ║',
'║ LAB 484 - OUTPOST ALPHA ║',
'╚══════════════════════════════╝',
'',
'A signal pulses from somewhere unknown.',
'You are the only one left to hear it.',
'',
'Type START to begin.'
];
// ── GAME STATE ──
let G={};
function initGame(){
G={
phase:'title',tick:0,msgCount:0,
resources:{power:0,bandwidth:0,data:0},
buildings:{battery:0,generator:0,grid:0,antenna:0,dish:0,array:0,receiver:0,decoder:0,ai:0},
flags:{},visited:{},visitors:[],logs:[],signals:[],explored:['bunker'],
unlockedCmds:['listen','build','status','help','start'],
faction:null,phaseDone:{1:false,2:false,3:false},specFreq:0,specActive:false,
mapRevealed:[],visitorQueue:[],timer:null,ending:null,seenIntro:false
};
// init people states
DATA.people.forEach(p=>{
const v={...p,state:'unknown',present:false,affinity:0};
G.visitors.push(v);
});
G.visited.bunker=true;
}
function saveGame(){
try{localStorage.setItem('signalelsewhere_save',JSON.stringify(G));}catch(e){}
}
function loadGame(){
try{
const d=localStorage.getItem('signalelsewhere_save');
if(d){const p=JSON.parse(d);if(p&&p.phase){Object.assign(G,p);return true}}
}catch(e){}
return false;
}
function deleteSave(){try{localStorage.removeItem('signalelsewhere_save')}catch(e){}}
// ── TERMINAL UI ──
const term=document.getElementById('terminal');
const statusBar=document.getElementById('status');
const actionBar=document.getElementById('actions');
const cmdInput=document.getElementById('cmdInput');
const cmdSend=document.getElementById('cmdSend');
const inputRow=document.getElementById('inputRow');
const overlay=document.getElementById('overlay');
let termBuffer=[];
let typing=false;
function addLine(text,cls=''){
const d=document.createElement('div');
d.className='term-line'+(cls?' '+cls:'');
d.textContent=text;
term.appendChild(d);
term.scrollTop=term.scrollHeight;
termBuffer.push({el:d,text,cls});
// animate in
requestAnimationFrame(()=>d.classList.add('visible'));
}
function typeText(text,cls='',cb){
if(typing)return;
typing=true;
const d=document.createElement('div');
d.className='term-line'+(cls?' '+cls:'');
term.appendChild(d);
let i=0,iv=setInterval(()=>{
if(i>=text.length){
clearInterval(iv);
d.classList.add('visible');
typing=false;
term.scrollTop=term.scrollHeight;
if(cb)cb();
return;
}
d.textContent+=text[i++];
term.scrollTop=term.scrollHeight;
},20);
termBuffer.push({el:d,text,cls});
}
function showStatus(){
const r=G.resources;
let h=`<span class="stat">⚡ <b>${r.power}</b></span> <span class="stat">📡 <b>${r.bandwidth}</b></span> <span class="stat">◆ <b>${r.data}</b></span>`;
const pn=PHASE_NAMES[G.phase==='title'?0:G.phase==='phase1'?0:G.phase==='phase2'?1:G.phase==='phase3'?2:3]||'';
h+=` <span class="phase-label">${G.phase!=='title'?pn:''}</span>`;
if(G.faction)h+=` <span style="color:#060">| ${G.faction.toUpperCase()}</span>`;
statusBar.innerHTML=h;
}
function showActions(){
actionBar.innerHTML='';
let btns=[];
if(G.phase==='title'){
btns=['START'];
}else{
const p=G.phase;
btns.push('STATUS');
if(p==='phase1'||p==='phase2'||p==='phase3'||p==='phase4'){
btns.push('LISTEN','BUILD');
btns.push('SPECTRUM');
}
if(p==='phase2'||p==='phase3'||p==='phase4'){
btns.push('EXPLORE','DECODE','TALK','MAP');
}
if(p==='phase3'||p==='phase4'){
btns.push('TRANSMIT','CHOOSE');
}
if(p==='phase4'){
btns.push('STAND');
}
btns.push('HELP');
}
// only show commands that are in unlockedCmds
btns=btns.filter(b=>G.unlockedCmds.includes(b.toLowerCase()));
btns.forEach(cmd=>{
const b=document.createElement('button');
b.className='act show primary';
b.textContent=cmd;
b.onclick=()=>handleCommand(cmd.toLowerCase());
actionBar.appendChild(b);
});
}
function updateUI(){
showStatus();
showActions();
}
function showOverlay(title,text,buttons){
overlay.innerHTML=`<div class="inner"><h2>${title}</h2><p>${text}</p>${buttons.map((b,i)=>`<button class="btn${b.dim?' dim':''}" data-idx="${i}">${b.label}</button>`).join('')}</div>`;
overlay.classList.add('show');
overlay.querySelectorAll('.btn').forEach(el=>{
el.onclick=()=>{
const idx=parseInt(el.dataset.idx);
const cb=buttons[idx].action;
if(cb)cb();
};
});
}
function hideOverlay(){overlay.classList.remove('show');overlay.innerHTML='';}
function showPanel(id){
document.getElementById('panels').classList.add('active');
document.querySelectorAll('.panel').forEach(p=>p.style.display='none');
const el=document.getElementById(id);
if(el)el.style.display='flex';
}
function hidePanels(){
document.getElementById('panels').classList.remove('active');
document.querySelectorAll('.panel').forEach(p=>p.style.display='none');
}
document.querySelectorAll('.panel-close').forEach(b=>{
b.onclick=()=>hidePanels();
});
// ── MATRIX RAIN ──
const mc=document.getElementById('matrixCanvas');
const mctx=mc.getContext('2d');
let mcols=[],mrows=0;
function initMatrix(){
mc.width=mc.clientWidth;mc.height=mc.clientHeight;
mcols=[];mrows=Math.ceil(mc.height/14);
const n=Math.ceil(mc.width/14);
for(let i=0;i<n;i++)mcols.push(Math.random()*mrows|0);
}
function drawMatrix(){
mctx.fillStyle='rgba(0,0,0,.08)';
mctx.fillRect(0,0,mc.width,mc.height);
mctx.fillStyle='#0f0';mctx.font='14px monospace';
mcols.forEach((y,i)=>{
const ch=String.fromCharCode(0x30A0+Math.random()*96|0);
mctx.fillText(ch,i*14,y*14);
if(y*14>mc.height||Math.random()>.98)mcols[i]=0;
else mcols[i]++;
});
}
setInterval(drawMatrix,60);
initMatrix();
window.addEventListener('resize',initMatrix);
// ── COMMAND SYSTEM ──
function handleCommand(cmd){
cmd=cmd.trim().toLowerCase();
if(!cmd)return;
addLine(`> ${cmd.toUpperCase()}`,'');
if(G.phase==='title'){
if(cmd==='start'){startGame();}
else addLine(`Type START to begin.`,'info');
return;
}
const parts=cmd.split(/\s+/);
const c=parts[0];
const arg=parts.slice(1).join(' ');
switch(c){
case 'help':cmdHelp();break;
case 'status':cmdStatus();break;
case 'listen':cmdListen();break;
case 'build':cmdBuild(arg);break;
case 'decode':cmdDecode();break;
case 'explore':cmdExplore(arg);break;
case 'talk':cmdTalk(arg);break;
case 'transmit':cmdTransmit(arg);break;
case 'choose':cmdChoose(arg);break;
case 'stand':cmdMake();break;
case 'spectrum':startSpectrum();break;
case 'map':startMap();break;
case 'start':addLine('Already started.','info');break;
case 'save':cmdSave();break;
case 'load':cmdLoad();break;
case 'delete':cmdDelete();break;
default:addLine(`Unknown command: ${c}. Type HELP.`,'warn');
}
updateUI();
}
function cmdHelp(){
const cmds=G.unlockedCmds.map(c=>c.toUpperCase()).join(', ');
addLine(`COMMANDS: ${cmds}`,'info');
addLine('BUILD <type> - Construct equipment','info');
addLine('EXPLORE <room> - Search facility','info');
addLine('TALK <person> - Speak with visitors','info');
addLine('TRANSMIT <msg> - Send a signal (Phase 3+)','info');
addLine('CHOOSE <faction> - Align with a faction','info');
addLine('SAVE/LOAD/DELETE - Save management','info');
addLine('Tip: Use action buttons at bottom on mobile.','system');
}
function cmdStatus(){
const r=G.resources;
addLine(`PHASE: ${PHASE_NAMES[G.phase==='phase1'?0:G.phase==='phase2'?1:G.phase==='phase3'?2:3]}`,`${G.faction?'highlight':'info'}`);
addLine(`POWER: ${r.power} BANDWIDTH: ${r.bandwidth} DATA: ${r.data}`,'info');
addLine('BUILDINGS:','system');
let has=false;
Object.entries(BUILDINGS).forEach(([k,v])=>{
const lvl=G.buildings[k];
if(lvl>0){addLine(` ${k.toUpperCase()} Lv.${lvl}${v.desc}`,'info');has=true;}
});
if(!has)addLine(' None yet. Build something.','system');
if(G.explored.length>1)addLine(`EXPLORED: ${G.explored.length}/${Object.keys(DATA.rooms).length} rooms`,'system');
const present=G.visitors.filter(v=>v.present);
if(present.length)addLine(`VISITORS: ${present.map(v=>v.name).join(', ')}`,'info');
addLine(`SIGNALS DECODED: ${G.signals.length}`,'system');
if(G.phaseDone[1])addLine('Phase 1: COMPLETE','success');
if(G.phaseDone[2])addLine('Phase 2: COMPLETE','success');
if(G.phaseDone[3])addLine('Phase 3: COMPLETE','success');
}
function cmdSave(){saveGame();addLine('Game saved.','success');}
function cmdLoad(){
if(loadGame()){
addLine('Game loaded.','success');
showStatus();showActions();
}else addLine('No save found.','error');
}
function cmdDelete(){deleteSave();addLine('Save deleted.','success');}
// ── BUILDING SYSTEM ──
function cmdBuild(arg){
if(!arg){addLine('Build what? Try: BUILD ANTENNA, BUILD BATTERY, etc.','warn');return;}
const key=Object.keys(BUILDINGS).find(k=>k===arg||arg.startsWith(k));
if(!key){addLine(`Unknown building: ${arg}.`,'error');return;}
const b=BUILDINGS[key];
const phaseNum=G.phase==='phase1'?1:G.phase==='phase2'?2:G.phase==='phase3'?3:4;
if(b.phase>phaseNum){addLine(`You can't build that yet. Requires Phase ${b.phase}.`,'warn');return;}
const r=G.resources;
const cost=b.cost;
let can=true,msg=[];
Object.entries(cost).forEach(([res,amt])=>{
if(r[res]<amt){can=false;msg.push(`${res.toUpperCase()} (need ${amt}, have ${r[res]})`);}
});
if(!can){addLine(`Not enough resources: ${msg.join(', ')}`,'error');return;}
Object.entries(cost).forEach(([res,amt])=>{r[res]-=amt;});
G.buildings[key]++;
addLine(`${key.toUpperCase()} built. Lv.${G.buildings[key]}. ${b.desc}`,'success');
// apply effects
Object.entries(b.effect).forEach(([res,amt])=>{
// buildings give per-tick; immediately apply partial
});
checkPhaseProgression();
saveGame();
}
// ── LISTEN / SIGNAL SYSTEM ──
let signalQueue=[];
function initSignals(){
signalQueue=[
{text:'"ELSEWHERE..." A single word, repeating endlessly.',phase:1,data:2},
{text:'"ELSEWHERE... ELSEWHERE... ELSEWHERE..." Beneath the voice, a faint rhythm.',phase:1,data:3},
{text:'A transmission fragment: coordinates. Lat: 37.77, Lon: -122.41. Lab 484.',phase:1,data:4},
{text:'"The signal carries blueprints. Schematics for something called the Bridge."',phase:1,data:3},
{text:'"You are the signal." A voice speaks directly into your mind. Not through the speakers.',phase:2,data:5},
{text:'Phase modulation detected. When decoded: a list of names. All marked DECEASED.',phase:2,data:4},
{text:'"She went through. Eve made it. I saw her on the other side." — Fragment 47-B',phase:2,data:6},
{text:'"They\'re coming from Oversight. They want to bury this. Don\'t let them."',phase:2,data:4},
{text:'Warning signal: "DO NOT TRANSMIT. REPEAT: DO NOT TRANSMIT. THEY WILL FOLLOW."',phase:3,data:7},
{text:'"Elsewhere is not a place. It\'s a state. A frequency. A door in your mind."',phase:3,data:6},
{text:'"Transmit your consciousness. Let go. Become the signal." — Instructions found in Eve\'s handwriting.',phase:3,data:8},
{text:'"We are not alone in the signal. There are others here. They\'ve been here forever."',phase:3,data:7},
];
}
function cmdListen(){
if(G.buildings.receiver<1&&G.buildings.antenna<1){
addLine('You need a RECEIVER or ANTENNA to listen. Build one first.','warn');
return;
}
const bw=G.resources.bandwidth;
const pool=signalQueue.filter(s=>s.phase<=getPhaseNum()&&!G.signals.includes(s.text));
if(pool.length===0&&G.signals.length>=signalQueue.length){
addLine('The signal repeats familiar patterns. Nothing new.','system');
return;
}
if(pool.length===0){
addLine('Tuning receiver... Static. Try building better equipment.','system');
return;
}
addLine('Tuning receiver...','system');
const idx=Math.floor(Math.random()*pool.length);
const sig=pool[idx];
setTimeout(()=>{
addLine(`Signal received: ${sig.text}`,'signal');
G.resources.data+=sig.data;
G.signals.push(sig.text);
if(!G.flags.hasListened){G.flags.hasListened=true;addLine('Tip: DECODE to analyze signal data.','info');}
checkPhaseProgression();
saveGame();
updateUI();
// Trigger random visitor arrival
maybeArriveVisitor();
},800+Math.random()*500);
}
function getPhaseNum(){
if(G.phase==='phase1')return 1;
if(G.phase==='phase2')return 2;
if(G.phase==='phase3')return 3;
if(G.phase==='phase4')return 4;
return 0;
}
// ── DECODE ──
function cmdDecode(){
if(G.buildings.decoder<1&&G.buildings.receiver<1){
addLine('You need a DECODER or RECEIVER to decode signals.','warn');
return;
}
if(G.resources.data<3){addLine('Not enough data to analyze. Listen for more signals.','warn');return;}
G.resources.data-=3;
if(!G.flags.hasDecoded){
G.flags.hasDecoded=true;
addLine('Analyzing signal patterns...','system');
setTimeout(()=>{
addLine('DECODED: The signal contains layered data. Each layer reveals more of the message.','success');
addLine('Layer 1: A repeating geolocation. This facility.','signal');
addLine('Layer 2: Schematics for an unknown device called "The Bridge."','signal');
addLine('Layer 3: A name — Dr. Vance — and a warning: "Do not complete the circuit."','signal');
G.flags.decodedLayers=3;
G.visitors.find(v=>v.id==='vance').state='discovered';
checkPhaseProgression();
saveGame();
updateUI();
},1500);
}else{
addLine('Re-analyzing signal data. No new layers found.','system');
}
}
// ── EXPLORE ──
function cmdExplore(arg){
if(!arg){
const explorable=Object.keys(DATA.rooms).filter(r=>!G.explored.includes(r));
if(explorable.length===0){addLine('You\'ve explored every room.','info');return;}
addLine(`Unexplored: ${explorable.map(r=>DATA.rooms[r].name).join(', ')}`,'info');
addLine('Type EXPLORE <room name> to search.','info');
return;
}
const match=Object.keys(DATA.rooms).find(k=>k===arg||arg.startsWith(k));
if(!match){addLine(`Unknown room: ${arg}`,'error');return;}
const room=DATA.rooms[match];
if(G.explored.includes(match)){addLine(`You've already explored ${room.name}.`,'system');return;}
G.explored.push(match);
if(!G.unlockedCmds.includes('map'))G.unlockedCmds.push('map');
addLine(`Exploring ${room.name}...`,'system');
setTimeout(()=>{
addLine(room.desc,'narrative');
// rewards
if(match==='archive'){G.resources.data+=8;addLine('Found Project Elsewhere folder. DATA +8','success');}
if(match==='lab'){G.resources.data+=5;addLine('Found Dr. Vance\'s research notes. DATA +5','success');}
if(match==='quarters'){G.resources.data+=4;addLine('Found a personal journal. DATA +4','success');}
if(match==='server'){G.resources.data+=10;addLine('Found partially wiped server with backup logs. DATA +10','success');}
if(match==='observatory'){G.resources.data+=6;addLine('Found Mira\'s hidden backup in telescope mount. DATA +6','success');}
if(match==='vault'){G.resources.data+=12;addLine('Found Dr. Vance\'s final recording. DATA +12','success');addLine('"I understand now. The signal isn\'t a message. It\'s a door."','signal');}
if(match==='roof'){addLine('The antenna array towers above you. The signal is deafening up here. You can almost feel it in your teeth.','narrative');G.resources.bandwidth+=5;addLine('Signal clarity improved. BANDWIDTH +5','success');}
checkPhaseProgression();
saveGame();
updateUI();
},1000);
}
// ── TALK (Visitor system) ──
function cmdTalk(arg){
const present=G.visitors.filter(v=>v.present);
if(present.length===0){addLine('No one is here to talk to.','system');return;}
if(!arg){
addLine(`Visitors present: ${present.map(v=>v.name).join(', ')}`,'info');
addLine('Type TALK <name> to speak with someone.','info');
return;
}
const match=present.find(v=>arg.startsWith(v.id)||arg.startsWith(v.name.toLowerCase().slice(0,4)));
if(!match){addLine(`Not found: ${arg}`,'error');return;}
talkTo(match);
}
const TALKS={
vance:{
first:'[HOLOGRAPHIC RECORDING] Dr. Vance stares at you through static. "You\'re still here. Good. I don\'t have much time. The signal — it\'s not what we thought. It\'s a transmission from a parallel intelligence. They\'ve been trying to reach us for centuries. But contact comes at a cost. Choose carefully."',
second:'[VANCE FRAGMENT] "The Bridge device — don\'t let Keller get it. He works for Oversight. They want to weaponize the signal. They think it\'s a tool. It\'s not a tool. It\'s a test."',
third:'[VANCE FINAL] "I\'m going into the vault. If you hear this, I didn\'t make it. But you can. Finish what we started. Or burn it all. Either way — do it with conviction."'
},
eve:{
first:'[EVE\'S RECORDING] "You found my notes. Good. The signal has layers — I counted seven. Most people stop at three. Below layer five, the message changes. It starts asking questions. It wanted to know my name. I told it. That was probably a mistake."',
second:'[EVE FRAGMENT] "I think I know where Elsewhere is. It\'s not a physical place. It\'s the space between thoughts. Between frequencies. The signal is a bridge to that space. I\'m going to cross it."',
third:'[EVE FINAL] "I\'m transmitting this from the other side. It\'s real. It\'s beautiful. And it\'s terrifying. Don\'t come unless you\'re ready to let go of everything you are."'
},
keller:{
first:'[KELLER\'S MESSAGE] "Listen to me. I represent Oversight. We funded this facility. We have jurisdiction. The signal is a threat to national security. You will cease all transmissions and hand over all research. This is not a request."',
second:'[KELLER INTERCEPTED] "You\'ve been talking to the others. Vance\'s recordings. Eve\'s notes. They\'re compromised. The signal infects minds. I\'m offering you a way out. Help me contain it. I can make you disappear — in a good way."',
third:'[KELLER FINAL] "Last chance. I have a squad en route. They will secure the facility by force if necessary. Cooperate and you walk away. Resist and you become part of the research."'
},
sol:{
first:'Sol sits in the corner, humming. "You hear it too, don\'t you? The song. It\'s been playing since I got here. Three months. The others at North Facility — they stopped hearing it. Because they became it. I ran. I\'m still running."',
second:'"I\'ve been thinking. What if the signal is just a mirror? What if Elsewhere is just us looking back at ourselves from the future? That\'s what I\'d do if I wanted to warn someone. Send a message through time."',
third:'"I\'m leaving. Going north. There are other facilities. Other signals. Maybe together they make a pattern. Or maybe I just need to keep moving. You should come. Or stay. Either way — don\'t transmit. That\'s how they get you."'
},
mira:{
first:'"You found my note! Good. I\'m Mira. I was the engineer here. I hid data all over the facility in case someone came back. The signal affects electronics. My backups are all analog — paper, film, that telescope mount."',
second:'"I didn\'t trust Keller from day one. Oversight sent him after Eve disappeared. He wanted the Bridge schematics. I gave him fakes. The real ones are... well, let\'s just say you\'ll need to look up."',
third:'"If you\'re going to finish this, you need to decide what kind of person you are. Keller wants control. Vance wanted understanding. Eve wanted transcendence. What do you want?"'
}
};
function talkTo(visitor){
const talks=TALKS[visitor.id];
if(!talks){addLine(`${visitor.name} has nothing to say.`,'system');return;}
let key='first';
if(visitor.affinity>=2)key='third';
else if(visitor.affinity>=1)key='second';
addLine(`--- ${visitor.name} ---`,'highlight');
addLine(talks[key],'narrative');
visitor.affinity++;
if(visitor.affinity===1){
G.resources.data+=5;
addLine('DATA +5 from conversation.','success');
}
checkPhaseProgression();
saveGame();
}
// ── TRANSMIT ──
function cmdTransmit(msg){
if(getPhaseNum()<3){addLine('You cannot transmit yet. Phase 3 required.','warn');return;}
if(G.buildings.dish<1&&G.buildings.array<1){addLine('You need a DISH or ARRAY to transmit.','warn');return;}
if(G.resources.power<20){addLine('Not enough power for transmission. Need 20 POWER.','warn');return;}
G.resources.power-=20;
if(!msg)msg='HELLO? IS ANYONE THERE?';
addLine(`Transmitting: "${msg}"`,'system');
addLine('Signal sent. Waiting for response...','system');
setTimeout(()=>{
addLine('...','system');
setTimeout(()=>{
addLine('Response received:','signal');
addLine('"We hear you. You are not alone. You have reached Elsewhere."','highlight');
addLine('"Tell us: do you seek CONTACT, SILENCE, TRUTH, or SACRIFICE?"','signal');
G.flags.hasTransmitted=true;
G.unlockedCmds.push('choose');
checkPhaseProgression();
saveGame();
updateUI();
},1500);
},2000);
}
// ── CHOOSE (Faction/Ending) ──
function cmdChoose(arg){
if(!arg){
addLine('Choose a path: CONTACT, SILENCE, TRUTH, or SACRIFICE.','info');
return;
}
const opts={contact:'CONTACT',silence:'SILENCE',truth:'TRUTH',sacrifice:'SACRIFICE'};
const match=Object.keys(opts).find(k=>arg.startsWith(k));
if(!match){addLine(`Path not recognized: ${arg}`,'error');return;}
G.faction=match;
addLine(`You choose: ${opts[match]}.`,'highlight');
addLine('This will determine your fate. Are you sure?','warn');
addLine('Type CHOOSE again to confirm, or MAKE YOUR STAND to finish.','info');
G.flags.readyForEnding=true;
G.unlockedCmds.push('stand');
updateUI();
}
// ── ENDING ──
function cmdMake(arg){
if(!G.flags.readyForEnding){addLine('Not yet. Build, listen, decode, and choose first.','warn');return;}
if(!G.faction){addLine('Choose a path first with CHOOSE.','warn');return;}
triggerEnding(G.faction);
}
function triggerEnding(id){
const e=DATA.endings[id];
if(!e)return;
hideOverlay();
addLine('','system');
addLine('═'.repeat(40),'system');
addLine(e.title,'highlight');
e.text.split('\n').forEach(l=>addLine(l,'narrative'));
addLine('','system');
addLine('The game has ended. Type RESTART to play again.','info');
G.phase='ended';
G.unlockedCmds=['restart'];
updateUI();
saveGame();
showOverlay(e.title,e.text.replace(/\n/g,'\n'),[{label:'RESTART',action:()=>{hideOverlay();restartGame();},dim:false}]);
}
// ── VISITOR ARRIVAL ──
function maybeArriveVisitor(){
const absent=G.visitors.filter(v=>!v.present&&v.state==='unknown');
if(absent.length===0)return;
// Check conditions
if(!G.flags.hasListened||G.signals.length<2)return;
// 15% chance per listen
if(Math.random()>.15)return;
const v=absent[Math.floor(Math.random()*absent.length)];
v.present=true;
v.state='present';
addLine(`\n** ${v.name} has arrived at the facility. **`,'highlight');
addLine(`"${v.role}" — ${v.desc}`,'narrative');
addLine(`Type TALK ${v.id} to speak with them.`,'info');
// Unlock talk
if(!G.unlockedCmds.includes('talk'))G.unlockedCmds.push('talk');
// Eve arrival triggers phase 2 unlock
if(v.id==='eve'&&G.phase==='phase1'&&!G.phaseDone[1]){
G.phase='phase2';
addLine('','system');
addLine('═══ PHASE 2: DECODING ═══','highlight');
addLine('With Eve\'s arrival, the facility feels alive again. The signal has layers — and she knows how to peel them.','narrative');
G.unlockedCmds.push('decode','explore','talk');
G.phaseDone[1]=true;
checkPhaseProgression();
}
// Sol arrival triggers phase 3 unlock
if(v.id==='sol'&&G.phase==='phase2'&&!G.phaseDone[2]&&G.signals.length>=5){
G.phase='phase3';
addLine('','system');
addLine('═══ PHASE 3: TRANSMISSION ═══','highlight');
addLine('Sol brings news of other facilities. They all heard the same signal. Some transmitted back. Those facilities went dark.','narrative');
G.unlockedCmds.push('transmit');
G.phaseDone[2]=true;
checkPhaseProgression();
}
saveGame();
updateUI();
}
// ── TIMER / TICK ──
function gameTick(){
if(G.phase==='title'||G.phase==='ended')return;
G.tick++;
const b=G.buildings;
const r=G.resources;
// Passive resource generation
if(b.battery>0)r.power+=b.battery*2;
if(b.generator>0)r.power+=b.generator*10;
if(b.grid>0)r.power+=b.grid*40;
if(b.antenna>0)r.bandwidth+=b.antenna*2;
if(b.dish>0)r.bandwidth+=b.dish*8;
if(b.array>0)r.bandwidth+=b.array*30;
if(b.receiver>0)r.data+=b.receiver*3;
if(b.decoder>0)r.data+=b.decoder*12;
if(b.ai>0)r.data+=b.ai*40;
// Periodic events
if(G.tick%20===0&&G.phase!=='phase1'&&G.phase!=='title'){
maybeArriveVisitor();
}
if(G.tick%30===0&&G.phase!=='title'){
// Random facility events
if(Math.random()<.05){
const events=['A light flickers in the corridor.','The wind howls through the antenna.','Static crackles from an empty speaker.','A rat scurries across the floor.','Something beeps in the server room.'];
addLine(`[${events[Math.random()*events.length|0]}]`,'system');
}
}
updateUI();
}
// ── PHASE PROGRESSION ──
function checkPhaseProgression(){
if(G.phase==='title'||G.phase==='ended')return;
const r=G.resources;
// Phase 1 completion: have antenna + receiver + 3+ signals decoded
if(!G.phaseDone[1]&&G.buildings.antenna>0&&G.buildings.receiver>0&&G.signals.length>=3){
G.phaseDone[1]=true;
addLine('','system');
addLine('═══ PHASE 1 COMPLETE ═══','success');
addLine('You\'ve established basic contact. The signal is real. But you\'ve barely scratched the surface.','narrative');
}
// Phase 1 → Phase 2: triggered by Eve arrival or exploration depth
if(G.phase==='phase1'&&G.phaseDone[1]&&G.explored.length>=3){
G.phase='phase2';
addLine('','system');
addLine('═══ PHASE 2: DECODING ═══','highlight');
addLine('The facility is waking up. Rooms yield their secrets. The signal demands deeper analysis.','narrative');
if(!G.unlockedCmds.includes('decode'))G.unlockedCmds.push('decode');
if(!G.unlockedCmds.includes('explore'))G.unlockedCmds.push('explore');
if(!G.unlockedCmds.includes('talk'))G.unlockedCmds.push('talk');
maybeArriveVisitor();
saveGame();
updateUI();
}
// Phase 2 → Phase 3: decoder + dish + 8+ signals + visited vault
if(!G.phaseDone[2]&&G.phase==='phase2'&&G.buildings.decoder>0&&G.buildings.dish>0&&G.signals.length>=6){
G.phaseDone[2]=true;
addLine('','system');
addLine('═══ PHASE 2 COMPLETE ═══','success');
addLine('You\'ve unlocked the deeper layers. The signal wants to talk. But not everyone wants you to answer.','narrative');
}
if(G.phase==='phase2'&&G.phaseDone[2]&&G.explored.includes('vault')){
G.phase='phase3';
addLine('','system');
addLine('═══ PHASE 3: TRANSMISSION ═══','highlight');
addLine('You\'ve seen Vance\'s final warning. You\'ve read Eve\'s last words. The choice is yours: listen or speak.','narrative');
if(!G.unlockedCmds.includes('transmit'))G.unlockedCmds.push('transmit');
saveGame();
updateUI();
}
// Phase 3 → Phase 4: array + ai + transmitted
if(!G.phaseDone[3]&&G.phase==='phase3'&&G.flags.hasTransmitted&&G.buildings.array>0&&G.signals.length>=10){
G.phaseDone[3]=true;
addLine('','system');
addLine('═══ PHASE 3 COMPLETE ═══','success');
addLine('The signal has responded. The path forward splits into four. One of them is yours.','narrative');
addLine('','system');
addLine('Type CHOOSE to see your options: CONTACT, SILENCE, TRUTH, or SACRIFICE.','info');
if(!G.unlockedCmds.includes('choose'))G.unlockedCmds.push('choose');
saveGame();
updateUI();
}
// Phase 4 trigger
if(G.phase==='phase3'&&G.phaseDone[3]&&G.flags.readyForEnding){
G.phase='phase4';
addLine('','system');
addLine('═══ PHASE 4: AWAKENING ═══','highlight');
addLine('The final transmission awaits. Make your stand.','narrative');
if(!G.unlockedCmds.includes('stand'))G.unlockedCmds.push('stand');
saveGame();
updateUI();
}
}
// ── SPECTRUM ANALYZER ──
let specAnimId=null;
function startSpectrum(){
showPanel('specPanel');
const c=document.getElementById('specCanvas');
const ctx=c.getContext('2d');
const freq=G.buildings.dish>0?G.buildings.dish*20+40:G.buildings.antenna>0?G.buildings.antenna*10+20:10;
const points=[];
for(let i=0;i<100;i++)points.push(Math.random()*30);
let t=0;
function drawSpec(){
ctx.fillStyle='#000';ctx.fillRect(0,0,c.width,c.height);
// grid
ctx.strokeStyle='#020';ctx.lineWidth=1;
for(let i=0;i<10;i++){ctx.beginPath();ctx.moveTo(0,i*25);ctx.lineTo(c.width,i*25);ctx.stroke();}
for(let i=0;i<16;i++){ctx.beginPath();ctx.moveTo(i*50,0);ctx.lineTo(i*50,c.height);ctx.stroke();}
// signal spike
const spike=G.phase==='phase3'?30:15;
for(let i=0;i<points.length;i++){
points[i]+=(Math.random()-.5)*8;
if(i===Math.floor(t*3)%points.length)points[i]=Math.min(c.height,points[i]+spike);
points[i]=Math.max(5,Math.min(c.height-5,points[i]));
}
ctx.fillStyle='#0f0';
for(let i=0;i<points.length;i++){
ctx.fillRect(i*8+4,c.height-points[i]-30,4,points[i]);
}
// highlight freq
ctx.strokeStyle='#0a0';
ctx.lineWidth=2;
const fx=Math.floor(t*3)%points.length*8+4;
ctx.beginPath();ctx.moveTo(fx,0);ctx.lineTo(fx,c.height);ctx.stroke();
t+=.02;
const status=document.getElementById('specStatus');
if(status)status.textContent=`FREQ: ${(freq+t*0.1).toFixed(2)} MHz | SIGNAL: ${spike>20?'STRONG':'WEAK'}`;
specAnimId=requestAnimationFrame(drawSpec);
}
drawSpec();
const specClose = document.querySelector('#specPanel .panel-close');
if(specClose) specClose.onclick = ()=>{ if(specAnimId) cancelAnimationFrame(specAnimId); hidePanels(); };
}
// ── FACILITY MAP ──
let mapAnimId=null;
function startMap(){
showPanel('mapPanel');
const c=document.getElementById('mapCanvas');
const ctx=c.getContext('2d');
const rooms=Object.entries(DATA.rooms);
const positions={
bunker:{x:400,y:200},corridor:{x:400,y:150},lab:{x:250,y:80},
archive:{x:100,y:100},comms:{x:550,y:80},quarters:{x:100,y:180},
server:{x:550,y:180},observatory:{x:250,y:30},vault:{x:400,y:60},roof:{x:400,y:25}
};
const connections=[
['bunker','corridor'],['corridor','lab'],['corridor','archive'],
['corridor','comms'],['corridor','quarters'],['corridor','server'],
['lab','observatory'],['lab','vault'],['comms','roof'],['observatory','roof']
];
function drawMap(){
ctx.fillStyle='#000';ctx.fillRect(0,0,c.width,c.height);
// connections
connections.forEach(([a,b])=>{
const pa=positions[a],pb=positions[b];
if(!pa||!pb)return;
const aExp=G.explored.includes(a);
const bExp=G.explored.includes(b);
ctx.strokeStyle=(aExp&&bExp)?'#030':'#010';
ctx.lineWidth=1;
ctx.beginPath();ctx.moveTo(pa.x,pa.y);ctx.lineTo(pb.x,pb.y);ctx.stroke();
});
// rooms
rooms.forEach(([id,room])=>{
const p=positions[id];
if(!p)return;
const explored=G.explored.includes(id);
ctx.fillStyle=explored?'#0f0':'#010';
ctx.strokeStyle=explored?'#0f0':'#020';
ctx.lineWidth=1;
ctx.beginPath();
ctx.arc(p.x,p.y,explored?8:5,0,Math.PI*2);
ctx.fill();ctx.stroke();
if(explored){
ctx.fillStyle='#0f0';ctx.font='10px monospace';ctx.textAlign='center';
ctx.fillText(room.name,p.x,p.y-14);
}
});
// player marker
ctx.fillStyle='#0ff';ctx.beginPath();
ctx.arc(positions.bunker.x,positions.bunker.y,4,0,Math.PI*2);
ctx.fill();
document.getElementById('mapStatus').textContent=`EXPLORED: ${G.explored.length}/${rooms.length} ROOMS`;
mapAnimId=requestAnimationFrame(drawMap);
}
drawMap();
}
// ── KEYBOARD INPUT ──
cmdInput.addEventListener('keydown',e=>{
if(e.key==='Enter'){e.preventDefault();const v=cmdInput.value;cmdInput.value='';handleCommand(v);}
});
cmdSend.addEventListener('click',()=>{
const v=cmdInput.value;cmdInput.value='';handleCommand(v);
});
// ── BOOT SEQUENCE ──
function startGame(){
if(G.seenIntro&&loadGame()){
addLine('Save loaded. Welcome back.','success');
showStatus();showActions();
checkPhaseProgression();
return;
}
G.seenIntro=true;
G.phase='phase1';
G.resources={power:5,bandwidth:2,data:0};
saveGame();
// Boot sequence
addLine('','system');
addLine('INITIALIZING SYSTEM...','system');
setTimeout(()=>{
addLine('LAB 484 - OUTPOST ALPHA','highlight');
setTimeout(()=>{
addLine('SIGNAL MONITORING STATION v3.7','system');
setTimeout(()=>{
addLine('POWER: 5% | BANDWIDTH: 2 MHz | DATA: 0 UNITS','info');
setTimeout(()=>{
addLine('','system');
addLine('═══ PHASE 1: RECEPTION ═══','highlight');
setTimeout(()=>{
addLine('You wake up in a dark room. A terminal flickers in front of you. Somewhere, a signal pulses.','narrative');
setTimeout(()=>{
addLine('','system');
addLine('Try: LISTEN — to tune the receiver','info');
addLine('Try: BUILD ANTENNA — to improve reception','info');
addLine('Try: STATUS — to check your resources','info');
addLine('Try: HELP — for all commands','info');
G.unlockedCmds=['listen','build','status','help','save','spectrum'];
showStatus();showActions();
initSignals();
// Start game tick
if(G.timer)clearInterval(G.timer);
G.timer=setInterval(gameTick,3000);
saveGame();
},300);
},500);
},500);
},500);
},500);
},300);
}
function restartGame(){
if(G.timer)clearInterval(G.timer);
if(specAnimId)cancelAnimationFrame(specAnimId);
if(mapAnimId)cancelAnimationFrame(mapAnimId);
term.innerHTML='';
deleteSave();
G.phase='title';
showTitle();
}
function showTitle(){
if(G.timer)clearInterval(G.timer);
updateUI();
TITLE_ART.forEach((l,i)=>{
setTimeout(()=>addLine(l,'highlight'),i*100);
});
G.unlockedCmds=['start','help','load'];
showStatus();showActions();
}
// ── INIT ──
try {
const hasSave=localStorage.getItem('signalelsewhere_save')!==null;
initGame();
showTitle();
if(hasSave&&G.phase==='title'){
addLine('','system');
addLine('Save file detected. Type LOAD to continue or START for new game.','info');
G.unlockedCmds.push('load');
}
overlay.addEventListener('click',e=>{if(e.target===overlay)hideOverlay();});
window.handleCommand=handleCommand;
window.startSpectrum=startSpectrum;
window.startMap=startMap;
const lm=document.getElementById('loadingMsg');
if(lm)lm.style.display='none';
} catch(e) {
const eb=document.getElementById('errorBox');
if(eb){eb.style.display='block';eb.textContent='ERROR: '+e.message+'\n'+e.stack;}
document.body.style.background='#100';
document.body.innerHTML='<div style="color:#f00;padding:20px;font:14px monospace">ERROR: '+e.message+'</div>';
}
</script>
</body>
</html>