/** * 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); }); } // 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(); if (isPlaying) { if (presetId === 'sta-sevastopol') { expandedAudio.startStationSparks(4000, 11000); } else if (presetId === 'sta-gateway' || presetId.includes('sickbay') || presetId.includes('med')) { expandedAudio.startMedicalMonitor(1.15); } 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 === 'sta-sevastopol') { expandedAudio.startStationSparks(4000, 11000); } else if (activePresetId === 'sta-gateway' || activePresetId.includes('sickbay') || activePresetId.includes('med')) { expandedAudio.startMedicalMonitor(1.15); } }, 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(); 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': whoniverseAudio.synthesizeTelepathicChime(); break; case 'centrifuge': case 'astrogator': expandedAudio.synthesizeRetroAstrogator(); break; case 'hal': expandedAudio.synthesizeHalChime(); break; case 'ftl-jump': case 'flak': expandedAudio.synthesizeFtlJump(); visualizer.spawnWarpPulses(); 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; 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 ` ${defs} ${inner} `; } 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'); });