generated from Labyricorn/labyricorn-project-template
Steps 6.4-6.7B — Local surfaces, reference-exhibit validation, SciFi Observation surface
One commit for the work accumulated in the working tree since Step 6.3, which had never been split into per-step commits: - src/local-surfaces.js + src/surface-url.js (new); src/ui.js, src/validation.js, src/connection.js and public/index.html updated for local-surface hosting and generic surface rendering - tests: local-surfaces (20), scifi-surfaces (24) and postmessage-interop (7) new; connection/museum-gallery/surface-validation suites updated - reference exhibits: shared/contract-core.js defaults to Contract 5.3 (major 5, minor 3, xzbt 5.3); museum-gallery advertises its surface catalog; aquarium/haunted-house/planetarium adapters updated - SciFi-XZBT (Step 6.7A/6.7B): surface-mode.js + surface-bus.js, Observation-surface boot branch, local-change hooks, view.pillars / view.warp-flight targets; fixture byte-identical to G:/.vibe/SciFi-XZBT - SciFi-XZBT contract adapter handshake fix: the inbound bridge filter no longer gates on an exact advisory xzbt value (Contract 5.3 §6.5), only on its presence/type, matching the host's own envelope validation; the adapter now advertises contract minor 3 / version 5.3.0, which it already implemented via the 5.3 surfaces field. Root cause of the five failing postmessage-interop tests (host hello was silently dropped). - docs: architecture 6.4 and 6.7A, reference 6.6 and 6.7; evidence logs; test-fixtures/PROVENANCE.md resync record Test results: NGN 154/154 (was 149/154); postmessage-interop 7/7 (was 2/7); SciFi contract harness 21/21, real-adapter suite 32/32. git diff --check clean for changed files; two pre-existing trailing-whitespace lines remain in test-fixtures/reference-exhibits/scifi/index.html, copied verbatim from the authoritative SciFi source. Step 6.7 live verification (browser Observation, packaged standalone) is still pending and is not claimed here.
This commit is contained in:
@@ -5,6 +5,20 @@
|
||||
*/
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Step 6.7B: boot-mode resolver (Contract 5.3 SS31.4 query-string surface
|
||||
// form). 'console' is today's application, byte-for-byte -- the resolver
|
||||
// returns 'console' for no query string and for any unrecognized
|
||||
// ?surface= value. 'observation' renders only the Observation viewscreen
|
||||
// and mirrors an authoritative console instance over an exhibit-internal
|
||||
// BroadcastChannel; it never constructs a second contract session, audio
|
||||
// graph or generative engine (see the isConsoleMode guards below).
|
||||
const xzbtSurface = XZBTSurfaceMode.resolve(window.location);
|
||||
const isConsoleMode = xzbtSurface.mode !== 'observation';
|
||||
const xzbtInstanceId = isConsoleMode ? XZBTSurfaceMode.newInstanceId() : xzbtSurface.instanceId;
|
||||
if (!isConsoleMode) document.body.classList.add('surface-observation');
|
||||
let surfaceOwner = null; // console mode: bus owner, feeds attached surfaces
|
||||
let surfaceLink = null; // observation mode: bus attachment to the console
|
||||
|
||||
// 1. Initialize Audio Subsystems
|
||||
const audioManager = new AudioManager();
|
||||
const fncSystem = new FuturisticNoiceCancellation(audioManager);
|
||||
@@ -21,7 +35,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.expandedAudio = expandedAudio;
|
||||
window.whataverseAudio = whataverseAudio;
|
||||
window.engineTransitions = engineTransitions;
|
||||
const visualizer = new StarshipVisualizer(audioManager, warpCore);
|
||||
// Step 6.7B: console-only. Its canvases (spectrum, warp core) are
|
||||
// console-panel elements; never construct it on the Observation surface.
|
||||
const visualizer = isConsoleMode ? new StarshipVisualizer(audioManager, warpCore) : null;
|
||||
const observationEngine = new ObservationEngine(audioManager, warpCore, alerts, hullDrone, lifeSupport);
|
||||
window.observationEngine = observationEngine;
|
||||
|
||||
@@ -97,34 +113,55 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const timerDisplay = document.getElementById('timer-display');
|
||||
const timerButtons = document.querySelectorAll('.btn-timer');
|
||||
|
||||
// XZBT Generative Experience subsystem. Deterministic engine remains authoritative.
|
||||
const generativeExperience = new XZBTGenerativeExperience({
|
||||
getUniverse: () => UniverseRegistry[activeUniverseId],
|
||||
getPreset: () => UniverseRegistry[activeUniverseId]?.presets?.[activePresetId] || null,
|
||||
isPlaying: () => isPlaying,
|
||||
isObservation: () => observationActive,
|
||||
audioManager,
|
||||
onGenerated: (line, state) => {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker && observationActive) ticker.textContent = line.toUpperCase();
|
||||
window.dispatchEvent(new CustomEvent('xzbt:generated-content', { detail: { line, state } }));
|
||||
}
|
||||
});
|
||||
generativeExperience.bindUI();
|
||||
window.generativeExperience = generativeExperience;
|
||||
// XZBT Generative Experience subsystem. Deterministic engine remains
|
||||
// authoritative. Step 6.7B: console-only -- WebLLM/Kokoro must never be
|
||||
// constructed or prepared on the Observation surface (highest-risk guard
|
||||
// in this design; see the isConsoleMode guard on prepareExperience() in
|
||||
// enterObservation() below).
|
||||
let generativeExperience = null;
|
||||
if (isConsoleMode) {
|
||||
generativeExperience = new XZBTGenerativeExperience({
|
||||
getUniverse: () => UniverseRegistry[activeUniverseId],
|
||||
getPreset: () => UniverseRegistry[activeUniverseId]?.presets?.[activePresetId] || null,
|
||||
isPlaying: () => isPlaying,
|
||||
isObservation: () => observationActive,
|
||||
audioManager,
|
||||
onGenerated: (line, state) => {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
const tickerText = line.toUpperCase();
|
||||
if (ticker && observationActive) ticker.textContent = tickerText;
|
||||
// Mirror the announcement to any attached Observation surface(s):
|
||||
// the AI only verbalizes state the authority already decided, so
|
||||
// this is presentation-only text, not contract state (report SS6).
|
||||
if (surfaceOwner) surfaceOwner.broadcastPresentation('ticker', { tickerText });
|
||||
window.dispatchEvent(new CustomEvent('xzbt:generated-content', { detail: { line, state } }));
|
||||
}
|
||||
});
|
||||
generativeExperience.bindUI();
|
||||
window.generativeExperience = generativeExperience;
|
||||
}
|
||||
|
||||
// Forward-declare absolute state setters & helpers for Contract Adapter
|
||||
let setMasterVolumeDirect, setPlayingDirect, setMutedDirect, setFncEnabledDirect;
|
||||
let setObservationDirect, setViewportFrameDirect, setAlertActiveDirect;
|
||||
let setUniverseDirect, selectPresetDirect, getContractStateSnapshot;
|
||||
// Step 6.7B: console-only. Never constructed on the Observation surface --
|
||||
// this is what makes "no second contract session" a structural fact.
|
||||
let controlBus = null;
|
||||
let contractAdapter = null;
|
||||
|
||||
// Semantic control bus & XZBT Contract Adapter
|
||||
const controlBus = new XZBTControlBus();
|
||||
// Semantic control bus & XZBT Contract Adapter. Step 6.7B: constructed
|
||||
// only in console mode -- see the isConsoleMode guard closing after
|
||||
// generativeExperience.onSpeechStateChanged below.
|
||||
if (isConsoleMode) {
|
||||
controlBus = new XZBTControlBus();
|
||||
window.xzbtControlBus = controlBus;
|
||||
|
||||
const contractAdapter = new XZBTContractAdapter({
|
||||
contractAdapter = new XZBTContractAdapter({
|
||||
product: 'SciFi-XZBT',
|
||||
version: '5.2.0',
|
||||
version: '5.3.0',
|
||||
surfaces: XZBTSurfaceMode.SURFACES(xzbtInstanceId),
|
||||
instanceId: xzbtInstanceId,
|
||||
bindings: {
|
||||
getUniverse: () => activeUniverseId,
|
||||
getPreset: () => activePresetId,
|
||||
@@ -203,7 +240,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'fnc.enabled': (v, src) => setFncEnabledDirect(v, src),
|
||||
'view.observation': (v, src) => setObservationDirect(v, src),
|
||||
'view.viewport-frame': (v, src) => setViewportFrameDirect(v, src),
|
||||
'alert.active': (v, src) => setAlertActiveDirect(v, src)
|
||||
'alert.active': (v, src) => setAlertActiveDirect(v, src),
|
||||
// Step 6.7B: newly-canonical targets. observation-engine.js keeps
|
||||
// setPillars(); warp-flight has no engine-side setter (only
|
||||
// toggleWarpFlight()), so the desired boolean is compared against
|
||||
// the engine's current flightMode before toggling -- this keeps
|
||||
// observation-engine.js's own simulation code untouched.
|
||||
'view.pillars': v => {
|
||||
if (observationEngine) observationEngine.setPillars(!!v);
|
||||
},
|
||||
'view.warp-flight': v => {
|
||||
if (!observationEngine) return;
|
||||
const want = !!v;
|
||||
const isWarp = observationEngine.flightMode === 'warp';
|
||||
if (want !== isWarp) observationEngine.toggleWarpFlight();
|
||||
}
|
||||
},
|
||||
invokers: {
|
||||
'speech.say': async args => {
|
||||
@@ -252,43 +303,130 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
window.xzbtContractAdapter = contractAdapter;
|
||||
fncSystem.onMutation = (id, value) => contractAdapter.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onMutation = (id, value) => contractAdapter.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onInvoke = id => contractAdapter.invokeAction(id, {}, 'ui');
|
||||
generativeExperience.onSpeechStateChanged = state => contractAdapter.updateCapability('speech', state);
|
||||
observationEngine.onMutation = (id, value) => contractAdapter.applyMutation(id, value, 'ui');
|
||||
fncSystem.onMutation = (id, value) => contractAdapter?.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onMutation = (id, value) => contractAdapter?.applyMutation(id, value, 'ui');
|
||||
generativeExperience.onInvoke = id => contractAdapter?.invokeAction(id, {}, 'ui');
|
||||
generativeExperience.onSpeechStateChanged = state => contractAdapter?.updateCapability('speech', state);
|
||||
|
||||
// Step 6.7B: exhibit-internal surface-attachment bus, owner side. Fed
|
||||
// from the adapter's session-independent onLocalChange/onLocalAction
|
||||
// hooks (contract-adapter.js SS5.4) rather than contract events, which are
|
||||
// gated on an NGN session and would go silent standalone.
|
||||
surfaceOwner = XZBTSurfaceBus.createOwner(contractAdapter, {
|
||||
channelName: XZBTSurfaceMode.channelName(xzbtInstanceId),
|
||||
// A surface attaching/detaching can flip observationAudienceActive();
|
||||
// re-evaluate the scheduler so a pane opened with the console overlay
|
||||
// off (or the last pane closing with it still off) starts/stops the
|
||||
// ambient generator correctly (report SS6, SS8's symmetric detach case).
|
||||
onParticipantsChanged: () => scheduleObservationAmbientActivity()
|
||||
});
|
||||
contractAdapter.onLocalChange = (target, value, stateRevision) => {
|
||||
surfaceOwner.broadcastState(target, value, stateRevision);
|
||||
};
|
||||
contractAdapter.onLocalAction = (target, args) => {
|
||||
surfaceOwner.broadcastAction(target, args);
|
||||
};
|
||||
} else {
|
||||
// Step 6.7B: Observation surface (non-authoritative). Never constructs
|
||||
// a contract session, control bus, or generative engine; attaches to
|
||||
// the console instance named by ?xi= and mirrors its state over the
|
||||
// exhibit-internal bus. `applyPresentationState` is defined further
|
||||
// down (function declarations hoist; these callbacks only actually run
|
||||
// once a message arrives, long after the rest of this script has run).
|
||||
surfaceLink = XZBTSurfaceBus.attach({
|
||||
channelName: XZBTSurfaceMode.channelName(xzbtInstanceId),
|
||||
timeoutMs: 1500,
|
||||
onSnapshot: (values, stateRevision, registryRevision, presentation) => {
|
||||
Object.keys(values || {}).forEach(target => applyPresentationState(target, values[target]));
|
||||
if (presentation && typeof presentation.tickerText === 'string' && presentation.tickerText) {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker) ticker.textContent = presentation.tickerText;
|
||||
}
|
||||
// The surface renders the scene unconditionally while open -- no
|
||||
// view.observation gating (report SS8: surface existence and the
|
||||
// console's own overlay flag are deliberately separate concepts).
|
||||
enterObservation();
|
||||
},
|
||||
onChange: (target, value) => applyPresentationState(target, value),
|
||||
onAction: (target, args) => {
|
||||
if (target === 'display.ticker' && args && typeof args.text === 'string') {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker) ticker.textContent = args.text;
|
||||
}
|
||||
},
|
||||
onPresentation: (kind, payload) => {
|
||||
if (kind === 'ticker' && typeof payload.tickerText === 'string') {
|
||||
const ticker = document.getElementById('observation-ticker-text');
|
||||
if (ticker) ticker.textContent = payload.tickerText;
|
||||
} else if (kind === 'obs-activity') {
|
||||
// Render the authority's decision; never independently generate
|
||||
// one (report SS6's "authority decides, surface renders").
|
||||
triggerObservationActivity(payload.source || 'ambient');
|
||||
}
|
||||
},
|
||||
onTimeout: () => {
|
||||
// No console instance open on this channel (or a stale/unknown
|
||||
// ?xi=). Degrade to a clear waiting state rather than inventing
|
||||
// local authority (report SS9, Museum's proven behavior).
|
||||
console.warn('[SciFi-XZBT] Observation surface: no console instance found for xi=' + xzbtInstanceId);
|
||||
}
|
||||
});
|
||||
|
||||
window.__xzbtSurfaceDispose = () => { if (surfaceLink) surfaceLink.detach(); };
|
||||
window.addEventListener('beforeunload', () => { if (surfaceLink) surfaceLink.detach(); });
|
||||
} // end isConsoleMode / else observation-surface attach (control bus, contract adapter, surface bus)
|
||||
|
||||
// Step 6.7B: the one dock-interaction chokepoint (already true before this
|
||||
// step for every control except PILLARS/WARP, now including them). Console
|
||||
// mode routes into the one mutation chokepoint as before; the Observation
|
||||
// surface routes the same five interactive controls to the console over the
|
||||
// bus instead, and drops view.observation (RETURN) locally -- posting it
|
||||
// would toggle the *console's* own overlay, a cross-surface side effect
|
||||
// the design explicitly forbids (report SS8). RETURN/exit are additionally
|
||||
// hidden by CSS (body.surface-observation) as the primary defense.
|
||||
const XZBT_PRESENTATION_ROUTED_TARGETS = new Set([
|
||||
'view.warp-flight', 'view.viewport-frame', 'view.pillars', 'alert.active', 'preset.selected'
|
||||
]);
|
||||
observationEngine.onMutation = isConsoleMode
|
||||
? (id, value) => contractAdapter?.applyMutation(id, value, 'ui')
|
||||
: (id, value) => {
|
||||
if (!XZBT_PRESENTATION_ROUTED_TARGETS.has(id)) return;
|
||||
if (surfaceLink) surfaceLink.mutate('set', id, value);
|
||||
};
|
||||
|
||||
// Step 6.7B: console-only (controlBus/contractAdapter don't exist otherwise).
|
||||
if (isConsoleMode) {
|
||||
// Intercept XZBTControlBus calls to route through contractAdapter
|
||||
const origBusSet = controlBus.set.bind(controlBus);
|
||||
const origBusTrigger = controlBus.trigger.bind(controlBus);
|
||||
controlBus.set = function (id, value, source = 'external') {
|
||||
if (contractAdapter.legacyIdMap.has(id) || contractAdapter.targets.has(id)) {
|
||||
const canonicalId = contractAdapter.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter.targets.get(canonicalId);
|
||||
if (contractAdapter?.legacyIdMap.has(id) || contractAdapter?.targets.has(id)) {
|
||||
const canonicalId = contractAdapter?.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter?.targets.get(canonicalId);
|
||||
let normVal = value;
|
||||
if (target && target.kind === 'range') {
|
||||
if (contractAdapter.legacyIdMap.has(id)) {
|
||||
if (contractAdapter?.legacyIdMap.has(id)) {
|
||||
const legacy = controlBus.targets.get(id);
|
||||
if (legacy?.step) normVal = Math.round(Number(value) / legacy.step) * legacy.step;
|
||||
if (canonicalId === 'view.activity') normVal = Number(normVal) / 100;
|
||||
}
|
||||
}
|
||||
contractAdapter.applyMutation(canonicalId, normVal, source);
|
||||
contractAdapter?.applyMutation(canonicalId, normVal, source);
|
||||
return true;
|
||||
}
|
||||
return origBusSet(id, value, source);
|
||||
};
|
||||
controlBus.trigger = function (id, source = 'external') {
|
||||
if (contractAdapter.legacyIdMap.has(id) || contractAdapter.targets.has(id)) {
|
||||
const canonicalId = contractAdapter.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter.targets.get(canonicalId);
|
||||
if (contractAdapter?.legacyIdMap.has(id) || contractAdapter?.targets.has(id)) {
|
||||
const canonicalId = contractAdapter?.legacyIdMap.get(id) || id;
|
||||
const target = contractAdapter?.targets.get(canonicalId);
|
||||
if (target && target.kind === 'state') {
|
||||
// Toggle state if boolean
|
||||
const curr = contractAdapter.getContractState()[canonicalId];
|
||||
contractAdapter.applyMutation(canonicalId, !curr, source);
|
||||
const curr = contractAdapter?.getContractState()[canonicalId];
|
||||
contractAdapter?.applyMutation(canonicalId, !curr, source);
|
||||
return true;
|
||||
}
|
||||
contractAdapter.invokeAction(canonicalId, {}, source);
|
||||
contractAdapter?.invokeAction(canonicalId, {}, source);
|
||||
return true;
|
||||
}
|
||||
return origBusTrigger(id, source);
|
||||
@@ -331,6 +469,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
controlBus.register('red-alert', { label: 'Red Alert Toggle', type: 'action' });
|
||||
controlBus.register('generate-announcement', { label: 'Generate AI Announcement', type: 'action' });
|
||||
controlBus.register('mute', { label: 'Master Mute', type: 'action' });
|
||||
} // end isConsoleMode (control bus semantic registration)
|
||||
|
||||
|
||||
// TV presentation controls. The final Cast stream quality remains Chrome-controlled.
|
||||
@@ -365,7 +504,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btn.innerHTML = `<span class="u-icon">${u.icon}</span> <span>${u.name}</span>`;
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('universe.selected', u.id, 'ui');
|
||||
contractAdapter?.applyMutation('universe.selected', u.id, 'ui');
|
||||
});
|
||||
universeDropdown.appendChild(btn);
|
||||
});
|
||||
@@ -399,6 +538,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.className = universe.themeClass;
|
||||
// Assigning className wholesale drops the TV presentation classes, so re-apply them.
|
||||
applyTV();
|
||||
// ...and the Step 6.7B surface-mode class, for the same reason.
|
||||
if (!isConsoleMode) document.body.classList.add('surface-observation');
|
||||
headerMatrixLabel.textContent = universe.headerTitle;
|
||||
|
||||
// Custom titles per universe
|
||||
@@ -472,7 +613,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
soundboardTitle.textContent = conf.soundboard;
|
||||
|
||||
// Configure Visualizer mode
|
||||
visualizer.setMode(universe.visualizer || 'warp-core');
|
||||
visualizer?.setMode(universe.visualizer || 'warp-core');
|
||||
|
||||
// Render Event Buttons & Soundboard
|
||||
renderUniverseEvents(universe);
|
||||
@@ -509,10 +650,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
async function invokeConsoleEvent(evt, btn, source) {
|
||||
if (evt.type === 'alert-red' || evt.type === 'alert-yellow') {
|
||||
const value = evt.type === 'alert-red' ? 'red' : 'yellow';
|
||||
return contractAdapter.applyMutation('alert.active', alerts.activeAlert === value ? 'none' : value, source);
|
||||
return contractAdapter?.applyMutation('alert.active', alerts.activeAlert === value ? 'none' : value, source);
|
||||
}
|
||||
const canonical = [...contractAdapter.targets.values()].find(t => t.kind === 'impulse' && t.id.startsWith('event.') && (t.id === `event.${evt.type}` || `btn-${t.id.slice(6)}` === evt.id));
|
||||
if (canonical) return contractAdapter.invokeAction(canonical.id, {}, source);
|
||||
const canonical = [...contractAdapter?.targets.values()].find(t => t.kind === 'impulse' && t.id.startsWith('event.') && (t.id === `event.${evt.type}` || `btn-${t.id.slice(6)}` === evt.id));
|
||||
if (canonical) return contractAdapter?.invokeAction(canonical.id, {}, source);
|
||||
await audioManager.resume();
|
||||
handleUniverseEvent(evt.type, btn);
|
||||
}
|
||||
@@ -531,7 +672,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const aliases = { 'btn-chirp-single': 'sfx.console-chirp', 'btn-telepathic': 'sfx.telepathic-chime', 'btn-commlock': 'sfx.comlock', 'btn-afterburner-snd': 'sfx.afterburner' };
|
||||
const canonical = aliases[key.id] || `sfx.${key.id.replace(/^btn-/, '')}`;
|
||||
if (contractAdapter.targets.has(canonical)) return contractAdapter.invokeAction(canonical, {}, 'ui');
|
||||
if (contractAdapter?.targets.has(canonical)) return contractAdapter?.invokeAction(canonical, {}, 'ui');
|
||||
await audioManager.resume();
|
||||
handleSoundboardTrigger(key.id);
|
||||
});
|
||||
@@ -553,7 +694,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
`;
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('preset.selected', preset.id, 'ui');
|
||||
contractAdapter?.applyMutation('preset.selected', preset.id, 'ui');
|
||||
});
|
||||
|
||||
presetContainer.appendChild(btn);
|
||||
@@ -763,7 +904,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
btnPlay.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('transport.playing', !isPlaying, 'ui');
|
||||
contractAdapter?.applyMutation('transport.playing', !isPlaying, 'ui');
|
||||
});
|
||||
|
||||
// 6. Master Volume Control System
|
||||
@@ -800,65 +941,65 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
function setMasterVolume(val) {
|
||||
contractAdapter.applyMutation('mix.master', val, 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', val, 'ui');
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
contractAdapter.applyMutation('transport.muted', !audioManager.isMuted, 'ui');
|
||||
contractAdapter?.applyMutation('transport.muted', !audioManager.isMuted, 'ui');
|
||||
}
|
||||
|
||||
sliderMasterVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.master', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
btnMasterMute.addEventListener('click', toggleMute);
|
||||
btnHeaderMute.addEventListener('click', toggleMute);
|
||||
btnVolDown.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'ui');
|
||||
});
|
||||
btnVolUp.addEventListener('click', () => {
|
||||
contractAdapter.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'ui');
|
||||
});
|
||||
|
||||
volStepButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const stepVal = parseFloat(btn.getAttribute('data-vol'));
|
||||
contractAdapter.applyMutation('mix.master', stepVal, 'ui');
|
||||
contractAdapter?.applyMutation('mix.master', stepVal, 'ui');
|
||||
});
|
||||
});
|
||||
|
||||
// 7. Channel Synthesizer Sliders
|
||||
sliderHullVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.hull.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.hull.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderHullFreq.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.hull.frequency', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.hull.frequency', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderHullCutoff.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.hull.cutoff', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.hull.cutoff', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
sliderWarpVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.drive.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.drive.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderWarpBpm.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.drive.pulse-rate', parseInt(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.drive.pulse-rate', parseInt(e.target.value), 'ui');
|
||||
});
|
||||
sliderWarpCarrier.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.drive.carrier', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.drive.carrier', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
sliderAirVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.environment.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.environment.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderAirCutoff.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.environment.cutoff', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.environment.cutoff', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
sliderTelemetryVol.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.telemetry.level', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.telemetry.level', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
sliderTelemetryDensity.addEventListener('input', (e) => {
|
||||
contractAdapter.applyMutation('mix.telemetry.density', parseFloat(e.target.value), 'ui');
|
||||
contractAdapter?.applyMutation('mix.telemetry.density', parseFloat(e.target.value), 'ui');
|
||||
});
|
||||
|
||||
// 8. Universe Event Handlers
|
||||
@@ -872,7 +1013,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'warp':
|
||||
case 'quantum':
|
||||
alerts.synthesizeWarpJump();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'alert-red':
|
||||
case 'action-stations':
|
||||
@@ -902,11 +1043,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
break;
|
||||
case 'demat':
|
||||
whataverseAudio.synthesizeDematCycle(4);
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'vortex':
|
||||
whataverseAudio.synthesizeDematCycle(1);
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'cloister':
|
||||
if (whataverseAudio.activeCloister) {
|
||||
@@ -923,7 +1064,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'epstein':
|
||||
case 'subdrive':
|
||||
expandedAudio.synthesizeEpsteinBurn();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'purge':
|
||||
case 'decompress':
|
||||
@@ -936,11 +1077,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'starburst':
|
||||
case 'biodefense':
|
||||
expandedAudio.synthesizeStarburst();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'neural':
|
||||
expandedAudio.synthesizeNeuralBondSwell();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'centrifuge':
|
||||
case 'astrogator':
|
||||
@@ -951,7 +1092,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
break;
|
||||
case 'ftl-jump':
|
||||
expandedAudio.synthesizeFtlJump();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'flak':
|
||||
expandedAudio.synthesizeFlakBarrageBurst();
|
||||
@@ -960,29 +1101,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
case 'gravity':
|
||||
case 'rotation':
|
||||
expandedAudio.synthesizeSingularityEngage();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'solar':
|
||||
expandedAudio.synthesizeSolarRoar();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'afterburner':
|
||||
expandedAudio.synthesizeAfterburner();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'ludicrous':
|
||||
expandedAudio.synthesizeLudicrousSpeed();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
case 'improbability':
|
||||
expandedAudio.synthesizeImprobabilityFlip();
|
||||
visualizer.spawnWarpPulses();
|
||||
visualizer?.spawnWarpPulses();
|
||||
break;
|
||||
default:
|
||||
telemetry.synthesizeLCARDSingleChirp();
|
||||
break;
|
||||
}
|
||||
generativeExperience.describeExistingEvent(type);
|
||||
generativeExperience?.describeExistingEvent(type);
|
||||
}
|
||||
|
||||
function handleSoundboardTrigger(id) {
|
||||
@@ -2675,8 +2816,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return spawnObservationTransient(obsExpandingRing(obsInt(450,1150),obsInt(250,650),obsPick(['#06b6d4','#f43f5e','#fbbf24']),obsInt(45,100)),3500);
|
||||
}
|
||||
|
||||
// Step 6.7B: an "audience" is the console's own overlay OR at least one
|
||||
// attached Observation surface. Scheduling/deciding activity uses this;
|
||||
// *local rendering* still gates on observationActive alone (report SS6's
|
||||
// audience-scheduler correction -- without it, an audience watching only
|
||||
// the surface would see a static scene forever).
|
||||
const observationAudienceActive = () => observationActive || !!(surfaceOwner && surfaceOwner.attachedCount() > 0);
|
||||
|
||||
function triggerObservationActivity(source = 'ambient') {
|
||||
if (!observationActive || observationActivity <= 0.001) return;
|
||||
if (!observationAudienceActive() || observationActivity <= 0.001) return;
|
||||
// The authority decides; attached surfaces render the same decision
|
||||
// rather than independently rolling their own random event (report SS6,
|
||||
// "Transient activity events are mirrored, not regenerated"). Exact
|
||||
// visual parameters (which contact, which ring) are NOT byte-identical
|
||||
// across documents -- see the accepted-limitations note in README.md /
|
||||
// agents.md; only the "an event happens now" decision is synchronized.
|
||||
if (isConsoleMode && surfaceOwner) surfaceOwner.broadcastPresentation('obs-activity', { source });
|
||||
|
||||
// Telemetry is a synchronization pulse, but the slider controls whether
|
||||
// that pulse becomes visible. At maximum every telemetry pulse produces
|
||||
@@ -2713,7 +2868,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
clearTimeout(observationAmbientTimer);
|
||||
observationAmbientTimer = null;
|
||||
}
|
||||
if (!observationActive || observationActivity <= .01) return;
|
||||
// The Observation surface never runs its own scheduler -- it only
|
||||
// renders 'obs-activity' decisions relayed from the authority. Without
|
||||
// this guard two documents would each roll independent random timers,
|
||||
// violating "no duplicate random event generation" (report SS6/R3).
|
||||
if (!isConsoleMode) return;
|
||||
if (!observationAudienceActive() || observationActivity <= .01) return;
|
||||
|
||||
// Quiet = occasional subtle life. Active = a busy generative display.
|
||||
const slow = 12500;
|
||||
@@ -2798,7 +2958,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateObservationHudMode();
|
||||
|
||||
sliderObservationActivity.addEventListener('input', () => {
|
||||
contractAdapter.applyMutation('view.activity', Number(sliderObservationActivity.value) / 100, 'ui');
|
||||
contractAdapter?.applyMutation('view.activity', Number(sliderObservationActivity.value) / 100, 'ui');
|
||||
});
|
||||
|
||||
window.addEventListener('scifi-telemetry-activity', (event) => {
|
||||
@@ -4419,6 +4579,48 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6.7B: applies one mirrored contract-state value on the
|
||||
// Observation surface. Reuses the existing local setters/functions
|
||||
// directly -- never contractAdapter.applyMutation (there is none here) --
|
||||
// so this is local *rendering*, not a second computation of authority.
|
||||
// Targets the renderer doesn't care about (mix.*, fnc.*, transport.muted,
|
||||
// speech.*) are intentionally ignored.
|
||||
function applyPresentationState(target, value) {
|
||||
switch (target) {
|
||||
case 'universe.selected':
|
||||
if (value && value !== activeUniverseId) setUniverse(value);
|
||||
break;
|
||||
case 'preset.selected':
|
||||
if (value && value !== activePresetId) selectPreset(value);
|
||||
break;
|
||||
case 'view.viewport-frame':
|
||||
if (observationEngine) observationEngine.setViewport(!!value);
|
||||
break;
|
||||
case 'view.pillars':
|
||||
if (observationEngine) observationEngine.setPillars(!!value);
|
||||
break;
|
||||
case 'view.warp-flight': {
|
||||
if (!observationEngine) break;
|
||||
const isWarp = observationEngine.flightMode === 'warp';
|
||||
if (!!value !== isWarp) observationEngine.toggleWarpFlight();
|
||||
break;
|
||||
}
|
||||
case 'alert.active':
|
||||
if (typeof setAlertActiveDirect === 'function') setAlertActiveDirect(value);
|
||||
break;
|
||||
case 'view.activity':
|
||||
setObservationActivity(Number(value) * 100);
|
||||
break;
|
||||
case 'transport.playing':
|
||||
// Cosmetic only (the "PROCEDURAL AUDIO LINK" status line) -- never
|
||||
// engages audio on the surface.
|
||||
isPlaying = !!value;
|
||||
break;
|
||||
default:
|
||||
break; // view.observation, mix.*, fnc.*, speech.*, transport.muted: not rendered here.
|
||||
}
|
||||
}
|
||||
|
||||
function refreshObservation() {
|
||||
const universe = UniverseRegistry[activeUniverseId];
|
||||
const preset = universe && universe.presets ? universe.presets[activePresetId] : null;
|
||||
@@ -4442,18 +4644,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
function enterObservation() {
|
||||
if (observationActive) return;
|
||||
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||
// Step 6.7B / R2: the single most important guard in this design.
|
||||
// prepareExperience() lazily loads WebLLM + Kokoro (agents.md's one
|
||||
// documented network/import exception); calling it unconditionally
|
||||
// here would make every opened Observation surface download a
|
||||
// multi-gigabyte model. Console-only.
|
||||
if (isConsoleMode) {
|
||||
generativeExperience.prepareExperience().catch(err => console.warn('Generative preload failed', err));
|
||||
}
|
||||
observationActive = true;
|
||||
observationOverlay.classList.add('active');
|
||||
observationOverlay.setAttribute('aria-hidden', 'false');
|
||||
refreshObservation();
|
||||
resetObservationHudFade();
|
||||
resetObservationReturnFade();
|
||||
scheduleObservationAmbientActivity();
|
||||
if (isConsoleMode) {
|
||||
// The Observation surface never runs its own scheduler -- it renders
|
||||
// activity decisions broadcast by the authority (report SS6).
|
||||
scheduleObservationAmbientActivity();
|
||||
|
||||
// Give the display an immediate but restrained sign of life.
|
||||
if (observationActivity > .08) {
|
||||
window.setTimeout(() => triggerObservationActivity('ambient'), 700);
|
||||
// Give the display an immediate but restrained sign of life.
|
||||
if (observationActivity > .08) {
|
||||
window.setTimeout(() => triggerObservationActivity('ambient'), 700);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4502,7 +4715,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
btnObservation.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('view.observation', !observationActive, 'ui');
|
||||
contractAdapter?.applyMutation('view.observation', !observationActive, 'ui');
|
||||
});
|
||||
|
||||
// Click background to exit, but ignore clicks on the control dock or its buttons
|
||||
@@ -4510,7 +4723,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (e.target.closest('#observation-control-dock')) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('view.observation', false, 'ui');
|
||||
contractAdapter?.applyMutation('view.observation', false, 'ui');
|
||||
});
|
||||
|
||||
const observationReturnPill = document.getElementById('observation-return-pill');
|
||||
@@ -4518,19 +4731,21 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
observationReturnPill.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contractAdapter.applyMutation('view.observation', false, 'ui');
|
||||
contractAdapter?.applyMutation('view.observation', false, 'ui');
|
||||
});
|
||||
}
|
||||
|
||||
// 10. Keyboard Shortcuts
|
||||
// 10. Keyboard Shortcuts. Step 6.7B: console-only -- a keystroke on the
|
||||
// Observation surface must not mutate state (report SS7).
|
||||
if (isConsoleMode) {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
|
||||
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
contractAdapter.applyMutation('transport.playing', !isPlaying, 'hotkey');
|
||||
contractAdapter?.applyMutation('transport.playing', !isPlaying, 'hotkey');
|
||||
} else if (e.code === 'KeyM') {
|
||||
contractAdapter.applyMutation('transport.muted', !audioManager.isMuted, 'hotkey');
|
||||
contractAdapter?.applyMutation('transport.muted', !audioManager.isMuted, 'hotkey');
|
||||
} else if (e.code === 'KeyW') {
|
||||
if (observationActive && observationEngine) {
|
||||
observationEngine.toggleWarpFlight();
|
||||
@@ -4541,7 +4756,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (primaryEvent) invokeConsoleEvent(primaryEvent, primaryActionBtn, 'hotkey');
|
||||
} else if (e.code === 'KeyF') {
|
||||
if (observationActive && observationEngine) {
|
||||
contractAdapter.applyMutation('view.viewport-frame', !observationEngine.showViewport, 'hotkey');
|
||||
contractAdapter?.applyMutation('view.viewport-frame', !observationEngine.showViewport, 'hotkey');
|
||||
return;
|
||||
}
|
||||
} else if (e.code === 'KeyP') {
|
||||
@@ -4552,7 +4767,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
} else if (e.code === 'KeyR') {
|
||||
if (observationActive && observationEngine) {
|
||||
const nextAlert = alerts.activeAlert === 'red' ? 'none' : 'red';
|
||||
contractAdapter.applyMutation('alert.active', nextAlert, 'hotkey');
|
||||
contractAdapter?.applyMutation('alert.active', nextAlert, 'hotkey');
|
||||
return;
|
||||
}
|
||||
const firstAlertBtn = universeEventsContainer.children[1] || universeEventsContainer.children[0];
|
||||
@@ -4561,23 +4776,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (alertEvent) invokeConsoleEvent(alertEvent, firstAlertBtn, 'hotkey');
|
||||
} else if (e.code === 'ArrowUp' || e.code === 'Equal' || e.code === 'NumpadAdd') {
|
||||
e.preventDefault();
|
||||
contractAdapter.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', Math.min(1, audioManager.getMasterVolume() + 0.05), 'hotkey');
|
||||
} else if (e.code === 'ArrowDown' || e.code === 'Minus' || e.code === 'NumpadSubtract') {
|
||||
e.preventDefault();
|
||||
contractAdapter.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', Math.max(0, audioManager.getMasterVolume() - 0.05), 'hotkey');
|
||||
} else if (e.code >= 'Digit1' && e.code <= 'Digit9') {
|
||||
const num = parseInt(e.code.replace('Digit', ''));
|
||||
contractAdapter.applyMutation('mix.master', num * 0.1, 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', num * 0.1, 'hotkey');
|
||||
} else if (e.code === 'Digit0') {
|
||||
contractAdapter.applyMutation('mix.master', 1.0, 'hotkey');
|
||||
contractAdapter?.applyMutation('mix.master', 1.0, 'hotkey');
|
||||
} else if (e.code === 'Escape') {
|
||||
if (observationActive) {
|
||||
contractAdapter.applyMutation('view.observation', false, 'hotkey');
|
||||
contractAdapter?.applyMutation('view.observation', false, 'hotkey');
|
||||
return;
|
||||
}
|
||||
contractAdapter.applyMutation('alert.active', 'none', 'hotkey');
|
||||
contractAdapter?.applyMutation('alert.active', 'none', 'hotkey');
|
||||
}
|
||||
});
|
||||
} // end isConsoleMode (keyboard shortcuts)
|
||||
|
||||
// --- Implementation of Absolute Setters and Contract Helpers ---
|
||||
setMasterVolumeDirect = function (val, isMuted = false) {
|
||||
@@ -4688,6 +4904,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
'fnc.level': fncSystem ? fncSystem.level : 0.5,
|
||||
'view.observation': observationActive,
|
||||
'view.viewport-frame': observationEngine ? observationEngine.showViewport : true,
|
||||
'view.pillars': observationEngine ? observationEngine.showPillars : false,
|
||||
'view.warp-flight': observationEngine ? observationEngine.flightMode === 'warp' : false,
|
||||
'view.activity': observationActivity,
|
||||
'alert.active': alertStatus,
|
||||
'universe.selected': activeUniverseId,
|
||||
@@ -4699,5 +4917,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
initUniverseDropdown();
|
||||
setUniverse('starfleet');
|
||||
updateMasterVolumeUI(0.75, false);
|
||||
visualizer.init('spectrum-canvas', 'warp-core-canvas');
|
||||
visualizer?.init('spectrum-canvas', 'warp-core-canvas');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user