/**
* Main Application Controller - 10 Sci-Fi Universes Architecture
* Coordinates Starfleet, Whoniverse, Industrial, Bioships, Retro Future, Military,
* Deep Space, Outlaw, Space Stations, and Comedy universes.
*/
document.addEventListener('DOMContentLoaded', () => {
// 1. Initialize Audio Subsystems
const audioManager = new AudioManager();
const hullDrone = new HullDroneSynth(audioManager);
const warpCore = new WarpCoreSynth(audioManager);
const lifeSupport = new LifeSupportSynth(audioManager);
const telemetry = new TelemetrySynth(audioManager);
const alerts = new AlertSynth(audioManager);
const whoniverseAudio = new WhoniverseAudioSynth(audioManager);
const expandedAudio = new ExpandedSciFiAudioSynth(audioManager);
const engineTransitions = new EngineTransitionSynth(audioManager);
window.expandedAudio = expandedAudio;
window.whoniverseAudio = whoniverseAudio;
window.engineTransitions = engineTransitions;
const visualizer = new StarshipVisualizer(audioManager, warpCore);
const observationEngine = new ObservationEngine(audioManager, warpCore, alerts, hullDrone, lifeSupport);
window.observationEngine = observationEngine;
let isPlaying = false;
let activeUniverseId = 'starfleet';
let activePresetId = 'tng-bridge';
window.activeUniverseId = activeUniverseId;
window.activePresetId = activePresetId;
// 2. DOM Elements
const headerUniverseBtn = document.getElementById('header-universe-btn');
const headerUniverseLabel = document.getElementById('header-universe-label');
const universeDropdown = document.getElementById('universe-dropdown');
const headerShipName = document.getElementById('header-ship-name');
const headerMatrixLabel = document.getElementById('header-matrix-label');
const sidebarTitle = document.getElementById('sidebar-title');
const presetContainer = document.getElementById('preset-container');
const currentPresetTitle = document.getElementById('current-preset-title');
const currentPresetDesc = document.getElementById('current-preset-desc');
const statAudioStatus = document.getElementById('stat-audio-status');
const statCoreFreq = document.getElementById('stat-core-freq');
const btnPlay = document.getElementById('btn-master-play');
const playIcon = document.getElementById('play-icon');
const playText = document.getElementById('play-text');
// Master Volume Console Elements
const sliderMasterVol = document.getElementById('slider-master-vol');
const valMasterVol = document.getElementById('val-master-vol');
const valMasterDb = document.getElementById('val-master-db');
const btnMasterMute = document.getElementById('btn-master-mute');
const btnHeaderMute = document.getElementById('btn-header-mute');
const headerVolVal = document.getElementById('header-vol-val');
const btnVolDown = document.getElementById('btn-vol-down');
const btnVolUp = document.getElementById('btn-vol-up');
const volMeterLeds = document.getElementById('vol-meter-leds');
const volStepButtons = document.querySelectorAll('.btn-vol-step');
// Channel Sliders
const sliderHullVol = document.getElementById('slider-hull-vol');
const valHullVol = document.getElementById('val-hull-vol');
const sliderHullFreq = document.getElementById('slider-hull-freq');
const valHullFreq = document.getElementById('val-hull-freq');
const sliderHullCutoff = document.getElementById('slider-hull-cutoff');
const valHullCutoff = document.getElementById('val-hull-cutoff');
const sliderWarpVol = document.getElementById('slider-warp-vol');
const valWarpVol = document.getElementById('val-warp-vol');
const sliderWarpBpm = document.getElementById('slider-warp-bpm');
const valWarpBpm = document.getElementById('val-warp-bpm');
const sliderWarpCarrier = document.getElementById('slider-warp-carrier');
const valWarpCarrier = document.getElementById('val-warp-carrier');
const sliderAirVol = document.getElementById('slider-air-vol');
const valAirVol = document.getElementById('val-air-vol');
const sliderAirCutoff = document.getElementById('slider-air-cutoff');
const valAirCutoff = document.getElementById('val-air-cutoff');
const sliderTelemetryVol = document.getElementById('slider-telemetry-vol');
const valTelemetryVol = document.getElementById('val-telemetry-vol');
const sliderTelemetryDensity = document.getElementById('slider-telemetry-density');
const valTelemetryDensity = document.getElementById('val-telemetry-density');
// Visualizer & Dynamic Containers
const coreVisualizerTitle = document.getElementById('core-visualizer-title');
const coreVisualizerLabel = document.getElementById('core-visualizer-label');
const soundboardTitle = document.getElementById('soundboard-title');
const universeEventsContainer = document.getElementById('universe-events-container');
const soundboardContainer = document.getElementById('soundboard-container');
// Sleep Timer
const timerDisplay = document.getElementById('timer-display');
const timerButtons = document.querySelectorAll('.btn-timer');
// 3. Universe Management & Switching
function initUniverseDropdown() {
universeDropdown.innerHTML = '';
Object.values(UniverseRegistry).forEach(u => {
const btn = document.createElement('button');
btn.className = `universe-item ${u.id === activeUniverseId ? 'active' : ''}`;
btn.innerHTML = `${u.icon} ${u.name}`;
btn.addEventListener('click', (e) => {
e.stopPropagation();
setUniverse(u.id);
});
universeDropdown.appendChild(btn);
});
headerUniverseBtn.addEventListener('click', (e) => {
e.stopPropagation();
universeDropdown.classList.toggle('show');
});
document.addEventListener('click', () => {
universeDropdown.classList.remove('show');
});
}
function setUniverse(universeId) {
const universe = UniverseRegistry[universeId];
if (!universe) return;
activeUniverseId = universeId;
universeDropdown.classList.remove('show');
// Update Dropdown UI active class
document.querySelectorAll('.universe-item').forEach(el => {
el.classList.toggle('active', el.textContent.includes(universe.name));
});
// Update Header and Theme Class
headerUniverseLabel.textContent = universe.shortCode;
document.body.className = universe.themeClass;
headerMatrixLabel.textContent = universe.headerTitle;
// Custom titles per universe
const titlesMap = {
'starfleet': {
sidebar: 'STARSHIP PRESETS',
visTitle: 'INTERMIX CASCADE',
visLabel: 'INTERMIX CHAMBER // ACTIVE',
soundboard: 'LCARS KEYPAD'
},
'whoniverse': {
sidebar: 'TARDIS CONSOLES',
visTitle: 'CENTRAL TIME ROTOR COLUMN',
visLabel: 'VORTEX ROTOR // OPERATIONAL',
soundboard: 'TARDIS CONSOLE CONTROLS'
},
'industrial': {
sidebar: 'INDUSTRIAL FREIGHTERS',
visTitle: 'COMPRESSION FURNACE',
visLabel: 'CONTAINMENT CORE // ACTIVE',
soundboard: 'INDUSTRIAL INSTRUMENTATION'
},
'bioships': {
sidebar: 'SENTIENT LEVIATHANS',
visTitle: 'ORGANIC BIO-HEART SAC',
visLabel: 'VASCULAR NEXUS // PULSING',
soundboard: 'NEURAL LINK SENSORS'
},
'retrofuture': {
sidebar: 'GOLDEN AGE EXPLORATION',
visTitle: 'ASTROGATOR OSCILLOSCOPE',
visLabel: 'ANALOG BEAM // ROTATING',
soundboard: 'RETRO CONSOLE SWITCHES'
},
'military': {
sidebar: 'BATTLEFLEET WARSHIPS',
visTitle: 'TACTICAL FLYWHEEL',
visLabel: 'CONDITION ONE // ARMED',
soundboard: 'CIC TACTICAL KEYPAD'
},
'deepspace': {
sidebar: 'DEEP EXPEDITION ARKS',
visTitle: 'GRAVITY SINGULARITY CORE',
visLabel: 'EVENT HORIZON // LOCKED',
soundboard: 'VOID ACOUSTIC CONSOLE'
},
'outlaw': {
sidebar: 'FRONTIER & SALVAGE SHIPS',
visTitle: 'PATCHWORK IMPULSE DRIVE',
visLabel: 'KINETIC THRUSTERS // PRIMED',
soundboard: 'OUTLAW COCKPIT CONTROLS'
},
'spacestations': {
sidebar: 'ORBITAL STATIONS & DOCKS',
visTitle: 'CENTRIFUGAL DISTRIBUTION HUB',
visLabel: 'STATION ROTOR // DOCKED',
soundboard: 'STATION OPS CONSOLE'
},
'comedy': {
sidebar: 'WHIMSICAL & ADVENTURE FLEET',
visTitle: 'IMPROBABILITY ENGINE',
visLabel: 'REALITY FIELD // SHIFTING',
soundboard: 'CHEERFUL CONSOLE KEYPAD'
}
};
const conf = titlesMap[universeId] || titlesMap['starfleet'];
sidebarTitle.textContent = conf.sidebar;
coreVisualizerTitle.textContent = conf.visTitle;
coreVisualizerLabel.textContent = conf.visLabel;
soundboardTitle.textContent = conf.soundboard;
// Configure Visualizer mode
visualizer.setMode(universe.visualizer || 'warp-core');
// Render Event Buttons & Soundboard
renderUniverseEvents(universe);
renderUniverseSoundboard(universe);
// Render Presets
renderUniversePresets(universe);
// Select Default Preset
const firstPresetId = Object.keys(universe.presets)[0];
if (firstPresetId) {
selectPreset(firstPresetId);
}
if (observationActive && typeof refreshObservation === 'function') {
refreshObservation();
}
}
function renderUniverseEvents(universe) {
universeEventsContainer.innerHTML = '';
universe.events.forEach(evt => {
const btn = document.createElement('button');
btn.className = 'btn-lcars-pill';
btn.id = evt.id;
btn.textContent = evt.label;
btn.addEventListener('click', async () => {
await audioManager.resume();
handleUniverseEvent(evt.type, btn);
});
universeEventsContainer.appendChild(btn);
});
}
function renderUniverseSoundboard(universe) {
soundboardContainer.innerHTML = '';
universe.soundboard.forEach(key => {
const btn = document.createElement('button');
btn.className = 'btn-chirp';
btn.id = key.id;
btn.textContent = key.label;
if (key.span) {
btn.style.gridColumn = `span ${key.span}`;
}
btn.addEventListener('click', async () => {
await audioManager.resume();
handleSoundboardTrigger(key.id);
});
soundboardContainer.appendChild(btn);
});
}
function renderUniversePresets(universe) {
presetContainer.innerHTML = '';
Object.values(universe.presets).forEach(preset => {
const btn = document.createElement('button');
btn.className = `preset-btn ${preset.id === activePresetId ? 'active' : ''}`;
btn.setAttribute('data-preset-id', preset.id);
btn.innerHTML = `
${preset.name}
${(preset.era || 'SHIP').toUpperCase()} // ${preset.warp.pulseShape.toUpperCase()} CORE
`;
btn.addEventListener('click', () => {
selectPreset(preset.id);
});
presetContainer.appendChild(btn);
});
}
// 4a. Phase 2 Preset Ambience
// Single funnel that stops every automated loop (legacy + registry) and then
// re-engages the ambience matching the currently selected preset.
function startPresetAmbience(presetId) {
expandedAudio.stopAllLoops();
whoniverseAudio.stopAmbientLoops();
// Legacy Phase 1 loops, kept on their original presets
if (presetId === 'sta-sevastopol') {
expandedAudio.startStationSparks(4000, 11000);
} else if (presetId === 'sta-gateway' || presetId.includes('sickbay') || presetId.includes('med')) {
expandedAudio.startMedicalMonitor(1.15);
}
// Phase 2 audio_gaps.md rows #25-#45
if (presetId === 'bio-vorlon') expandedAudio.startVorlonSong(); // #26
if (presetId === 'bio-wraith') expandedAudio.startHullRegen(); // #28
if (presetId === 'deep-event-horizon' || presetId === 'deep-icarus-ii') expandedAudio.startHullStrainMoans(); // #30
// Phase 3 (audio_gaps.md #46-#54)
if (presetId === 'sta-babylon-5') expandedAudio.startZocaloChatter(); // #46
if (presetId === 'ind-nostromo') expandedAudio.startTeletypeChatter(); // #51
if (presetId === 'ret-jupiter-2') expandedAudio.startHeterodyneDrone(); // #54
if (presetId === 'deep-icarus-ii') expandedAudio.startIcarusBeacon(); // #31
if (presetId === 'deep-event-horizon' || presetId === 'deep-nightflyer') expandedAudio.startVoidWhispers(); // #32
if (presetId === 'com-protector') expandedAudio.startBerylliumThrum(); // #33
if (presetId === 'out-marauder') expandedAudio.startRepulsorliftDrone(); // #35
if (presetId === 'out-bebop') expandedAudio.startBebopChug(); // #36
if (presetId === 'mil-agamemnon') expandedAudio.startCarouselGroan(); // #37
if (presetId === 'mil-viper') expandedAudio.startOxygenRegulator(); // #39
if (presetId === 'ind-nostromo') expandedAudio.startPipeDrips(); // #40
if (presetId === 'ind-serenity' || presetId === 'ind-starbug') expandedAudio.startSteamVent(); // #41
if (presetId === 'ret-discovery-one') expandedAudio.startHalBreathing(); // #43
if (presetId === 'ret-cygnus') expandedAudio.startCygnusChug(); // #45
}
// 4. Select & Apply Preset
function selectPreset(presetId) {
const universe = UniverseRegistry[activeUniverseId];
if (!universe) return;
const preset = universe.presets[presetId];
if (!preset) return;
activePresetId = presetId;
window.activePresetId = presetId;
document.querySelectorAll('.preset-btn').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-preset-id') === presetId);
});
headerShipName.textContent = preset.name.toUpperCase();
currentPresetTitle.textContent = preset.name.toUpperCase();
currentPresetDesc.textContent = preset.description;
statCoreFreq.textContent = `${preset.warp.carrierFreq.toFixed(1)} Hz`;
hullDrone.applyPreset(preset.hull);
warpCore.applyPreset(preset.warp);
lifeSupport.applyPreset(preset.lifeSupport);
telemetry.applyPreset(preset.telemetry);
syncSlidersToSynthParams();
expandedAudio.stopAllLoops();
whoniverseAudio.stopAmbientLoops();
if (isPlaying) {
startPresetAmbience(presetId);
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeDematSwitch();
else if (activeUniverseId === 'industrial') expandedAudio.synthesizeDockingClamp();
else telemetry.synthesizeLCARSSingleChirp();
}
if (observationActive && typeof refreshObservation === 'function') {
refreshObservation();
}
}
function syncSlidersToSynthParams() {
sliderHullVol.value = hullDrone.params.volume;
valHullVol.textContent = `${Math.round(hullDrone.params.volume * 100)}%`;
sliderHullFreq.value = hullDrone.params.baseFreq;
valHullFreq.textContent = `${hullDrone.params.baseFreq} Hz`;
sliderHullCutoff.value = hullDrone.params.filterCutoff;
valHullCutoff.textContent = `${hullDrone.params.filterCutoff} Hz`;
sliderWarpVol.value = warpCore.params.volume;
valWarpVol.textContent = `${Math.round(warpCore.params.volume * 100)}%`;
sliderWarpBpm.value = warpCore.params.bpm;
valWarpBpm.textContent = `${warpCore.params.bpm} BPM`;
sliderWarpCarrier.value = warpCore.params.carrierFreq;
valWarpCarrier.textContent = `${warpCore.params.carrierFreq} Hz`;
sliderAirVol.value = lifeSupport.params.volume;
valAirVol.textContent = `${Math.round(lifeSupport.params.volume * 100)}%`;
sliderAirCutoff.value = lifeSupport.params.lowpassFreq;
valAirCutoff.textContent = `${lifeSupport.params.lowpassFreq} Hz`;
sliderTelemetryVol.value = telemetry.params.volume;
valTelemetryVol.textContent = `${Math.round(telemetry.params.volume * 100)}%`;
sliderTelemetryDensity.value = telemetry.params.density;
valTelemetryDensity.textContent = `${Math.round(telemetry.params.density * 100)}%`;
}
// 5. Play / Pause Master Toggle
let isTransitioning = false;
const ENGINE_TRANSITION_SECS = 2.5;
async function togglePlay() {
if (isTransitioning) return;
const engineProfile = typeof getEngineProfileForPreset === 'function'
? getEngineProfileForPreset(activePresetId, activeUniverseId)
: 'galaxy';
if (!isPlaying) {
isTransitioning = true;
await audioManager.resume();
// Flashing state while engaging
btnPlay.classList.add('engaging');
btnPlay.classList.remove('playing', 'stopping');
statAudioStatus.textContent = 'ENGAGING...';
statAudioStatus.style.color = '#ffcc00';
// Layered & Staged Procedural Startup Script for this Profile
engineTransitions.playStartup(engineProfile, ENGINE_TRANSITION_SECS);
// Acoustic chime / chirp feedback
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeTelepathicChime();
else if (activeUniverseId === 'bioships') expandedAudio.synthesizeStarburst();
else telemetry.synthesizeLCARSDoubleChirp();
// Start engine sounds with smooth spool-up ramp
hullDrone.start(ENGINE_TRANSITION_SECS);
warpCore.start(ENGINE_TRANSITION_SECS);
lifeSupport.start(ENGINE_TRANSITION_SECS);
telemetry.start();
setTimeout(() => {
isPlaying = true;
isTransitioning = false;
btnPlay.classList.remove('engaging');
btnPlay.classList.add('playing');
playIcon.textContent = '■';
playText.textContent = 'FULL STOP';
statAudioStatus.textContent = 'ONLINE';
statAudioStatus.style.color = '#00ff88';
if (activePresetId) {
startPresetAmbience(activePresetId);
}
}, ENGINE_TRANSITION_SECS * 1000);
} else {
isTransitioning = true;
// Flashing state while stopping
btnPlay.classList.remove('playing', 'engaging');
btnPlay.classList.add('stopping');
statAudioStatus.textContent = 'STOPPING...';
statAudioStatus.style.color = '#ff9900';
// Layered & Staged Procedural Shutdown Script for this Profile
engineTransitions.playShutdown(engineProfile, ENGINE_TRANSITION_SECS);
if (activeUniverseId === 'whoniverse') whoniverseAudio.synthesizeLeverClunk();
else telemetry.synthesizeLCARSDoubleChirp();
// Stop engine sounds with smooth spool-down fade
hullDrone.stop(ENGINE_TRANSITION_SECS);
warpCore.stop(ENGINE_TRANSITION_SECS);
lifeSupport.stop(ENGINE_TRANSITION_SECS);
telemetry.stop();
expandedAudio.stopAllLoops();
whoniverseAudio.stopAmbientLoops();
alerts.stopAlert();
whoniverseAudio.stopCloisterBell();
resetAlertButtons();
setTimeout(() => {
isPlaying = false;
isTransitioning = false;
btnPlay.classList.remove('stopping');
playIcon.textContent = '▶';
playText.textContent = 'ENGAGE';
statAudioStatus.textContent = 'STANDBY';
statAudioStatus.style.color = '#ffcc00';
}, ENGINE_TRANSITION_SECS * 1000);
}
}
btnPlay.addEventListener('click', togglePlay);
// 6. Master Volume Control System
function updateMasterVolumeUI(val, isMuted = false) {
sliderMasterVol.value = val;
const pct = Math.round(val * 100);
valMasterVol.textContent = isMuted ? 'MUTED' : `${pct}%`;
headerVolVal.textContent = isMuted ? 'MUTE' : `${pct}%`;
if (isMuted || val <= 0.001) {
valMasterDb.textContent = '-∞ dB';
} else {
const db = 20 * Math.log10(val);
valMasterDb.textContent = `${db >= 0 ? '+' : ''}${db.toFixed(1)} dB`;
}
btnMasterMute.classList.toggle('muted', isMuted);
btnHeaderMute.classList.toggle('muted', isMuted);
btnMasterMute.textContent = isMuted ? 'UNMUTE' : 'MUTE';
btnHeaderMute.textContent = isMuted ? 'MUTED' : 'MUTE';
if (volMeterLeds) {
const pips = volMeterLeds.querySelectorAll('.led-pip');
const activeCount = isMuted ? 0 : Math.round(val * pips.length);
pips.forEach((pip, idx) => {
pip.classList.toggle('active', idx < activeCount);
});
}
volStepButtons.forEach(btn => {
const stepVal = parseFloat(btn.getAttribute('data-vol'));
btn.classList.toggle('active', !isMuted && Math.abs(stepVal - val) < 0.04);
});
}
function setMasterVolume(val) {
const clamped = Math.max(0, Math.min(1, val));
if (audioManager.isMuted) audioManager.setMute(false);
audioManager.setMasterVolume(clamped);
updateMasterVolumeUI(clamped, false);
}
function toggleMute() {
const isMuted = audioManager.toggleMute();
updateMasterVolumeUI(audioManager.getMasterVolume(), isMuted);
}
sliderMasterVol.addEventListener('input', (e) => setMasterVolume(parseFloat(e.target.value)));
btnMasterMute.addEventListener('click', toggleMute);
btnHeaderMute.addEventListener('click', toggleMute);
btnVolDown.addEventListener('click', () => setMasterVolume(audioManager.getMasterVolume() - 0.05));
btnVolUp.addEventListener('click', () => setMasterVolume(audioManager.getMasterVolume() + 0.05));
volStepButtons.forEach(btn => {
btn.addEventListener('click', () => {
const stepVal = parseFloat(btn.getAttribute('data-vol'));
setMasterVolume(stepVal);
});
});
// 7. Channel Synthesizer Sliders
sliderHullVol.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
hullDrone.setVolume(val);
valHullVol.textContent = `${Math.round(val * 100)}%`;
});
sliderHullFreq.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
hullDrone.setBaseFreq(val);
valHullFreq.textContent = `${val} Hz`;
});
sliderHullCutoff.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
hullDrone.setFilterCutoff(val);
valHullCutoff.textContent = `${val} Hz`;
});
sliderWarpVol.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
warpCore.setVolume(val);
valWarpVol.textContent = `${Math.round(val * 100)}%`;
});
sliderWarpBpm.addEventListener('input', (e) => {
const val = parseInt(e.target.value);
warpCore.setBpm(val);
valWarpBpm.textContent = `${val} BPM`;
});
sliderWarpCarrier.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
warpCore.setCarrierFreq(val);
valWarpCarrier.textContent = `${val} Hz`;
statCoreFreq.textContent = `${val.toFixed(1)} Hz`;
});
sliderAirVol.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
lifeSupport.setVolume(val);
valAirVol.textContent = `${Math.round(val * 100)}%`;
});
sliderAirCutoff.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
lifeSupport.setFilterCutoff(val);
valAirCutoff.textContent = `${val} Hz`;
});
sliderTelemetryVol.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
telemetry.setVolume(val);
valTelemetryVol.textContent = `${Math.round(val * 100)}%`;
});
sliderTelemetryDensity.addEventListener('input', (e) => {
const val = parseFloat(e.target.value);
telemetry.setDensity(val);
valTelemetryDensity.textContent = `${Math.round(val * 100)}%`;
});
// 8. Universe Event Handlers
function resetAlertButtons() {
document.querySelectorAll('.btn-lcars-pill').forEach(b => b.classList.remove('active-alert'));
}
function handleUniverseEvent(type, btnElement) {
switch (type) {
case 'warp':
case 'quantum':
alerts.synthesizeWarpJump();
visualizer.spawnWarpPulses();
break;
case 'alert-red':
case 'action-stations':
case 'scramble':
if (alerts.activeAlert === 'red') {
alerts.stopAlert();
resetAlertButtons();
} else {
resetAlertButtons();
btnElement.classList.add('active-alert');
alerts.triggerRedAlert('tng');
}
break;
case 'alert-yellow':
if (alerts.activeAlert === 'yellow') {
alerts.stopAlert();
resetAlertButtons();
} else {
resetAlertButtons();
btnElement.classList.add('active-alert');
alerts.triggerYellowAlert();
}
break;
case 'demat':
whoniverseAudio.synthesizeDematCycle(4);
visualizer.spawnWarpPulses();
break;
case 'vortex':
whoniverseAudio.synthesizeDematCycle(1);
visualizer.spawnWarpPulses();
break;
case 'cloister':
if (whoniverseAudio.activeCloister) {
whoniverseAudio.stopCloisterBell();
resetAlertButtons();
} else {
resetAlertButtons();
btnElement.classList.add('active-alert');
whoniverseAudio.triggerCloisterBell();
}
break;
case 'epstein':
case 'subdrive':
expandedAudio.synthesizeEpsteinBurn();
visualizer.spawnWarpPulses();
break;
case 'purge':
case 'decompress':
case 'docking':
expandedAudio.synthesizeDockingClamp();
break;
case 'vent':
whoniverseAudio.synthesizeFastReturn();
break;
case 'starburst':
case 'biodefense':
expandedAudio.synthesizeStarburst();
visualizer.spawnWarpPulses();
break;
case 'neural':
expandedAudio.synthesizeNeuralBondSwell();
visualizer.spawnWarpPulses();
break;
case 'centrifuge':
case 'astrogator':
expandedAudio.synthesizeRetroAstrogator();
break;
case 'hal':
expandedAudio.synthesizeHalChime();
break;
case 'ftl-jump':
expandedAudio.synthesizeFtlJump();
visualizer.spawnWarpPulses();
break;
case 'flak':
expandedAudio.synthesizeFlakBarrageBurst();
break;
case 'singularity':
case 'gravity':
case 'rotation':
expandedAudio.synthesizeSingularityEngage();
visualizer.spawnWarpPulses();
break;
case 'solar':
expandedAudio.synthesizeSolarRoar();
visualizer.spawnWarpPulses();
break;
case 'afterburner':
expandedAudio.synthesizeAfterburner();
visualizer.spawnWarpPulses();
break;
case 'ludicrous':
expandedAudio.synthesizeLudicrousSpeed();
visualizer.spawnWarpPulses();
break;
case 'improbability':
expandedAudio.synthesizeImprobabilityFlip();
visualizer.spawnWarpPulses();
break;
default:
telemetry.synthesizeLCARSSingleChirp();
break;
}
}
function handleSoundboardTrigger(id) {
const universe = UniverseRegistry[activeUniverseId];
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
const era = preset ? preset.era : 'tng';
switch (id) {
// Star Trek
case 'btn-comm-badge':
expandedAudio.synthesizeCommBadge();
break;
case 'btn-door-swish':
expandedAudio.synthesizeDoorSwish(era);
break;
case 'btn-bosun-whistle':
expandedAudio.synthesizeBosunWhistle();
break;
case 'btn-medical-monitor':
expandedAudio.synthesizeMedicalMonitor();
break;
case 'btn-chirp-single':
case 'btn-switch-pop':
telemetry.synthesizeLCARSSingleChirp();
break;
case 'btn-chirp-double':
telemetry.synthesizeLCARSDoubleChirp();
break;
case 'btn-chirp-ack':
telemetry.synthesizeLCARSSequence();
break;
case 'btn-chirp-sweep':
telemetry.synthesizeSensorSweep();
break;
case 'btn-chirp-door':
telemetry.synthesizeDoorChime();
break;
// Whoniverse
case 'btn-sonic':
whoniverseAudio.synthesizeSonicScrewdriver();
break;
case 'btn-lever-clunk':
whoniverseAudio.synthesizeLeverClunk();
break;
case 'btn-tardis-door':
whoniverseAudio.synthesizeTardisDoor();
break;
case 'btn-fast-return':
whoniverseAudio.synthesizeFastReturn();
break;
case 'btn-demat-switch':
case 'btn-relay-click':
whoniverseAudio.synthesizeDematSwitch();
break;
case 'btn-telepathic':
case 'btn-telepathic-chime':
whoniverseAudio.synthesizeTelepathicChime();
break;
case 'btn-scanner':
whoniverseAudio.synthesizeDematSwitch();
telemetry.synthesizeSensorSweep();
break;
// Space Stations
case 'btn-air-handler':
expandedAudio.synthesizeAirHandlerThud();
break;
case 'btn-mag-boot':
expandedAudio.synthesizeMagBootLatch();
break;
case 'btn-commlock':
expandedAudio.synthesizeCommlockTone();
break;
case 'btn-sevas-spark':
expandedAudio.synthesizeSevastopolSpark();
break;
case 'btn-station-ecg':
expandedAudio.synthesizeStationMedicalPing();
break;
case 'btn-tram-gong':
whoniverseAudio.synthesizeCloisterStrike();
break;
// Deep Space
case 'btn-solar-roar':
expandedAudio.synthesizeSolarRoar();
break;
case 'btn-cryo-sigh':
expandedAudio.synthesizeCryoDepressurize();
break;
case 'btn-orion-thump':
expandedAudio.synthesizeOrionPulseThump();
break;
// Outlaw & Frontier
case 'btn-tape-clunk':
expandedAudio.synthesizeCassetteTransport();
break;
case 'btn-grappler-servo':
expandedAudio.synthesizeGrapplerServo();
break;
case 'btn-radio-static':
expandedAudio.synthesizeRadioStatic();
break;
case 'btn-afterburner-snd':
expandedAudio.synthesizeAfterburner();
break;
// Comedy & Adventure
case 'btn-cheerful-door':
expandedAudio.synthesizeCheerfulDoor();
break;
case 'btn-tea-machine':
expandedAudio.synthesizeTeaDispenser();
break;
case 'btn-kettle-whistle':
expandedAudio.synthesizeKettleWhistle();
break;
case 'btn-plaid-alarm':
expandedAudio.synthesizePlaidAlarm();
break;
case 'btn-beryllium-pulse':
expandedAudio.synthesizeImprobabilityFlip();
break;
// General / Industrial / Military
case 'btn-geiger':
expandedAudio.synthesizeGeigerBurst();
break;
case 'btn-dock-clamp':
case 'btn-pneumatic-door':
expandedAudio.synthesizeDockingClamp();
break;
case 'btn-dradis-ping':
expandedAudio.synthesizeDradisPing();
break;
case 'btn-hal-chime':
expandedAudio.synthesizeHalChime();
break;
// Phase 2 (audio_gaps.md #21-#45)
case 'btn-cardassian-door':
expandedAudio.synthesizeCardassianDoor();
break;
case 'btn-turbolift':
expandedAudio.synthesizeTurboliftWhoosh();
break;
case 'btn-replicator':
expandedAudio.synthesizeReplicatorShimmer();
break;
case 'btn-scanner-activate':
whoniverseAudio.synthesizeScannerActivate();
break;
case 'btn-type-clatter':
whoniverseAudio.synthesizeTypeClatter();
break;
case 'btn-vorlon-song':
expandedAudio.synthesizeVorlonSingingSwell();
break;
case 'btn-neural-bond':
expandedAudio.synthesizeNeuralBondSwell();
break;
case 'btn-hull-regen':
expandedAudio.synthesizeHullRegenBurst();
break;
case 'btn-belter-telemetry':
expandedAudio.synthesizeBelterTelemetryJitter();
break;
case 'btn-hull-strain':
expandedAudio.synthesizeHullStrainMoan();
break;
case 'btn-icarus-beacon':
expandedAudio.synthesizeIcarusBeacon();
break;
case 'btn-void-whisper':
expandedAudio.synthesizeVoidWhisper();
break;
case 'btn-beryllium-thrum':
expandedAudio.synthesizeBerylliumThrumSwell();
break;
case 'btn-omega-13':
expandedAudio.synthesizeOmega13Whine();
break;
case 'btn-repulsor-drone':
expandedAudio.synthesizeRepulsorliftDrone();
break;
case 'btn-bebop-chug':
expandedAudio.synthesizeBebopChugStroke();
break;
case 'btn-carousel-groan':
expandedAudio.synthesizeCarouselGroanCycle();
break;
case 'btn-slipstream':
expandedAudio.synthesizeSlipstreamSurge();
break;
case 'btn-oxygen-reg':
expandedAudio.synthesizeOxygenRegulatorCycle();
break;
case 'btn-pipe-drips':
expandedAudio.synthesizePipeDrip();
break;
case 'btn-steam-vent':
expandedAudio.synthesizeSteamVentSwell();
break;
case 'btn-crash-couch':
expandedAudio.synthesizeCrashCouchGimbal();
break;
case 'btn-hal-breath':
expandedAudio.synthesizeHalBreathCycle();
break;
case 'btn-death-blossom':
expandedAudio.synthesizeDeathBlossomSurge();
break;
case 'btn-cygnus-chug':
expandedAudio.synthesizeCygnusPistonChug();
break;
// Phase 3 (audio_gaps.md #48-#53)
case 'btn-transporter':
expandedAudio.synthesizeTransporterCycle();
break;
case 'btn-breech-clank':
expandedAudio.synthesizeBreechClank();
break;
case 'btn-dock-groan':
expandedAudio.synthesizeDockingGroan();
break;
case 'btn-tape-spooler':
expandedAudio.synthesizeTapeSpooler();
break;
case 'btn-teletype':
expandedAudio.synthesizeTeletypeChatterPhrase();
break;
case 'btn-zocalo-chatter':
if (expandedAudio.zocaloChatterActive) expandedAudio.stopZocaloChatter();
else expandedAudio.startZocaloChatter();
break;
case 'btn-hetero-drone':
if (expandedAudio.heterodyneDroneActive) expandedAudio.stopHeterodyneDrone();
else expandedAudio.startHeterodyneDrone();
break;
default:
telemetry.synthesizeLCARSSingleChirp();
break;
}
}
// 9. Sleep Timer
timerButtons.forEach(btn => {
btn.addEventListener('click', () => {
const minutes = parseInt(btn.getAttribute('data-minutes'));
if (minutes === 0) {
audioManager.stopSleepTimer();
timerDisplay.textContent = 'TIMER INACTIVE (CONTINUOUS)';
} else {
audioManager.startSleepTimer(
minutes,
(secondsLeft) => {
const m = Math.floor(secondsLeft / 60);
const s = secondsLeft % 60;
timerDisplay.textContent = `SLEEP TIMER: ${m}:${s < 10 ? '0' : ''}${s}`;
},
() => {
timerDisplay.textContent = 'SLEEP TIMER COMPLETED';
if (isPlaying) togglePlay();
}
);
}
});
});
// =========================================================================
// OBSERVATION MODE
// One passive full-screen view with theme-specific inline SVG displays.
// Deliberately exits only on an actual click/tap (or Escape).
// =========================================================================
const btnObservation = document.getElementById('btn-observation');
const observationOverlay = document.getElementById('observation-overlay');
const observationStage = document.getElementById('observation-stage');
const observationSimLayer = document.getElementById('observation-sim-layer');
const observationUniverse = document.getElementById('observation-universe');
const observationProfile = document.getElementById('observation-profile');
const observationStatus = document.getElementById('observation-status');
const observationEventLayer = document.getElementById('observation-event-layer');
const sliderObservationActivity = document.getElementById('slider-observation-activity');
const valObservationActivity = document.getElementById('val-observation-activity');
const toggleObservationHud = document.getElementById('toggle-observation-hud');
const valObservationHud = document.getElementById('val-observation-hud');
const observationChrome = observationOverlay.querySelector('.observation-chrome');
const observationReturn = observationOverlay.querySelector('.observation-return');
let observationActive = false;
let observationActivity = 0.60;
window.observationActivity = observationActivity;
let observationAmbientTimer = null;
let observationTransientSerial = 0;
function obsClamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function obsRand(min, max) {
return min + Math.random() * (max - min);
}
function obsInt(min, max) {
return Math.floor(obsRand(min, max + 1));
}
function obsPick(items) {
return items[Math.floor(Math.random() * items.length)];
}
function obsHex(n, width = 3) {
return Math.floor(n).toString(16).toUpperCase().padStart(width, '0');
}
// =========================================================================
// PERSISTENT OBSERVATION SCENE ENGINE
// Original visual worlds, not franchise reproductions. Persistent entities
// use simple stateful motion rather than one-shot CSS decorations.
// =========================================================================
let observationSceneRaf = null;
let observationSceneLast = 0;
let observationSceneEntities = [];
let observationSceneBoostUntil = 0;
let observationSplineMorphs = [];
function obsEaseSmooth(t) {
return t * t * (3 - 2 * t);
}
function obsEaseInCubic(t) {
return t * t * t;
}
function obsEaseOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function obsBezierPoint(t, p0, p1, p2, p3) {
const u = 1 - t;
const tt = t * t;
const uu = u * u;
const uuu = uu * u;
const ttt = tt * t;
return {
x: uuu*p0.x + 3*uu*t*p1.x + 3*u*tt*p2.x + ttt*p3.x,
y: uuu*p0.y + 3*uu*t*p1.y + 3*u*tt*p2.y + ttt*p3.y
};
}
function obsBezierDerivative(t, p0, p1, p2, p3) {
const u = 1 - t;
return {
x: 3*u*u*(p1.x-p0.x) + 6*u*t*(p2.x-p1.x) + 3*t*t*(p3.x-p2.x),
y: 3*u*u*(p1.y-p0.y) + 6*u*t*(p2.y-p1.y) + 3*t*t*(p3.y-p2.y)
};
}
// =========================================================================
// LONG-LIVED SPLINE MORPH ENGINE
// Applies only to persistent pathwork in the main observation stage.
// The goal is subtle recalculation/breathing, not chaotic wriggling.
// =========================================================================
function obsThemeSplineScale(theme) {
return ({
starfleet: 0.60,
whoniverse: 1.05,
industrial: 0.48,
bioships: 1.28,
retrofuture: 0.72,
military: 0.44,
deepspace: 0.34,
outlaw: 0.86,
spacestations: 0.50,
comedy: 0.90
})[theme] || 0.65;
}
function obsTokenizePath(d) {
return (d.match(/[A-Za-z]|[-+]?(?:\d*\.\d+|\d+)(?:e[-+]?\d+)?/g) || []);
}
function obsParsePathSegments(d) {
const tokens = obsTokenizePath(d);
const segments = [];
let i = 0;
while (i < tokens.length) {
const cmd = tokens[i++];
if (!/^[A-Za-z]$/.test(cmd)) break;
const upper = cmd.toUpperCase();
let count = 0;
if (upper === 'M' || upper === 'L' || upper === 'T') count = 2;
else if (upper === 'Q' || upper === 'S') count = 4;
else if (upper === 'C') count = 6;
else if (upper === 'H' || upper === 'V') count = 1;
else if (upper === 'Z') count = 0;
else count = 0;
if (upper == 'Z') {
segments.push({ cmd, values: [] });
continue;
}
const values = [];
while (i < tokens.length && !/^[A-Za-z]$/.test(tokens[i])) {
values.push(Number(tokens[i++]));
}
if (!values.length) {
segments.push({ cmd, values: [] });
continue;
}
if (count == 0) {
segments.push({ cmd, values });
continue;
}
// Split repeated coordinate groups into separate segments. For repeated M,
// SVG treats the subsequent groups as implicit L commands.
for (let start = 0, segIdx = 0; start < values.length; start += count, segIdx++) {
const slice = values.slice(start, start + count);
if (slice.length < count) break;
const segCmd = (upper === 'M' && segIdx > 0) ? (cmd === upper ? 'L' : 'l') : cmd;
segments.push({ cmd: segCmd, values: slice });
}
}
return segments;
}
function obsBuildPathFromSegments(segments) {
return segments.map(seg => {
if (!seg.values || !seg.values.length) return seg.cmd;
const nums = seg.values.map(v => {
const n = Math.abs(v) < 1e-6 ? 0 : v;
return Number(n.toFixed(2)).toString();
}).join(' ');
return `${seg.cmd}${nums}`;
}).join(' ');
}
function obsCreateSplineMorphState(pathEl) {
if (!pathEl) return null;
const d = pathEl.getAttribute('d');
if (!d || !/[CQST]/i.test(d)) return null;
const segments = obsParsePathSegments(d);
if (!segments.length) return null;
const bbox = pathEl.getBBox();
const width = Math.max(80, bbox.width || 80);
const height = Math.max(80, bbox.height || 80);
const routeLike = pathEl.classList.contains('obs-route-wander');
const themeScale = obsThemeSplineScale(activeUniverseId);
const kindScale = routeLike ? 1.0 : 0.7;
let drawable = segments
.map((seg, idx) => ({ idx, upper: seg.cmd.toUpperCase(), count: seg.values.length }))
.filter(item => ['Q', 'T', 'S', 'C'].includes(item.upper));
const lastDrawableIdx = drawable.length ? drawable[drawable.length - 1].idx : -1;
const ampFractions = [];
segments.forEach((seg, segIdx) => {
const upper = seg.cmd.toUpperCase();
let frac = [];
if (upper === 'M' || upper === 'L') {
frac = new Array(seg.values.length).fill(0);
} else if (upper === 'C') {
frac = [1, 1, 1, 1, segIdx === lastDrawableIdx ? 0 : 0.18, segIdx === lastDrawableIdx ? 0 : 0.18];
} else if (upper === 'S') {
frac = [1, 1, segIdx === lastDrawableIdx ? 0 : 0.18, segIdx === lastDrawableIdx ? 0 : 0.18];
} else if (upper === 'Q') {
frac = [0.92, 0.92, segIdx === lastDrawableIdx ? 0 : 0.15, segIdx === lastDrawableIdx ? 0 : 0.15];
} else if (upper === 'T') {
frac = [segIdx === lastDrawableIdx ? 0 : 0.14, segIdx === lastDrawableIdx ? 0 : 0.14];
} else {
frac = new Array(seg.values.length).fill(0);
}
ampFractions.push(frac);
});
const offsets = segments.map(seg => new Array(seg.values.length).fill(0));
const targets = segments.map(seg => new Array(seg.values.length).fill(0));
function amplitudeFor(segIndex, valueIndex) {
const seg = segments[segIndex];
const frac = (ampFractions[segIndex] && ampFractions[segIndex][valueIndex]) || 0;
if (frac <= 0) return 0;
const upper = seg.cmd.toUpperCase();
const axis = upper === 'H' ? 'x' : upper === 'V' ? 'y' : (valueIndex % 2 === 0 ? 'x' : 'y');
const span = axis === 'x' ? width : height;
const baseFrac = routeLike ? 0.038 : 0.026;
return span * baseFrac * frac * kindScale * themeScale;
}
return {
el: pathEl,
baseD: d,
segments,
ampFractions,
offsets,
targets,
nextRetargetAt: performance.now() + obsRand(2200, 7800),
lastRetargetAt: performance.now(),
width,
height,
routeLike,
targetMultiplier: 1
};
}
function initializeObservationSplineMorphs() {
observationSplineMorphs = [];
if (!observationStage) return;
const paths = observationStage.querySelectorAll('path.obs-dashflow, path.obs-route-wander');
paths.forEach(pathEl => {
const state = obsCreateSplineMorphState(pathEl);
if (state) observationSplineMorphs.push(state);
});
}
function stopObservationSplineMorphs() {
observationSplineMorphs.forEach(state => {
if (state.el && state.el.isConnected) state.el.setAttribute('d', state.baseD);
});
observationSplineMorphs = [];
}
function retargetObservationSpline(state, now, forceScale = 1) {
const activity = observationActivity;
state.targetMultiplier = forceScale;
state.lastRetargetAt = now;
state.segments.forEach((seg, segIndex) => {
seg.values.forEach((_, valueIndex) => {
const amp = (function() {
const frac = (state.ampFractions[segIndex] && state.ampFractions[segIndex][valueIndex]) || 0;
if (frac <= 0) return 0;
const upper = seg.cmd.toUpperCase();
const axis = upper === 'H' ? 'x' : upper === 'V' ? 'y' : (valueIndex % 2 === 0 ? 'x' : 'y');
const span = axis === 'x' ? state.width : state.height;
const baseFrac = state.routeLike ? 0.038 : 0.026;
return span * baseFrac * frac * (state.routeLike ? 1.0 : 0.7) * obsThemeSplineScale(activeUniverseId) * forceScale;
})();
if (amp <= 0) {
state.targets[segIndex][valueIndex] = 0;
return;
}
const bias = state.routeLike ? 1.0 : 0.75;
state.targets[segIndex][valueIndex] = obsRand(-amp * bias, amp * bias);
});
});
const slow = state.routeLike ? 32000 : 42000;
const fast = state.routeLike ? 8500 : 14000;
const next = slow - (slow - fast) * Math.pow(activity, 0.82);
state.nextRetargetAt = now + next * obsRand(0.70, 1.35);
}
function updateObservationSplineMorphs(now, speedScale) {
if (!observationSplineMorphs.length) return;
const themeScale = obsThemeSplineScale(activeUniverseId);
observationSplineMorphs = observationSplineMorphs.filter(state => state.el && state.el.isConnected);
observationSplineMorphs.forEach(state => {
if (now >= state.nextRetargetAt) {
retargetObservationSpline(state, now, 1);
}
const follow = (0.012 + observationActivity * 0.030 + Math.max(0, speedScale - 0.4) * 0.006) * themeScale;
state.segments.forEach((seg, segIndex) => {
seg.values.forEach((baseValue, valueIndex) => {
const current = state.offsets[segIndex][valueIndex];
const target = state.targets[segIndex][valueIndex];
state.offsets[segIndex][valueIndex] = current + (target - current) * follow;
});
});
const morphed = state.segments.map((seg, segIndex) => ({
cmd: seg.cmd,
values: seg.values.map((baseValue, valueIndex) => baseValue + state.offsets[segIndex][valueIndex])
}));
state.el.setAttribute('d', obsBuildPathFromSegments(morphed));
});
}
function pulseObservationSplineMorphs() {
if (!observationActive || !observationSplineMorphs.length) return;
const candidates = observationSplineMorphs.filter(state => state.el && state.el.isConnected);
if (!candidates.length) return;
const count = observationActivity > 0.72 && Math.random() < observationActivity ? 2 : 1;
const shuffled = [...candidates].sort(() => Math.random() - 0.5);
for (let i = 0; i < Math.min(count, shuffled.length); i++) {
retargetObservationSpline(shuffled[i], performance.now(), 1.22 + observationActivity * 0.38);
}
}
function obsSceneShip(kind='scout', color='#dbeafe', accent='#38bdf8') {
if (kind === 'freighter') {
return `
`;
}
if (kind === 'shuttle') {
return `
`;
}
if (kind === 'tug') {
return `
`;
}
return `
`;
}
function obsSceneDefs() {
return `
`;
}
function obsFlightMarkup(id, shipMarkup, cfg) {
return `
${shipMarkup}
`;
}
function obsOrbitMarkup(id, body, cfg) {
return `
${body}
`;
}
function obsFloatMarkup(id, body, cfg) {
return `
${body}
`;
}
function buildObservationSceneMarkup(theme) {
const defs = obsSceneDefs();
if (theme === 'starfleet') {
return `${defs}
${obsFlightMarkup('sf-flight-1', obsSceneShip('scout','#e0f2fe','#38bdf8'), {
x0:410,y0:640,x1:520,y1:520,x2:880,y2:245,x3:1110,y3:330,duration:26000,delay:900,mode:'arrival',scale0:.35,scale1:.75,minActivity:.15
})}
${obsFlightMarkup('sf-flight-2', obsSceneShip('shuttle','#fde68a','#f97316'), {
x0:1190,y0:600,x1:1030,y1:500,x2:680,y2:420,x3:510,y3:260,duration:31000,delay:4600,mode:'cruise',scale0:.28,scale1:.55,minActivity:.35
})}
${obsOrbitMarkup('sf-orbit-1', ``, {
cx:800,cy:455,rx:245,ry:155,duration:24000,phase:.2,scale0:.65,scale1:1.05,minActivity:.05
})}
${obsOrbitMarkup('sf-orbit-2', ``, {
cx:800,cy:455,rx:165,ry:245,duration:39000,phase:.62,scale0:.5,scale1:.9,minActivity:.5
})}
`;
}
if (theme === 'whoniverse') {
const glyph = (char,color,size) => `${char}`;
return `${defs}
${obsOrbitMarkup('who-glyph-1', glyph('◎','#00e5ff',38), {cx:800,cy:455,rx:335,ry:160,duration:23000,phase:.1,scale0:.55,scale1:1.2,minActivity:.05})}
${obsOrbitMarkup('who-glyph-2', glyph('∆','#d4af37',30), {cx:800,cy:455,rx:255,ry:280,duration:31000,phase:.42,scale0:.5,scale1:1,minActivity:.2})}
${obsOrbitMarkup('who-glyph-3', glyph('∞','#00e5ff',28), {cx:800,cy:455,rx:420,ry:110,duration:39000,phase:.73,scale0:.4,scale1:1.15,minActivity:.45})}
${obsFloatMarkup('who-fragment-1', ``,
{cx:480,cy:520,ampX:95,ampY:75,duration:17000,phase:.35,minActivity:.25})}
${obsFloatMarkup('who-fragment-2', ``,
{cx:1110,cy:330,ampX:70,ampY:110,duration:21000,phase:.8,minActivity:.55})}`;
}
if (theme === 'industrial') {
// v13co: the sim layer sits ABOVE the stage (z-index 2 vs 1), so anything
// placed here draws over the camera console. Every entity is therefore
// parented to a clip matching one feed's picture area. The v12 entities
// (a wheeled cart, a crane hook, a fan and two steam blobs) were free
// floating in screen space, which is what made the smoke look detached
// and put a rolling "cart" across the cargo bay. They are replaced by
// motion that belongs to a specific feed.
const feedClip = (id, x, y) =>
``;
return `${defs}
${feedClip('indSimCam1', 290, 259)}
${feedClip('indSimCam2', 825, 259)}
${feedClip('indSimCam3', 290, 483)}
${feedClip('indSimCam4', 825, 483)}
${obsFloatMarkup('ind-bay-drone', `
`, {cx:532,cy:420,ampX:186,ampY:11,duration:31000,phase:.15,minActivity:.2,opacity:.66})}
${obsFloatMarkup('ind-ember-1', ``,
{cx:1035,cy:352,ampX:38,ampY:52,duration:13000,phase:.2,minActivity:.25,opacity:.6})}
${obsFloatMarkup('ind-ember-2', ``,
{cx:1092,cy:320,ampX:46,ampY:44,duration:17000,phase:.62,minActivity:.45,opacity:.5})}
${obsFloatMarkup('ind-ember-3', ``,
{cx:1160,cy:372,ampX:30,ampY:40,duration:21000,phase:.85,minActivity:.6,opacity:.42})}
${obsFloatMarkup('ind-lock-tell', `
`,
{cx:532,cy:640,ampX:0,ampY:13,duration:23000,phase:.3,minActivity:.5,opacity:.4})}
${obsFloatMarkup('ind-corridor-lamp', `
`,
{cx:1067,cy:600,ampX:118,ampY:26,duration:27000,phase:.4,minActivity:.3,opacity:.55})}
`;
}
if (theme === 'bioships') {
const particle = (color,r=5) => ``;
return `${defs}
${obsFlightMarkup('bio-flow-1', particle('#a7f3d0',5), {x0:300,y0:520,x1:520,y1:220,x2:760,y2:680,x3:1110,y3:350,duration:15000,delay:0,mode:'cruise',scale0:.7,scale1:1,minActivity:.05,opacity:.7})}
${obsFlightMarkup('bio-flow-2', particle('#10b981',4), {x0:1240,y0:610,x1:970,y1:430,x2:880,y2:720,x3:480,y3:390,duration:18000,delay:1600,mode:'cruise',scale0:.5,scale1:.9,minActivity:.15,opacity:.65})}
${obsFlightMarkup('bio-flow-3', particle('#8b5cf6',6), {x0:340,y0:290,x1:620,y1:570,x2:920,y2:180,x3:1300,y3:510,duration:22000,delay:4200,mode:'cruise',scale0:.55,scale1:1.05,minActivity:.35,opacity:.58})}
${obsOrbitMarkup('bio-node-1', ``, {cx:800,cy:470,rx:210,ry:125,duration:19000,phase:.3,scale0:.65,scale1:1.2,minActivity:.3})}
${obsFloatMarkup('bio-membrane', ``,
{cx:800,cy:470,ampX:26,ampY:18,duration:13000,phase:.1,minActivity:.55,opacity:.6})}`;
}
if (theme === 'retrofuture') {
const vectorShip = ``;
return `${defs}
${obsFlightMarkup('retro-v1', vectorShip, {x0:350,y0:610,x1:510,y1:430,x2:950,y2:330,x3:1210,y3:500,duration:25000,delay:0,mode:'cruise',scale0:.5,scale1:.8,minActivity:.1,opacity:.65})}
${obsFlightMarkup('retro-v2', vectorShip, {x0:1180,y0:260,x1:1010,y1:570,x2:610,y2:610,x3:430,y3:330,duration:33000,delay:4300,mode:'cruise',scale0:.35,scale1:.65,minActivity:.45,opacity:.52})}
${obsOrbitMarkup('retro-blip', ``, {cx:800,cy:460,rx:235,ry:235,duration:30000,phase:.15,scale0:.55,scale1:.95,minActivity:.05})}
${obsFloatMarkup('retro-liss', ``,
{cx:800,cy:460,ampX:45,ampY:25,duration:14000,phase:.42,minActivity:.7,opacity:.5})}`;
}
if (theme === 'military') {
const tri = ``;
return `${defs}
${obsFlightMarkup('mil-form', `
${tri}
${tri}
${tri}
${tri}
`, {x0:390,y0:620,x1:550,y1:410,x2:900,y2:290,x3:1160,y3:390,duration:28000,delay:800,mode:'cruise',scale0:.4,scale1:.7,minActivity:.15,opacity:.65})}
${obsFlightMarkup('mil-hostile', ``,
{x0:1190,y0:650,x1:1020,y1:520,x2:660,y2:430,x3:470,y3:240,duration:22000,delay:6200,mode:'arrival',scale0:.35,scale1:.6,minActivity:.45,opacity:.58})}
${obsOrbitMarkup('mil-cap', ``,
{cx:800,cy:470,rx:300,ry:185,duration:41000,phase:.5,scale0:.45,scale1:.75,minActivity:.65,opacity:.52})}`;
}
if (theme === 'deepspace') {
return `${defs}
${obsFlightMarkup('deep-ship', obsSceneShip('shuttle','#e0e7ff','#818cf8'), {
x0:230,y0:570,x1:520,y1:500,x2:900,y2:350,x3:1370,y3:300,duration:52000,delay:0,mode:'cruise',scale0:.12,scale1:.28,minActivity:.2,opacity:.6
})}
${obsFlightMarkup('deep-comet', ``, {
x0:180,y0:700,x1:480,y1:520,x2:930,y2:230,x3:1490,y3:150,duration:36000,delay:7500,mode:'cruise',scale0:.55,scale1:.9,minActivity:.5,opacity:.52
})}
${obsFloatMarkup('deep-planet', `
`, {cx:1200,cy:590,ampX:80,ampY:24,duration:95000,phase:.22,minActivity:.05,opacity:.55})}`;
}
if (theme === 'outlaw') {
return `${defs}
${obsFlightMarkup('out-runner', `
`, {x0:300,y0:610,x1:480,y1:430,x2:940,y2:300,x3:1290,y3:410,duration:31000,delay:1400,mode:'cruise',scale0:.35,scale1:.75,minActivity:.1,opacity:.62})}
${obsFlightMarkup('out-pursuer', obsSceneShip('scout','#f9a8d4','#8b5cf6'), {
x0:1280,y0:590,x1:1070,y1:520,x2:730,y2:520,x3:420,y3:310,duration:27000,delay:7900,mode:'arrival',scale0:.25,scale1:.52,minActivity:.5,opacity:.5
})}
${obsFloatMarkup('out-needle', `
`, {cx:1250,cy:535,ampX:8,ampY:4,duration:9000,phase:.2,minActivity:.25,opacity:.58})}`;
}
if (theme === 'spacestations') {
const clipOpen = ``;
const clipClose = ``;
return `${defs}${clipOpen}
${obsFlightMarkup('st-arrival', obsSceneShip('shuttle','#e0f2fe','#38bdf8'), {
x0:270,y0:520,x1:500,y1:455,x2:920,y2:380,x3:1195,y3:415,duration:36000,delay:0,mode:'arrival',scale0:.16,scale1:.62,minActivity:.05,opacity:.75
})}
${obsFlightMarkup('st-depart', obsSceneShip('tug','#bae6fd','#f97316'), {
x0:1180,y0:485,x1:1030,y1:430,x2:650,y2:310,x3:260,y3:255,duration:30000,delay:8400,mode:'depart',scale0:.58,scale1:.16,minActivity:.2,opacity:.68
})}
${obsFlightMarkup('st-freighter', obsSceneShip('freighter','#cbd5e1','#0ea5e9'), {
x0:185,y0:255,x1:530,y1:210,x2:920,y2:230,x3:1420,y3:300,duration:52000,delay:3800,mode:'cruise',scale0:.26,scale1:.42,minActivity:.4,opacity:.55
})}
${obsOrbitMarkup('st-hold', obsSceneShip('shuttle','#7dd3fc','#fdba74'), {
cx:800,cy:410,rx:290,ry:145,duration:43000,phase:.18,scale0:.18,scale1:.32,minActivity:.62,opacity:.5
})}
${obsFlightMarkup('st-nearpass', obsSceneShip('freighter','#f1f5f9','#38bdf8'), {
x0:170,y0:650,x1:470,y1:565,x2:950,y2:535,x3:1450,y3:600,duration:42000,delay:14500,mode:'cruise',scale0:.42,scale1:.78,minActivity:.78,opacity:.48
})}
${clipClose}`;
}
if (theme === 'comedy') {
return `${defs}
${obsFlightMarkup('com-tour', `
`, {x0:260,y0:600,x1:520,y1:255,x2:1030,y2:670,x3:1350,y3:330,duration:36000,delay:1200,mode:'cruise',scale0:.32,scale1:.62,minActivity:.08,opacity:.65})}
${obsOrbitMarkup('com-orbit', `?`,
{cx:800,cy:445,rx:330,ry:160,duration:33000,phase:.35,scale0:.65,scale1:1.15,minActivity:.45,opacity:.58})}
${obsFloatMarkup('com-cube', ``,
{cx:1120,cy:570,ampX:90,ampY:70,duration:18000,phase:.7,minActivity:.7,opacity:.52})}`;
}
return defs;
}
function registerObservationSceneEntities() {
observationSceneEntities = [];
if (!observationSimLayer) return;
observationSimLayer.querySelectorAll('[data-obs-kind]').forEach(el => {
const d = el.dataset;
const sceneNow = performance.now();
const common = {
el,
kind: d.obsKind,
minActivity: Number(d.minactivity || 0),
baseOpacity: Number(d.opacity || .7),
phase: Number(d.phase || 0),
startedAt: sceneNow,
inactiveUntil: sceneNow
};
if (d.obsKind === 'flight') {
const firstStart = sceneNow + Number(d.delay || 0);
observationSceneEntities.push({
...common,
startedAt: firstStart,
inactiveUntil: firstStart,
p0:{x:Number(d.x0),y:Number(d.y0)},
p1:{x:Number(d.x1),y:Number(d.y1)},
p2:{x:Number(d.x2),y:Number(d.y2)},
p3:{x:Number(d.x3),y:Number(d.y3)},
duration:Number(d.duration || 30000),
mode:d.mode || 'cruise',
scale0:Number(d.scale0 || 1),
scale1:Number(d.scale1 || 1),
cycle:0
});
} else if (d.obsKind === 'orbit') {
observationSceneEntities.push({
...common,
cx:Number(d.cx),cy:Number(d.cy),
rx:Number(d.rx),ry:Number(d.ry),
duration:Number(d.duration || 30000),
scale0:Number(d.scale0 || 1),
scale1:Number(d.scale1 || 1)
});
} else if (d.obsKind === 'float') {
observationSceneEntities.push({
...common,
cx:Number(d.cx),cy:Number(d.cy),
ampX:Number(d.ampx || 0),ampY:Number(d.ampy || 0),
duration:Number(d.duration || 20000)
});
}
});
}
function observationSceneResetFlight(entity, now) {
entity.cycle += 1;
const restBase = 5500 - observationActivity * 3500;
entity.inactiveUntil = now + obsRand(restBase * .35, restBase * 1.35);
entity.startedAt = entity.inactiveUntil;
}
function updateObservationSceneEntity(entity, now, speedScale) {
const visibleByActivity = observationActivity + .001 >= entity.minActivity;
if (!visibleByActivity) {
entity.el.style.opacity = '0';
return;
}
if (entity.kind === 'flight') {
if (now < entity.inactiveUntil) {
entity.el.style.opacity = '0';
return;
}
const elapsed = (now - entity.startedAt) * speedScale;
let raw = elapsed / entity.duration;
if (raw >= 1) {
observationSceneResetFlight(entity, now);
entity.el.style.opacity = '0';
return;
}
raw = obsClamp(raw, 0, 1);
let t = raw;
if (entity.mode === 'arrival') t = obsEaseOutCubic(raw);
else if (entity.mode === 'depart') t = obsEaseInCubic(raw);
else t = obsEaseSmooth(raw);
const p = obsBezierPoint(t, entity.p0, entity.p1, entity.p2, entity.p3);
const v = obsBezierDerivative(t, entity.p0, entity.p1, entity.p2, entity.p3);
const angle = Math.atan2(v.y, v.x) * 180 / Math.PI;
const scale = entity.scale0 + (entity.scale1 - entity.scale0) * t;
const edgeFade = Math.min(1, raw / .08, (1-raw) / .08);
const opacity = entity.baseOpacity * obsClamp(edgeFade,0,1);
entity.el.style.opacity = String(opacity);
entity.el.setAttribute('transform', `translate(${p.x.toFixed(2)} ${p.y.toFixed(2)}) rotate(${angle.toFixed(2)}) scale(${scale.toFixed(3)})`);
return;
}
if (entity.kind === 'orbit') {
const cycle = ((now - entity.startedAt) * speedScale / entity.duration + entity.phase) % 1;
const a = cycle * Math.PI * 2;
const x = entity.cx + Math.cos(a) * entity.rx;
const y = entity.cy + Math.sin(a) * entity.ry;
const depth = (Math.sin(a) + 1) / 2;
const scale = entity.scale0 + (entity.scale1 - entity.scale0) * depth;
entity.el.style.opacity = String(entity.baseOpacity * (.55 + depth * .45));
entity.el.setAttribute('transform', `translate(${x.toFixed(2)} ${y.toFixed(2)}) scale(${scale.toFixed(3)})`);
return;
}
if (entity.kind === 'float') {
const cycle = ((now - entity.startedAt) * speedScale / entity.duration + entity.phase) * Math.PI * 2;
const x = entity.cx + Math.sin(cycle) * entity.ampX;
const y = entity.cy + Math.cos(cycle * .73) * entity.ampY;
entity.el.style.opacity = String(entity.baseOpacity);
entity.el.setAttribute('transform', `translate(${x.toFixed(2)} ${y.toFixed(2)})`);
}
}
function observationSceneFrame(now) {
observationSceneRaf = null;
if (!observationActive || !observationSimLayer) return;
const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const baseSpeed = reduceMotion ? .08 : (.32 + observationActivity * 1.18);
const boost = now < observationSceneBoostUntil ? .38 : 0;
const speedScale = baseSpeed + boost;
for (const entity of observationSceneEntities) {
updateObservationSceneEntity(entity, now, speedScale);
}
updateObservationSplineMorphs(now, speedScale);
observationSceneLast = now;
observationSceneRaf = requestAnimationFrame(observationSceneFrame);
}
function startObservationScene() {
stopObservationScene();
if (!observationSimLayer) return;
observationSimLayer.innerHTML = ``;
registerObservationSceneEntities();
observationSceneLast = performance.now();
observationSceneRaf = requestAnimationFrame(observationSceneFrame);
}
function stopObservationScene() {
if (observationSceneRaf) {
cancelAnimationFrame(observationSceneRaf);
observationSceneRaf = null;
}
observationSceneEntities = [];
if (observationSimLayer) observationSimLayer.innerHTML = '';
}
function pulseObservationScene() {
if (!observationActive) return;
observationSceneBoostUntil = performance.now() + 1200 + observationActivity * 900;
const eligible = observationSceneEntities.filter(e => observationActivity + .001 >= e.minActivity);
if (!eligible.length) return;
const chosen = obsPick(eligible);
// Telemetry can wake a waiting scene object early without assigning the
// sound a literal fictional meaning.
if (chosen.kind === 'flight' && performance.now() < chosen.inactiveUntil && Math.random() < .55) {
const wakeNow = performance.now();
chosen.inactiveUntil = wakeNow;
chosen.startedAt = wakeNow;
}
}
function transientSvg(body) {
return ``;
}
function spawnObservationTransient(body, lifetime = 4200) {
if (!observationActive || !observationEventLayer) return;
const node = document.createElement('div');
node.className = 'obs-transient';
node.dataset.serial = String(++observationTransientSerial);
node.style.setProperty('--obs-life', `${lifetime}ms`);
node.innerHTML = transientSvg(body);
observationEventLayer.appendChild(node);
const maxTransient = 5 + Math.round(observationActivity * 8);
while (observationEventLayer.children.length > maxTransient) {
observationEventLayer.firstElementChild.remove();
}
window.setTimeout(() => node.remove(), lifetime + 300);
}
function obsTextCard(x, y, title, line, color = 'var(--primary-accent)', width = 290) {
return `
${title}
${line}
`;
}
function obsExhibitX(width = 290) {
const leftMargin = 240;
const rightMargin = 240;
return obsInt(leftMargin, 1600 - rightMargin - width);
}
function obsExhibitY(height = 82) {
const topMargin = 220;
const bottomMargin = 190;
return obsInt(topMargin, 900 - bottomMargin - height);
}
function obsExhibitCardPosition(width = 290, height = 82) {
return { x: obsExhibitX(width), y: obsExhibitY(height) };
}
function obsContact(x, y, color = 'var(--primary-accent)', label = 'CONTACT') {
const size = obsInt(26, 54);
return `
${label}
`;
}
function obsExpandingRing(x, y, color = 'var(--primary-accent)', radius = 70) {
return `
`;
}
function generateStarfleetActivity(power) {
const effect = obsPick(['contact','vector','data','ring','diagnostic','streak']);
if (effect === 'contact') {
const x = obsInt(420,1180), y = obsInt(230,690);
return spawnObservationTransient(
obsContact(x,y,'var(--primary-accent)',`CONTACT ${obsInt(2,99).toString().padStart(2,'0')}`) +
``,
4300
);
}
if (effect === 'vector') {
const y1=obsInt(260,680), y2=obsInt(220,690);
return spawnObservationTransient(`
`, 5200);
}
if (effect === 'data') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x, pos.y,
obsPick(['SENSOR UPDATE','NAV SOLUTION','SYSTEM QUERY','PASSIVE ARRAY']),
obsPick([
`BEARING ${obsInt(0,359).toString().padStart(3,'0')}.${obsInt(0,9)}°`,
`RANGE ${obsInt(2,980)}.${obsInt(0,9)} Mm`,
`SUBSPACE VAR ${obsRand(.01,.99).toFixed(3)}`,
`VECTOR ${obsInt(100,999)}-${obsInt(10,99)}`
])
), 4200);
}
if (effect === 'diagnostic') {
const bars = Array.from({length: obsInt(5,9)}, (_,i) =>
``).join('');
return spawnObservationTransient(`${bars}`, 3500);
}
if (effect === 'streak') {
const lines = Array.from({length: obsInt(8,16)}, () => {
const y=obsInt(160,760), x=obsInt(150,1300), len=obsInt(70,240);
return ``;
}).join('');
return spawnObservationTransient(`${lines}`, 3000);
}
return spawnObservationTransient(obsExpandingRing(obsInt(420,1180),obsInt(250,680),'var(--primary-accent)',obsInt(45,90)), 3200);
}
function generateWhoniverseActivity(power) {
const effect = obsPick(['echo','glyphs','coordinate','warp','rings']);
if (effect === 'echo') {
const x=obsInt(430,1170), y=obsInt(270,650);
return spawnObservationTransient(`
${[0,1,2].map(i=>`
`).join('')}
`, 4700);
}
if (effect === 'glyphs') {
const glyphs = Array.from({length: obsInt(4,9)}, (_,i) =>
`${obsPick(['◉','◎','⌁','∆','∴','∞','⊙','⊛'])}`
).join('');
return spawnObservationTransient(glyphs, 6500);
}
if (effect === 'coordinate') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x, pos.y, 'TEMPORAL COORDINATE',
`${obsInt(0,99)}:${obsInt(0,99)}:${obsInt(0,99)} ∴ ${obsInt(1,13)}.${obsInt(0,9)}`, '#00e5ff'
), 4300);
}
if (effect === 'warp') {
return spawnObservationTransient(`
`, 4400);
}
return spawnObservationTransient(obsExpandingRing(800,450,obsPick(['#00e5ff','#d4af37']),obsInt(60,120)), 3500);
}
function generateIndustrialActivity(power) {
// v13co: every industrial transient is clipped to ONE feed's picture area.
// In v12 these drew in raw screen space, so steam puffs and signal bars
// spilled across bezels and the console furniture.
const feeds = [
{ix:290, iy:259, id:'CAM 01', where:'CARGO BAY 2'},
{ix:825, iy:259, id:'CAM 02', where:'REACTOR ACCESS'},
{ix:290, iy:483, id:'CAM 03', where:'AIRLOCK C'},
{ix:825, iy:483, id:'CAM 04', where:'MACHINE CORRIDOR'}
];
const f = obsPick(feeds);
const W = 485, H = 189;
const clipId = `indFx-${Math.random().toString(36).slice(2, 9)}`;
const inFeed = (body) => `
${body}`;
const canCycle = (f.id === 'CAM 01' || f.id === 'CAM 03');
const effect = obsPick(['signal', 'dropout', 'vapour', 'motion', 'gain', 'warning']
.concat(canCycle ? ['door'] : []));
// Digital break-up: displaced scan blocks plus a recalibration caption.
if (effect === 'signal') {
const blocks = Array.from({length: obsInt(9, 15)}, () => {
const by = obsInt(0, H - 14);
return ``;
}).join('');
return spawnObservationTransient(inFeed(`
${blocks}
SIGNAL RECALIBRATION
`), 3600);
}
// Total feed loss, then reacquire.
if (effect === 'dropout') {
return spawnObservationTransient(inFeed(`
NO SIGNAL
${f.id} // REACQUIRING
`), 2600);
}
// A vent release inside the room the camera is actually looking at.
if (effect === 'vapour') {
const x = obsInt(90, W - 90), y = H - obsInt(16, 40);
const puffs = Array.from({length: obsInt(6, 11)}, (_, i) => {
const dur = obsRand(2.8, 5.2).toFixed(1);
return ``;
}).join('');
return spawnObservationTransient(inFeed(`
${puffs}
`), 6200);
}
// Motion detection bracket that tracks across the picture.
if (effect === 'motion') {
const bx = obsInt(40, W - 190), by = obsInt(46, H - 130);
return spawnObservationTransient(inFeed(`
MOTION // ${f.id}
`), 4600);
}
// Pressure-door cycle seen head on.
if (effect === 'door') {
const mid = W / 2, top = 58, bot = H - 52;
return spawnObservationTransient(inFeed(`
HATCH CYCLE // ${f.where}
`), 4800);
}
// Auto-iris hunting after a lighting change.
if (effect === 'gain') {
return spawnObservationTransient(inFeed(`
AGC // IRIS ADJUST
`), 2400);
}
// Plant readout card — stays on the console rail, not over a feed.
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x, pos.y,
obsPick(['PRESSURE TRANSIENT', 'COOLANT LOOP', 'MAINTENANCE BUS', 'LOCAL POWER', 'GANTRY INTERLOCK']),
obsPick([
`P-${obsInt(1, 9)} ${obsInt(30, 98)}%`,
`FLOW ${obsInt(120, 980)} L/M`,
`BUS ${obsHex(obsInt(100, 999))} NOMINAL`,
`VALVE ${obsInt(1, 44)} CYCLING`,
`HOIST ${obsInt(1, 4)} LOAD ${obsRand(.4, 4.2).toFixed(1)} T`
]),
'#ffaa00'
), 4500);
}
function generateBioshipActivity(power) {
const effect = obsPick(['neural','spores','ripple','tendril','organ','metric']);
if (effect === 'neural') {
const y1=obsInt(260,650), y2=obsInt(260,650);
return spawnObservationTransient(`
`, 3300);
}
if (effect === 'spores') {
const spores=Array.from({length:obsInt(10,24)},()=>`
`).join('');
return spawnObservationTransient(spores, 7000);
}
if (effect === 'tendril') {
const sx=obsPick([180,1420]), sy=obsInt(300,660);
return spawnObservationTransient(`
`, 5200);
}
if (effect === 'organ') {
return spawnObservationTransient(`
`, 5000);
}
if (effect === 'metric') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x, pos.y, obsPick(['NEURAL RESPONSE','METABOLIC FIELD','SYMBIOTIC VARIANCE','BIOELECTRIC FLUX']),
`${obsRand(0.1,9.9).toFixed(2)} // ${obsInt(20,99)}%`, '#10b981'
), 4300);
}
return spawnObservationTransient(obsExpandingRing(800,470,obsPick(['#10b981','#8b5cf6','#a7f3d0']),obsInt(55,110)), 3700);
}
function generateRetroActivity(power) {
const effect = obsPick(['blip','scope','counter','vector','bloom','reel']);
if (effect === 'blip') {
return spawnObservationTransient(
obsContact(obsInt(470,1130),obsInt(260,660),'#86efac',`OBJ ${obsInt(1,99)}`), 4300
);
}
if (effect === 'scope') {
const y=obsInt(560,720);
return spawnObservationTransient(`
`, 4500);
}
if (effect === 'counter') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x, pos.y, obsPick(['ASTROGATOR','VECTOR MEMORY','TAPE INDEX','OBJECT FILE']),
`${obsInt(0,9999).toString().padStart(4,'0')} / ${obsInt(0,9999).toString().padStart(4,'0')}`,'#22c55e'
),4300);
}
if (effect === 'reel') {
return spawnObservationTransient(`
`, 4800);
}
if (effect === 'bloom') {
return spawnObservationTransient(obsExpandingRing(obsInt(480,1120),obsInt(270,650),'#86efac',obsInt(35,75)),3000);
}
return spawnObservationTransient(`
`, 5200);
}
function generateMilitaryActivity(power) {
const effect = obsPick(['contact','intercept','formation','sector','status','sweep']);
if (effect === 'contact') {
const pos = obsExhibitCardPosition(320, 82);
return spawnObservationTransient(
obsContact(obsInt(430,1170),obsInt(240,680),'#eab308',obsPick(['UNKNOWN','TRACK','CONTACT','RETURN'])) +
obsTextCard(pos.x,pos.y,'TACTICAL UPDATE',`TRACK ${obsHex(obsInt(1,4095))} // ${obsInt(2,98)}%`,'#eab308',320),
4600
);
}
if (effect === 'intercept') {
const x=obsInt(450,1150),y=obsInt(250,650);
return spawnObservationTransient(`
${obsContact(x,y,'#ef4444','VECTOR')}
`, 4700);
}
if (effect === 'formation') {
const cx=obsInt(600,1000), cy=obsInt(340,600);
return spawnObservationTransient(`
`, 5200);
}
if (effect === 'sector') {
return spawnObservationTransient(`
`, 3800);
}
if (effect === 'status') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x,pos.y,obsPick(['TACTICAL ARRAY','TRACK CORRELATION','FORMATION UPDATE','IFF QUERY']),
obsPick([`SECTOR ${obsInt(1,12)} / CLEAR`,`RETURN ${obsHex(obsInt(1,4095))}`,`VECTOR ${obsInt(0,359)}.${obsInt(0,9)}°`,`CONF ${obsInt(34,99)}%`]),'#eab308'
),4200);
}
return spawnObservationTransient(obsExpandingRing(800,470,'#eab308',obsInt(65,130)),3500);
}
function generateDeepSpaceActivity(power) {
const effect = obsPick(['anomaly','comet','lens','spectral','planet','signal']);
if (effect === 'anomaly') {
const x=obsInt(430,1180), y=obsInt(230,650);
return spawnObservationTransient(`
`, 6500);
}
if (effect === 'comet') {
return spawnObservationTransient(`
`, 5200);
}
if (effect === 'lens') {
return spawnObservationTransient(`
`, 6800);
}
if (effect === 'planet') {
const y=obsInt(260,620), r=obsInt(45,110);
return spawnObservationTransient(`
`, 10000);
}
if (effect === 'spectral') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x,pos.y,obsPick(['SPECTRAL EVENT','GRAVITIC VARIANCE','DEEP FIELD RETURN','OPTICAL TRANSIENT']),
`${obsRand(.001,9.999).toFixed(3)} / ${obsInt(1,999)} ly`,'#818cf8'
),4800);
}
return spawnObservationTransient(obsExpandingRing(obsInt(420,1180),obsInt(240,660),'#38bdf8',obsInt(45,95)),4000);
}
function generateOutlawActivity(power) {
const effect = obsPick(['glitch','route','contact','signal','gauge','rear']);
if (effect === 'glitch') {
const bars=Array.from({length:obsInt(8,18)},()=>`
`).join('');
return spawnObservationTransient(`${bars}`,3000);
}
if (effect === 'route') {
return spawnObservationTransient(`
ROUTE RECALC...
`,5600);
}
if (effect === 'contact' || effect === 'rear') {
return spawnObservationTransient(obsContact(
obsInt(430,1170),obsInt(240,670),obsPick(['#ec4899','#f59e0b']),'UNKNOWN'
),4200);
}
if (effect === 'signal') {
const pos = obsExhibitCardPosition(290, 82);
return spawnObservationTransient(obsTextCard(
pos.x,pos.y,obsPick(['TRANSPONDER HIT','SIGNAL QUALITY','NAV PATCH','RADIO BURST']),
`${obsInt(9,99)}% // ${obsHex(obsInt(1,4095))}`,'#ec4899'
),4300);
}
return spawnObservationTransient(`
`,4500);
}
function generateStationActivity(power) {
const effect = obsPick(['dock','depart','guidance','traffic','beacon','queue']);
if (effect === 'dock') {
const y = obsInt(250, 520);
return spawnObservationTransient(`
DOCK VECTOR ${obsInt(1,18)}
`, 6200);
}
if (effect === 'depart') {
const y = obsInt(235, 540);
return spawnObservationTransient(`
`, 7000);
}
if (effect === 'guidance') {
const x = obsInt(470, 1100), y = obsInt(230, 570);
return spawnObservationTransient(`
`, 4600);
}
if (effect === 'beacon') {
const x = obsPick([248, 1352]);
const y = obsPick([168, 648]);
return spawnObservationTransient(`
`, 3600);
}
if (effect === 'queue') {
const pos = obsExhibitCardPosition(310, 82);
return spawnObservationTransient(obsTextCard(
pos.x, pos.y,
obsPick(['TRAFFIC CONTROL','DOCK STATUS','PORT AUTHORITY','BERTH SCHEDULER']),
obsPick([
`QUEUE ${obsInt(2,18)} // CLEARANCE`,
`BAY ${obsInt(1,24)} // ARRIVAL WINDOW`,
`HOLD ARC ${obsRand(0.8,9.9).toFixed(1)} // ROUTING`,
`${obsInt(2,84)} OBJECTS // PORT FLOW`
]),
'#38bdf8', 310
), 4600);
}
const x = obsInt(330, 1270), y = obsInt(205, 610);
return spawnObservationTransient(`
TRAFFIC FLOW ${obsInt(1,9)}
`, 5200);
}
function generateComedyActivity(power) {
const effect = obsPick(['route','oddity','planet','status','contact','geometry']);
if (effect === 'route') {
return spawnObservationTransient(`
`,6000);
}
if (effect === 'oddity') {
const pos = obsExhibitCardPosition(330, 82);
return spawnObservationTransient(obsTextCard(
pos.x,pos.y,
obsPick(['NAVIGATION UPDATE','ENVIRONMENTAL NOTE','SERVICE MESSAGE','SENSOR CONCLUSION']),
obsPick([
`ROUTE CONFIDENCE ${obsRand(96,103).toFixed(1)}%`,
`TEA PROBABILITY ${obsInt(12,99)}%`,
`MOSTLY NOMINAL`,
`OBJECT: PROBABLY HARMLESS`,
`PANIC INDEX ${obsInt(0,3)} / 10`,
`SCENIC DETOUR +${obsInt(2,48)} MIN`
]),'#06b6d4',330
),5000);
}
if (effect === 'planet') {
return spawnObservationTransient(`
`,9200);
}
if (effect === 'geometry') {
const sides=obsPick([3,4,5,6]);
const r=obsInt(55,110), cx=obsInt(500,1100),cy=obsInt(300,620);
const pts=Array.from({length:sides},(_,i)=>{
const a=-Math.PI/2+i*Math.PI*2/sides;
return `${(cx+Math.cos(a)*r).toFixed(1)},${(cy+Math.sin(a)*r).toFixed(1)}`;
}).join(' ');
return spawnObservationTransient(``,5200);
}
if (effect === 'contact') {
return spawnObservationTransient(obsContact(obsInt(450,1150),obsInt(250,650),'#67e8f9',obsPick(['FRIENDLY?','THING','OBJECT','PROBABLY FINE'])),4300);
}
return spawnObservationTransient(obsExpandingRing(obsInt(450,1150),obsInt(250,650),obsPick(['#06b6d4','#f43f5e','#fbbf24']),obsInt(45,100)),3500);
}
function triggerObservationActivity(source = 'ambient') {
if (!observationActive || observationActivity <= 0.001) return;
// Telemetry is a synchronization pulse, but the slider controls whether
// that pulse becomes visible. At maximum every telemetry pulse produces
// a visible response; at low levels many pass quietly.
if (source === 'telemetry') {
const responseChance = 0.18 + observationActivity * 0.82;
if (Math.random() > responseChance) return;
}
let count = 1;
if (source === 'telemetry' && observationActivity > .55 && Math.random() < observationActivity) count++;
if (observationActivity > .82 && Math.random() < (observationActivity - .7)) count++;
const generator = {
starfleet: generateStarfleetActivity,
whoniverse: generateWhoniverseActivity,
industrial: generateIndustrialActivity,
bioships: generateBioshipActivity,
retrofuture: generateRetroActivity,
military: generateMilitaryActivity,
deepspace: generateDeepSpaceActivity,
outlaw: generateOutlawActivity,
spacestations: generateStationActivity,
comedy: generateComedyActivity
}[activeUniverseId] || generateStarfleetActivity;
for (let i = 0; i < count; i++) {
window.setTimeout(() => generator(observationActivity), i * obsInt(120,380));
}
}
function scheduleObservationAmbientActivity() {
if (observationAmbientTimer) {
clearTimeout(observationAmbientTimer);
observationAmbientTimer = null;
}
if (!observationActive || observationActivity <= .01) return;
// Quiet = occasional subtle life. Active = a busy generative display.
const slow = 12500;
const fast = 1500;
const base = slow - (slow - fast) * Math.pow(observationActivity, .82);
const jitter = base * obsRand(.25,.75);
observationAmbientTimer = window.setTimeout(() => {
triggerObservationActivity('ambient');
scheduleObservationAmbientActivity();
}, base + jitter);
}
function setObservationActivity(raw) {
observationActivity = obsClamp(Number(raw) / 100, 0, 1);
window.observationActivity = observationActivity;
observationOverlay.style.setProperty('--obs-activity', String(observationActivity));
const pct = Math.round(observationActivity * 100);
valObservationActivity.textContent = `${pct}%`;
if (observationActive) {
observationStatus.innerHTML =
`OBSERVATION ACTIVE
PROCEDURAL AUDIO LINK: ${isPlaying ? 'ONLINE' : 'STANDBY'}
VISUAL ACTIVITY: ${pct}%`;
scheduleObservationAmbientActivity();
}
}
function resetObservationReturnFade() {
if (!observationReturn) return;
observationReturn.style.transition = 'none';
observationReturn.classList.remove('observation-return-fade');
void observationReturn.offsetWidth;
observationReturn.style.transition = '';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (observationActive) {
observationReturn.classList.add('observation-return-fade');
}
});
});
}
function resetObservationHudFade() {
if (!observationChrome) return;
// Remove transition momentarily so each OBSERVATION session starts fully visible.
observationChrome.style.transition = 'none';
observationChrome.classList.remove('observation-hud-fade');
void observationChrome.offsetWidth;
observationChrome.style.transition = '';
if (!toggleObservationHud.checked) {
// Start the thirty-second fade on the next frame.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
if (observationActive && !toggleObservationHud.checked) {
observationChrome.classList.add('observation-hud-fade');
}
});
});
}
}
function updateObservationHudMode() {
const hold = toggleObservationHud.checked;
valObservationHud.textContent = hold ? 'ON' : 'FADE';
if (!observationActive || !observationChrome) return;
if (hold) {
observationChrome.style.transition = 'none';
observationChrome.classList.remove('observation-hud-fade');
void observationChrome.offsetWidth;
observationChrome.style.transition = '';
} else {
resetObservationHudFade();
}
}
toggleObservationHud.addEventListener('change', updateObservationHudMode);
updateObservationHudMode();
sliderObservationActivity.addEventListener('input', () => {
setObservationActivity(sliderObservationActivity.value);
if (observationActive) scheduleObservationInstrumentTick();
});
window.addEventListener('scifi-telemetry-activity', (event) => {
if (!observationActive) return;
triggerObservationActivity('telemetry');
nudgeObservationInstruments(event.detail ? event.detail.density : null);
pulseObservationScene();
pulseObservationSplineMorphs();
});
setObservationActivity(sliderObservationActivity.value);
// =========================================================================
// LIVE OBSERVATION INSTRUMENTATION
// The fixed OBSERVATION chrome is intentionally outside this system.
// Only fantasy elements inside the main SVG/event layers participate.
// =========================================================================
let observationInstrumentStates = [];
let observationInstrumentTimer = null;
let observationTransientObserver = null;
const observationTextVocabulary = {
starfleet: ['NOMINAL','PASSIVE','SCANNING','CORRELATING','TRACKING','VERIFIED','STABLE'],
whoniverse: ['VORTEX LOCK','CONVERGING','DRIFTING','RECURSIVE','RESOLVING','TEMPORAL HOLD'],
industrial: ['SECURE FEED','MONITORING','SIGNAL HOLD','CYCLING','LOCAL CHECK','STABLE'],
bioships: ['QUIESCENT','RESPONDING','ADAPTING','RESONANT','SYNCHRONIZED','PULSING'],
retrofuture: ['READY','READ','PROCESS','HOLD','TRACK','STABLE'],
military: ['PASSIVE','TRACKING','CORRELATING','VERIFIED','WATCH','CLEAR'],
deepspace: ['DEEP FIELD','OBSERVING','SPECTRAL HOLD','PASSIVE','RESOLVING','STABLE'],
outlaw: ['MANUAL','INTERMITTENT','PATCHED','SEARCHING','HOLDING','DIRTY LOCK'],
spacestations: ['PASSIVE WATCH','TRAFFIC HOLD','DOCKING','ROUTING','OBSERVING','CLEAR'],
comedy: ['PROBABLY FINE','MOSTLY NOMINAL','STILL FINE','SCENIC','TEA ADJACENT','UNNECESSARILY PRECISE']
};
const observationPlainTextVariants = {
comedy: {
'TEA SOURCE DETECTED': ['TEA SOURCE DETECTED','TEA SOURCE PROBABLE','TEA SOURCE LOST','BEVERAGE VECTOR ACQUIRED'],
'NO IMMEDIATE CAUSE FOR PANIC': ['NO IMMEDIATE CAUSE FOR PANIC','STILL NO CAUSE FOR PANIC','PANIC NOT CURRENTLY REQUIRED','SITUATION MOSTLY NORMAL'],
'SCENIC NAVIGATION': ['SCENIC NAVIGATION','SCENIC RECALCULATION','ROUTE APPRECIATION','NAVIGATION, BUT NICER']
}
};
function getObservationTelemetryDensity() {
if (typeof telemetry !== 'undefined' && telemetry && telemetry.params) {
return obsClamp(Number(telemetry.params.density) || 0, 0, 1);
}
const slider = document.getElementById('slider-telemetry-density');
return slider ? obsClamp(Number(slider.value) || 0, 0, 1) : 0.5;
}
function classifyObservationNumber(source, raw, index) {
const upper = source.toUpperCase();
const decimals = raw.includes('.') ? raw.split('.')[1].length : 0;
const unsignedRaw = raw.replace(/^[-+]/, '');
const integerPart = unsignedRaw.split('.')[0];
const leadingZero = integerPart.length > 1 && integerPart.startsWith('0');
const resolution = Math.pow(10, -decimals);
const base = Number(raw);
let kind = 'generic';
let min = -Infinity;
let max = Infinity;
let circular = false;
let macroScale = Math.max(resolution * 2, Math.abs(base || 1) * 0.025);
const after = source.slice(index + raw.length, index + raw.length + 4);
const before = source.slice(Math.max(0, index - 24), index).toUpperCase();
if (after.includes('%') || upper.includes('QUALITY') || upper.includes('CONFIDENCE') || upper.includes('EFFICIENCY')) {
kind = 'percent';
min = 0;
max = upper.includes('CONFIDENCE') ? 103 : 100;
macroScale = 8;
} else if (upper.includes('NAV VECTOR') || upper.includes('BEARING') || upper.includes('HEADING') || upper.includes('VECTOR ')) {
kind = 'heading';
min = 0;
max = 360;
circular = true;
macroScale = 18;
} else if (upper.includes('CAM ') || upper.includes('CONTACT ') || upper.includes('TRACK ') ||
upper.includes('BAY ') || upper.includes('SECTOR ') || upper.includes('OBJECT ') ||
before.endsWith('CAM ') || before.endsWith('TRACK ')) {
kind = 'identifier';
min = 0;
max = leadingZero ? Math.pow(10, integerPart.length) - 1 : Math.max(99, base + 24);
macroScale = Math.max(1, Math.min(4, 1 + base * .02));
} else if (upper.includes('RANGE') || upper.includes('DIST') || upper.includes(' LY') || upper.includes(' MM')) {
kind = 'range';
min = 0;
max = Math.max(base * 3, base + 1000);
macroScale = Math.max(resolution * 3, Math.abs(base || 1) * .18);
} else if (upper.includes('TIME') || upper.includes(':')) {
kind = 'clockish';
min = 0;
max = decimals ? 99.9 : 99;
macroScale = decimals ? 2.5 : 5;
} else if (base >= 0 && base <= 100) {
min = 0;
max = 100;
macroScale = Math.max(resolution * 3, 4);
}
return {
raw,
base,
current: base,
target: base,
decimals,
integerWidth: integerPart.length,
leadingZero,
resolution,
kind,
min,
max,
circular,
macroScale,
flutterOffset: 0,
nextFlutterAt: performance.now() + obsRand(350, 1400),
correctionKick: 0,
correctionUntil: 0
};
}
function getStatusMutationRule(source) {
const text = source.trim();
const upper = text.toUpperCase();
// Camera location names are identities, not status fields.
if (/^CAM\s+\d+\s*\/\//i.test(text)) return null;
if (text.includes('//')) {
const splitAt = text.lastIndexOf('//');
const prefix = text.slice(0, splitAt + 2).trimEnd();
const suffix = text.slice(splitAt + 2).trim();
// These are display/status phrases and can safely wander.
if (/(PASSIVE|NOMINAL|ACTIVE|OBSERVATION|WATCH|FEED|SENSORS|SECURE|FIELD|ARRAY|CONTROL)/i.test(suffix) ||
/(SENSOR|VECTOR|TACTICAL|OPTICAL|BIOLOGICAL|TRAFFIC|LIVE)/i.test(prefix)) {
return { type: 'suffix', prefix, original: suffix };
}
}
// A few colon-delimited fantasy statuses.
const colonMatch = text.match(/^(ROUTE|TRANSPONDER)\s*:\s*(.+)$/i);
if (colonMatch) {
return { type: 'colon', prefix: colonMatch[1].toUpperCase(), original: colonMatch[2].trim() };
}
const plain = observationPlainTextVariants[activeUniverseId];
if (plain && plain[upper]) {
return { type: 'plain', original: upper, variants: plain[upper] };
}
return null;
}
function instrumentObservationTextNode(node) {
if (!node || node.nodeType !== 1 || node.dataset.obsLiveInstrumented === '1') return;
const source = (node.textContent || '').trim();
if (!source) return;
const numericMatches = [...source.matchAll(/[-+]?\d+(?:\.\d+)?/g)];
const numbers = numericMatches.map(m => classifyObservationNumber(source, m[0], m.index));
const state = {
node,
source,
numbers,
numberMatches: numericMatches.map(m => ({ raw: m[0], index: m.index })),
statusRule: getStatusMutationRule(source),
statusOverride: null,
statusUntil: 0,
nextTextGlitchAt: performance.now() + obsRand(2500, 9000),
glitchUntil: 0,
glitchSeed: 0
};
node.dataset.obsLiveInstrumented = '1';
node.classList.add('obs-live-instrument');
observationInstrumentStates.push(state);
}
function instrumentObservationTree(root) {
if (!root || !root.querySelectorAll) return;
if (root.matches && root.matches('text')) instrumentObservationTextNode(root);
root.querySelectorAll('text').forEach(instrumentObservationTextNode);
}
function initializeObservationLiveInstrumentation() {
stopObservationLiveInstrumentation(false);
observationInstrumentStates = [];
instrumentObservationTree(observationStage);
instrumentObservationTree(observationEventLayer);
observationTransientObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) instrumentObservationTree(node);
});
}
});
observationTransientObserver.observe(observationEventLayer, { childList: true, subtree: true });
scheduleObservationInstrumentTick();
}
function stopObservationLiveInstrumentation(clearStates = true) {
if (observationInstrumentTimer) {
clearTimeout(observationInstrumentTimer);
observationInstrumentTimer = null;
}
if (observationTransientObserver) {
observationTransientObserver.disconnect();
observationTransientObserver = null;
}
if (clearStates) observationInstrumentStates = [];
}
function formatObservationNumber(value, token) {
let v = value;
if (token.circular) {
const span = token.max - token.min;
v = ((v - token.min) % span + span) % span + token.min;
} else {
if (Number.isFinite(token.min)) v = Math.max(token.min, v);
if (Number.isFinite(token.max)) v = Math.min(token.max, v);
}
// Identity-like fields stay integer but can still "flutter" by one count.
if (token.kind === 'identifier') v = Math.round(v);
let result = token.decimals > 0 ? v.toFixed(token.decimals) : String(Math.round(v));
if (token.leadingZero) {
const sign = result.startsWith('-') ? '-' : '';
const unsigned = result.replace(/^-/, '');
const parts = unsigned.split('.');
parts[0] = parts[0].padStart(token.integerWidth, '0');
result = sign + parts.join('.');
}
return result;
}
function shortestCircularDelta(current, target, min, max) {
const span = max - min;
let delta = target - current;
if (delta > span / 2) delta -= span;
if (delta < -span / 2) delta += span;
return delta;
}
function rebuildObservationText(state, now) {
if (!state.node || !state.node.isConnected) return;
let cursor = 0;
let built = '';
state.numberMatches.forEach((match, i) => {
const token = state.numbers[i];
built += state.source.slice(cursor, match.index);
let displayValue = token.current + token.flutterOffset;
if (token.correctionUntil > now) {
const remaining = (token.correctionUntil - now) / 1400;
displayValue += token.correctionKick * Math.max(0, Math.min(1, remaining));
}
built += formatObservationNumber(displayValue, token);
cursor = match.index + match.raw.length;
});
built += state.source.slice(cursor);
if (state.statusOverride && state.statusUntil > now && state.statusRule) {
const rule = state.statusRule;
if (rule.type === 'suffix') {
const splitAt = built.lastIndexOf('//');
if (splitAt >= 0) built = `${built.slice(0, splitAt + 2)} ${state.statusOverride}`;
} else if (rule.type === 'colon') {
const colonAt = built.indexOf(':');
if (colonAt >= 0) built = `${built.slice(0, colonAt + 1)} ${state.statusOverride}`;
} else if (rule.type === 'plain') {
built = state.statusOverride;
}
} else if (state.statusOverride && state.statusUntil <= now) {
state.statusOverride = null;
}
// Very brief display corruption. This is visual noise only and always recovers.
if (state.glitchUntil > now && built.length > 3) {
const chars = [...built];
const candidates = [];
for (let i = 1; i < chars.length - 1; i++) {
if (/[A-Z0-9]/i.test(chars[i])) candidates.push(i);
}
if (candidates.length) {
const idx = candidates[state.glitchSeed % candidates.length];
chars[idx] = obsPick(['_','?','·','Ξ','0','1']);
built = chars.join('');
state.node.classList.add('obs-live-glitch');
}
} else {
state.node.classList.remove('obs-live-glitch');
}
state.node.textContent = built;
}
function updateObservationNumberToken(token, now) {
const activity = observationActivity;
let delta = token.circular
? shortestCircularDelta(token.current, token.target, token.min, token.max)
: token.target - token.current;
// Activity controls how quickly the displayed instrument tracks its target.
const trackingFraction = 0.07 + activity * 0.31;
if (Math.abs(delta) > token.resolution * .15) {
token.current += delta * trackingFraction;
} else {
token.current = token.target;
}
// Micro flutter: the least-significant digit hunts around the real value.
if (now >= token.nextFlutterAt) {
const flutterChance = 0.34 + activity * 0.56;
if (Math.random() < flutterChance) {
const direction = obsPick([-1, 1]);
const steps = Math.random() < (.18 + activity * .2) ? 2 : 1;
const identifierDamping = token.kind === 'identifier' ? .55 : 1;
token.flutterOffset = direction * token.resolution * steps * identifierDamping;
} else {
token.flutterOffset = 0;
}
const slow = 1850;
const fast = 240;
token.nextFlutterAt = now + (slow - (slow-fast) * activity) * obsRand(.65, 1.35);
} else if (Math.random() < .16 + activity * .18) {
token.flutterOffset *= .45;
if (Math.abs(token.flutterOffset) < token.resolution * .15) token.flutterOffset = 0;
}
if (token.circular) {
const span = token.max - token.min;
token.current = ((token.current - token.min) % span + span) % span + token.min;
} else {
if (Number.isFinite(token.min)) token.current = Math.max(token.min, token.current);
if (Number.isFinite(token.max)) token.current = Math.min(token.max, token.current);
}
}
function maybeGlitchObservationText(state, now) {
if (now < state.nextTextGlitchAt) return;
const activity = observationActivity;
const density = getObservationTelemetryDensity();
const chance = .06 + activity * .18 + density * .05;
if (Math.random() < chance) {
state.glitchSeed = obsInt(0, 100000);
state.glitchUntil = now + obsRand(90, 260 + activity * 180);
}
const slow = 17000;
const fast = 2800;
state.nextTextGlitchAt = now + (slow - (slow-fast) * activity) * obsRand(.6, 1.4);
}
function observationInstrumentTick() {
observationInstrumentTimer = null;
if (!observationActive) return;
const now = performance.now();
observationInstrumentStates = observationInstrumentStates.filter(state => state.node && state.node.isConnected);
for (const state of observationInstrumentStates) {
state.numbers.forEach(token => updateObservationNumberToken(token, now));
maybeGlitchObservationText(state, now);
rebuildObservationText(state, now);
}
scheduleObservationInstrumentTick();
}
function scheduleObservationInstrumentTick() {
if (!observationActive) return;
if (observationInstrumentTimer) clearTimeout(observationInstrumentTimer);
// Observation level directly controls the "instrument refresh rate".
// At quiet settings values settle visibly and slowly; at high settings
// they look like fast live telemetry.
const slowMs = 900;
const fastMs = 105;
const delay = slowMs - (slowMs - fastMs) * Math.pow(observationActivity, .88);
observationInstrumentTimer = setTimeout(observationInstrumentTick, delay);
}
function chooseObservationTarget(token, density) {
const amplitudeFloor = token.resolution;
const amplitude = Math.max(
amplitudeFloor,
token.macroScale * (.06 + Math.pow(density, 1.12))
);
let delta;
if (token.kind === 'identifier') {
const maxStep = Math.max(1, Math.round(1 + density * 3));
delta = obsPick([-1,1]) * obsInt(1, maxStep);
} else {
delta = obsRand(-amplitude, amplitude);
}
let target = token.target + delta;
if (token.circular) {
const span = token.max - token.min;
target = ((target - token.min) % span + span) % span + token.min;
} else {
if (Number.isFinite(token.min)) target = Math.max(token.min, target);
if (Number.isFinite(token.max)) target = Math.min(token.max, target);
}
token.target = target;
// Sometimes a display control-loop overshoots and corrects itself.
if (Math.random() < .28 + observationActivity * .24) {
const sign = Math.random() < .5 ? -1 : 1;
const kickSteps = token.kind === 'identifier' ? 1 : obsRand(.6, 2.2 + observationActivity * 1.5);
token.correctionKick = sign * token.resolution * kickSteps;
token.correctionUntil = performance.now() + obsRand(550, 1450);
}
}
function chooseObservationStatusOverride(state, density) {
if (!state.statusRule) return;
let variants;
const rule = state.statusRule;
if (rule.type === 'plain') {
variants = rule.variants || [];
} else {
variants = observationTextVocabulary[activeUniverseId] || observationTextVocabulary.starfleet;
}
if (!variants.length) return;
const original = rule.original.toUpperCase();
const choices = variants.filter(v => v !== original);
if (!choices.length) return;
state.statusOverride = obsPick(choices);
// Higher observation activity means state changes read faster and clear sooner.
const holdSlow = 6200;
const holdFast = 1700;
state.statusUntil = performance.now() +
(holdSlow - (holdSlow-holdFast) * observationActivity) * obsRand(.7, 1.35);
}
function nudgeObservationInstruments(densityOverride = null) {
if (!observationActive) return;
const density = densityOverride == null
? getObservationTelemetryDensity()
: obsClamp(Number(densityOverride) || 0, 0, 1);
const liveStates = observationInstrumentStates.filter(s => s.node && s.node.isConnected);
const numericStates = liveStates.filter(s => s.numbers.length);
const statusStates = liveStates.filter(s => s.statusRule);
// Density controls how much of the imaginary system is disturbed by a chirp.
const desiredNumeric = Math.min(
numericStates.length,
Math.max(1, Math.round(1 + density * (2 + numericStates.length * .22)))
);
const shuffledNumbers = [...numericStates].sort(() => Math.random() - .5);
for (let i = 0; i < desiredNumeric; i++) {
const state = shuffledNumbers[i];
state.numbers.forEach(token => {
// A dense chirp can perturb several values in a compound readout.
if (Math.random() < .55 + density * .4) chooseObservationTarget(token, density);
});
}
// Text states are less frequent than numeric movement. They read as system
// transitions rather than a constantly changing label soup.
if (statusStates.length && Math.random() < .28 + density * .5) {
const desiredText = density > .72 && Math.random() < density ? 2 : 1;
const shuffledText = [...statusStates].sort(() => Math.random() - .5);
for (let i = 0; i < Math.min(desiredText, shuffledText.length); i++) {
chooseObservationStatusOverride(shuffledText[i], density);
}
}
}
function seededUnit(seed) {
let x = Math.sin(seed * 12.9898 + 78.233) * 43758.5453;
return x - Math.floor(x);
}
function makeStars(count = 90, width = 1600, height = 900, tint = '#dbeafe', seedOffset = 0) {
let stars = '';
for (let i = 0; i < count; i++) {
const x = Math.floor(seededUnit(i + 1 + seedOffset) * width);
const y = Math.floor(seededUnit((i + 1 + seedOffset) * 7.31) * height);
const r = (0.55 + seededUnit((i + seedOffset + 4) * 3.17) * 1.65).toFixed(2);
const o = (0.25 + seededUnit((i + seedOffset + 9) * 5.19) * 0.72).toFixed(2);
stars += ``;
}
return stars;
}
function svgFrame(inner, defs = '') {
return `
`;
}
function renderObservationStarfleet() {
const starsA = makeStars(115, 1750, 900, '#dbeafe', 2);
const starsB = makeStars(55, 1750, 900, '#93c5fd', 220);
return svgFrame(`
${starsA}
${starsB}
LONG RANGE SENSOR ARRAY // PASSIVE
NAV VECTOR 034.8 // NOMINAL
`);
}
function renderObservationWhoniverse() {
return svgFrame(`
◉
◎
⌁ 37.4 ∴ 9
∆ 04:11:∞
TEMPORAL DISPLACEMENT // VORTEX OBSERVATION
`);
}
function renderObservationIndustrial() {
// The industrial viewport frame (getViewportFrameSvg) masks everything
// outside x 140..1460, y 160..740, so the whole surveillance console is
// laid out inside that safe area. v12 ran the feeds to x 110..1490 and had
// its edges eaten by the bulkhead.
const SAFE_X0 = 156, SAFE_X1 = 1444;
// Feeds are authored at 650x270 and drawn through a uniform scale, so the
// scenes below can keep one readable coordinate space.
const CW = 650, CH = 270, S = 0.78;
const IX = 14, IY = 14, IW = 622, IH = 242;
const CAM_X = [279, 814], CAM_Y = [248, 472];
// Identity text (camera names, equipment designations, container IDs) is
// opted OUT of the live-instrumentation number drift by pre-stamping the
// flag it checks. Only telemetry should wander; "CAM 02" must not quietly
// become "CAM 01", which is exactly what v12 did to every label it drew.
const ID = 'data-obs-live-instrumented="1"';
// ---------------------------------------------------------------- parts
const crate = (x, y, w, h, body, edge, id, idColor) => {
let ribs = '';
for (let i = 9; i < w - 5; i += 9) ribs += `M${i} 4V${h - 4}`;
return `
${id}
`;
};
// Vent vapour authored as in-scene geometry so the camera clip contains it.
// v12 floated these on the sim layer, which is why steam drifted over the
// bezels and read as detached from any feed.
const vapour = (x, y, count, seed, scale = 1) => {
let out = '';
for (let i = 0; i < count; i++) {
const dur = (4.5 + seededUnit(seed + i) * 3.6).toFixed(1);
const delay = (i * (dur / count)).toFixed(1);
const vx = Math.round(-24 + seededUnit(seed + i * 3.1) * 48);
const r = ((9 + seededUnit(seed + i * 5.7) * 13) * scale).toFixed(1);
out += ``;
}
return out;
};
const gauge = (x, y, r, label, needleColor, delay) => {
let ticks = '';
for (let a = -120; a <= 120; a += 30) {
const rad = (a - 90) * Math.PI / 180;
ticks += `M${(Math.cos(rad) * (r - 3)).toFixed(1)} ${(Math.sin(rad) * (r - 3)).toFixed(1)}` +
`L${(Math.cos(rad) * (r - 8)).toFixed(1)} ${(Math.sin(rad) * (r - 8)).toFixed(1)}`;
}
return `
${label}
`;
};
// Per-feed video artifacts. This replaces v12's single screen-wide sweep
// bar: each feed rolls, tears and grains on its own clock, inside its own
// clip, so the artifacts read as video rather than as a screen overlay.
const feedFx = (i) => `
`;
const cam = (i, label, code, status, scene) => `
${scene}
${feedFx(i)}
${label}
${code}
${status}
LIVE // SECURE FEED
SELECTED
`;
// ------------------------------------------------- procedural geometry
let bayChev = '';
for (let i = 0; i < 660; i += 16) bayChev += `M${i} 222L${i + 8} 212H${i + 16}L${i + 8} 222Z`;
let bayDeck = '';
for (let i = 20; i < 640; i += 44) bayDeck += `M${i} 214V256`;
let coreSeg = '';
for (let a = 0; a < 360; a += 45) {
const rad = a * Math.PI / 180;
coreSeg += `M${(Math.cos(rad) * 62).toFixed(1)} ${(Math.sin(rad) * 62).toFixed(1)}` +
`L${(Math.cos(rad) * 86).toFixed(1)} ${(Math.sin(rad) * 86).toFixed(1)}`;
}
let rods = '';
for (let i = 0; i < 7; i++) {
const rx = -60 + i * 20;
rods += `
`;
}
const trefoil = [0, 120, 240].map(a =>
``).join('');
let plantGrate = '';
for (let i = 20; i < 640; i += 40) plantGrate += `M${i} 210V256`;
let dogs = '';
for (let a = 0; a < 360; a += 45) {
const rad = a * Math.PI / 180;
dogs += `
`;
}
let hazRing = '';
for (let a = 0; a < 360; a += 15) {
if ((a / 15) % 2) continue;
const r0 = 78, r1 = 90, a0 = a * Math.PI / 180, a1 = (a + 15) * Math.PI / 180;
hazRing += `M${(Math.cos(a0) * r0).toFixed(1)} ${(Math.sin(a0) * r0).toFixed(1)}` +
`L${(Math.cos(a0) * r1).toFixed(1)} ${(Math.sin(a0) * r1).toFixed(1)}` +
`A${r1} ${r1} 0 0 1 ${(Math.cos(a1) * r1).toFixed(1)} ${(Math.sin(a1) * r1).toFixed(1)}` +
`L${(Math.cos(a1) * r0).toFixed(1)} ${(Math.sin(a1) * r0).toFixed(1)}` +
`A${r0} ${r0} 0 0 0 ${(Math.cos(a0) * r0).toFixed(1)} ${(Math.sin(a0) * r0).toFixed(1)}Z`;
}
const lamp = (y, txt, col, i) => `
${txt}
`;
const suits = [0, 1, 2].map(i => `
`).join('');
let lockFloor = '';
for (let i = 20; i < 640; i += 40) lockFloor += `M${i} 214V256`;
const vpx = 325, vpy = 128;
let corrFrames = '', corrFloor = '', corrLamps = '';
for (let i = 0; i < 7; i++) {
const s = Math.pow(0.76, i);
const x0 = vpx + (24 - vpx) * s, x1 = vpx + (626 - vpx) * s;
const y0 = vpy + (22 - vpy) * s, y1 = vpy + (248 - vpy) * s;
corrFrames += ``;
corrFloor += ``;
}
for (let x = 24; x <= 626; x += 60) {
corrFloor += ``;
}
for (let i = 1; i < 5; i++) {
const s = Math.pow(0.76, i);
const y = vpy + (34 - vpy) * s, w = 62 * s;
corrLamps += `
`;
}
const fanBlades = [0, 72, 144, 216, 288].map(a =>
``).join('');
const drip = (x, y, d) =>
`
`;
// ------------------------------------------------------- CAM 01 :: BAY
// A working cargo hold: a gantry crane on an overhead rail running a real
// pick-and-place cycle over stacked containers. v12 had a wheeled cart
// sliding back and forth, which read as a car and made no sense on a ship.
const sceneCargo = `
BAY 2 // AFT
${crate(90, 174, 88, 40, '#5d4427', '#8a6a42', 'CB-4471', '#e8c88a')}
${crate(90, 134, 88, 40, '#6b4d2c', '#96754a', 'CB-4472', '#e8c88a')}
${crate(182, 174, 88, 40, '#4f3a22', '#7d6039', 'HAZ-3/09', '#f0b64f')}
${crate(380, 174, 88, 40, '#5a4426', '#886946', 'CB-3310', '#e8c88a')}
${crate(380, 134, 88, 40, '#6b4d2c', '#96754a', 'CB-4472', '#e8c88a')}
${crate(472, 174, 88, 40, '#453120', '#6f5535', 'ORE-C', '#d9b070')}
${crate(-44, 6, 88, 40, '#6b4d2c', '#96754a', 'CB-4472', '#e8c88a')}
${vapour(250, 250, 6, 12.4, .8)}
GANTRY 4 // AUTO CYCLE
LOAD 2.4 T
`;
// --------------------------------------------------- CAM 02 :: REACTOR
const sceneReactor = `
${rods}
${gauge(424, 74, 26, 'TEMP', '#f59e0b', 0)}
${gauge(490, 74, 26, 'PRESS', '#38bdf8', 1.4)}
${gauge(556, 74, 26, 'FLUX', '#22c55e', 2.8)}
${trefoil}
${vapour(392, 206, 5, 31.7, .85)}
FUSION PLANT 1
OUTPUT 68%
`;
// --------------------------------------------------- CAM 03 :: AIRLOCK
const sceneAirlock = `
${suits}
${dogs}
CYCLE STATE
${lamp(20, 'VACUUM', '#38bdf8', 0)}
${lamp(44, 'EQUALIZE', '#f59e0b', 1)}
${lamp(68, 'PRESSURE', '#22c55e', 2)}
${gauge(556, 172, 28, 'CHAMBER kPa', '#38bdf8', 1.1)}
${vapour(300, 212, 5, 57.3, .7)}
AIRLOCK C
OUTER DOOR SEALED
`;
// -------------------------------------------------- CAM 04 :: CORRIDOR
const sceneCorridor = `
${corrFloor}
${corrFrames}
${corrLamps}
${fanBlades}
${drip(196, 96, 1.4)}
${drip(438, 82, 3.1)}
${drip(268, 66, 5.2)}
${vapour(520, 236, 5, 78.1, .75)}
FRAME 22 TO 34
EXTRACTOR 3 ONLINE
`;
// ------------------------------------------------- console side rails
let rails = '';
for (let i = 0; i < 8; i++) {
const y = 252 + i * 54;
const fill = 22 + ((i * 41) % 62);
rails += `
`;
}
const defs = `
`;
return svgFrame(`
${cam(0, 'CAM 01 // CARGO BAY 2', 'C-01 4K/IR', 'GANTRY 4 AUTO // 2 CONTACTS', sceneCargo)}
${cam(1, 'CAM 02 // REACTOR ACCESS', 'C-02 4K/RAD', 'PLANT 1 NOMINAL // LOOP A', sceneReactor)}
${cam(2, 'CAM 03 // AIRLOCK C', 'C-03 4K/LL', 'OUTER DOOR SEALED // NO EVA', sceneAirlock)}
${cam(3, 'CAM 04 // MACHINE CORRIDOR', 'C-04 4K/IR', 'FRAME 22-34 // EXTRACTOR ON', sceneCorridor)}
CSV KESTREL - HEAVY HAULER
REG CB-4471 // DECK 4 SURVEILLANCE RING
SHIP TIME
04:17:33
RECORDER
ARMED
4 OF 4 FEEDS LOCKED
BANDWIDTH 62% // ARCHIVE 71% FULL
${rails}
BUS A
BUS B
MANIFEST 4471-C // ORE CONCENTRATE + SEALED HAZ-3
HULL 12%
BAY 101.3 KPA
RCTR 68%
O2 20.9%
GRAV 0.94 G
NOMINAL
`, defs);
}
function renderObservationBioships() {
return svgFrame(`
BIOLOGICAL SYSTEMS // PASSIVE NEURAL OBSERVATION
`);
}
function renderObservationRetro() {
const stars = makeStars(95, 1600, 900, '#86efac', 720);
return svgFrame(`
${stars}
ASTROGATION ARRAY
VECTOR MEMORY: LOCKED
SCAN 360°
ANALOG BEAM: ACTIVE
`);
}
function renderObservationMilitary() {
const stars = makeStars(110, 1650, 900, '#fef3c7', 960);
return svgFrame(`
${stars}
CONTACT 03 // TRACKING
TACTICAL WATCH // PASSIVE
WEAPONS SAFE // SENSORS ACTIVE
`);
}
function renderObservationDeepSpace() {
const starsA = makeStars(155, 1800, 900, '#eef2ff', 1220);
const starsB = makeStars(65, 1800, 900, '#818cf8', 1430);
// Planets, nebula clouds, dust lanes, derelicts and anomalies now live in the canvas
// engine's procedural, session-seeded Deep Space field (see ObservationEngine's
// regenerateDeepSpaceField / renderDeepSpaceField). This legacy SVG layer is kept
// intentionally sparse -- just a comet accent and the HUD label -- so it never paints a
// fixed duplicate planet/nebula on top of that varying scene.
return svgFrame(`
${starsA}
${starsB}
LONG RANGE OPTICAL // DEEP FIELD OBSERVATION
`);
}
function renderObservationOutlaw() {
const stars = makeStars(115, 1700, 900, '#fbcfe8', 1710);
return svgFrame(`
${stars}
REAR FEED // 02
ROUTE: MANUAL
TRANSPONDER: INTERMITTENT
SIGNAL QUALITY: 62%
`);
}
function renderObservationStations() {
const starsNear = makeStars(105, 1160, 540, '#e0f2fe', 1990);
const starsFar = makeStars(55, 1160, 540, '#7dd3fc', 2310);
const telemetryModule = (x, y, w, h, title, line1, line2 = '') => `
${title}
${line1}
${line2 ? `${line2}` : ''}
`;
return svgFrame(`
${starsNear}
${starsFar}
${telemetryModule(105, 170, 300, 96, 'OBSERVATION DECK', 'RING RATE 01.8 // STABLE', 'STARFIELD DRIFT 004.2')}
${telemetryModule(1195, 170, 300, 96, 'TRAFFIC CONTROL', 'DOCK VECTOR 118.4 // ROUTING', 'QUEUE 03 // CLEARANCE')}
${telemetryModule(105, 590, 320, 96, 'PORT STATUS', 'BAY 12 // ARRIVAL WINDOW', 'HOLDING ARC 02.7')}
${telemetryModule(1175, 590, 320, 96, 'TRANSIT SPINE', 'LOCAL FLOW 07 // NOMINAL', 'HABITAT SPIN 00.9')}
STATION OBSERVATION // ROTATIONAL TRAFFIC WINDOW
`);
}
function renderObservationComedy() {
const stars = makeStars(120, 1700, 900, '#cffafe', 2260);
return svgFrame(`
${stars}
SCENIC NAVIGATION
ROUTE CONFIDENCE: 99.7% PROBABLY FINE
TEA SOURCE DETECTED
NO IMMEDIATE CAUSE FOR PANIC
`);
}
function getObservationSvg(universeId) {
switch (universeId) {
case 'starfleet': return renderObservationStarfleet();
case 'whoniverse': return renderObservationWhoniverse();
case 'industrial': return renderObservationIndustrial();
case 'bioships': return renderObservationBioships();
case 'retrofuture': return renderObservationRetro();
case 'military': return renderObservationMilitary();
case 'deepspace': return renderObservationDeepSpace();
case 'outlaw': return renderObservationOutlaw();
case 'spacestations': return renderObservationStations();
case 'comedy': return renderObservationComedy();
default: return renderObservationStarfleet();
}
}
function refreshObservation() {
const universe = UniverseRegistry[activeUniverseId];
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
if (!universe || !preset) return;
observationUniverse.textContent = `${universe.shortCode} // OBSERVATION`;
observationProfile.textContent = preset.name.toUpperCase();
observationStatus.innerHTML =
`OBSERVATION ACTIVE
PROCEDURAL AUDIO LINK: ${isPlaying ? 'ONLINE' : 'STANDBY'}
VISUAL ACTIVITY: ${Math.round(observationActivity * 100)}%`;
observationOverlay.style.setProperty('--obs-activity', String(observationActivity));
observationStage.innerHTML = getObservationSvg(activeUniverseId);
initializeObservationSplineMorphs();
startObservationScene();
initializeObservationLiveInstrumentation();
if (observationEngine) {
observationEngine.start(activeUniverseId);
}
}
function enterObservation() {
if (observationActive) return;
observationActive = true;
observationOverlay.classList.add('active');
observationOverlay.setAttribute('aria-hidden', 'false');
refreshObservation();
resetObservationHudFade();
resetObservationReturnFade();
scheduleObservationAmbientActivity();
// Give the display an immediate but restrained sign of life.
if (observationActivity > .08) {
window.setTimeout(() => triggerObservationActivity('ambient'), 700);
}
}
function exitObservation() {
if (!observationActive) return;
observationActive = false;
if (observationEngine) {
observationEngine.stop();
}
if (observationAmbientTimer) {
clearTimeout(observationAmbientTimer);
observationAmbientTimer = null;
}
stopObservationLiveInstrumentation();
stopObservationSplineMorphs();
stopObservationScene();
if (observationChrome) {
observationChrome.style.transition = 'none';
observationChrome.classList.remove('observation-hud-fade');
void observationChrome.offsetWidth;
observationChrome.style.transition = '';
}
if (observationReturn) {
observationReturn.style.transition = 'none';
observationReturn.classList.remove('observation-return-fade');
void observationReturn.offsetWidth;
observationReturn.style.transition = '';
}
observationOverlay.classList.remove('active');
observationOverlay.setAttribute('aria-hidden', 'true');
observationStage.innerHTML = '';
observationEventLayer.innerHTML = '';
}
window.exitObservation = exitObservation;
window.refreshObservation = refreshObservation;
window.enterObservation = enterObservation;
window.selectPreset = selectPreset;
window.setUniverse = setUniverse;
btnObservation.addEventListener('click', (e) => {
e.stopPropagation();
enterObservation();
});
// Click background to exit, but ignore clicks on the control dock or its buttons
observationOverlay.addEventListener('click', (e) => {
if (e.target.closest('#observation-control-dock')) return;
e.preventDefault();
e.stopPropagation();
exitObservation();
});
const observationReturnPill = document.getElementById('observation-return-pill');
if (observationReturnPill) {
observationReturnPill.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
exitObservation();
});
}
// 10. Keyboard Shortcuts
window.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
if (e.code === 'Space') {
e.preventDefault();
togglePlay();
} else if (e.code === 'KeyM') {
toggleMute();
} else if (e.code === 'KeyW') {
if (observationActive && observationEngine) {
observationEngine.toggleWarpFlight();
return;
}
const primaryActionBtn = universeEventsContainer.children[0];
if (primaryActionBtn) primaryActionBtn.click();
} else if (e.code === 'KeyF') {
if (observationActive && observationEngine) {
observationEngine.toggleViewportFrame();
return;
}
} else if (e.code === 'KeyP') {
if (observationActive && observationEngine) {
observationEngine.togglePillars();
return;
}
} else if (e.code === 'KeyR') {
if (observationActive && observationEngine) {
observationEngine.toggleAlert();
return;
}
const firstAlertBtn = universeEventsContainer.children[1] || universeEventsContainer.children[0];
if (firstAlertBtn) firstAlertBtn.click();
} else if (e.code === 'ArrowUp' || e.code === 'Equal' || e.code === 'NumpadAdd') {
e.preventDefault();
setMasterVolume(audioManager.getMasterVolume() + 0.05);
} else if (e.code === 'ArrowDown' || e.code === 'Minus' || e.code === 'NumpadSubtract') {
e.preventDefault();
setMasterVolume(audioManager.getMasterVolume() - 0.05);
} else if (e.code >= 'Digit1' && e.code <= 'Digit9') {
const num = parseInt(e.code.replace('Digit', ''));
setMasterVolume(num * 0.1);
} else if (e.code === 'Digit0') {
setMasterVolume(1.0);
} else if (e.code === 'Escape') {
if (observationActive) {
exitObservation();
return;
}
alerts.stopAlert();
whoniverseAudio.stopCloisterBell();
resetAlertButtons();
}
});
// 11. Initial Setup
initUniverseDropdown();
setUniverse('starfleet');
updateMasterVolumeUI(0.75, false);
visualizer.init('spectrum-canvas', 'warp-core-canvas');
});