/* * fee[dB]ack v0.3.0 — Venue 3D scene bridge. * * Activates the highway_3d `venue` background style when Visualization = Venue. * Reacts to v3:live-performance-state for lighting mood (read-only). */ (function (root) { 'use strict'; const THEME_ID = 'small-club'; const ASSET_BASE = '/static/assets/venue/themes/small-club/'; const BG_PLATE = 'bg-plate.png'; const BG_PLATE_WEBP = 'bg-plate.webp'; let _active = false; let _assetsLoaded = false; let _loadFailed = false; let _lastMood = 'idle'; let _bound = false; // The venue belongs to the SONG player and nowhere else. // // isVenueViz() only answers "is Venue the selected visualization" — a global // preference. It says nothing about what is on screen. Other surfaces borrow // the same highway_3d renderer (Virtuoso runs its practice charts on it), so // with Venue selected they inherited the venue backdrop: the crowd and the // stage showed up behind a chromatic exercise. The viz picker is a // preference for the player; it is not a licence to paint the venue over // whatever else happens to be using the renderer. // // So gate on both: Venue selected AND the player screen is the one showing. function isPlayerScreen() { try { const active = document.querySelector('.screen.active'); return !!active && active.id === 'player'; } catch (_) { return false; } } function shouldBeActive() { return isVenueViz() && isPlayerScreen(); } function isVenueViz() { if (root && root.v3VenueViz && typeof root.v3VenueViz.isVenueVisualization === 'function') { const sel = root.v3VenueViz.getSelectedVizId ? root.v3VenueViz.getSelectedVizId() : root.v3VenueViz.readVizSelection(); return root.v3VenueViz.isVenueVisualization(sel); } try { const sel = document.getElementById('viz-picker'); if (sel && sel.value) return String(sel.value) === 'venue'; return localStorage.getItem('vizSelection') === 'venue'; } catch (_) { return false; } } function h3dApi(name) { return root && typeof root[name] === 'function' ? root[name] : null; } function setH3dActive(on) { const fn = h3dApi('h3dVenueSceneSetActive'); if (fn) fn(!!on); } function setH3dMood(state) { const fn = h3dApi('h3dVenueSceneSetMood'); if (fn) fn(state); } function readH3dState() { const fn = h3dApi('h3dVenueSceneGetState'); return fn ? fn() : null; } function syncPlaceholderVisibility() { try { if (root && root.v3VenueViz && typeof root.v3VenueViz.syncPlayerVizClass === 'function') { const id = root.v3VenueViz.getSelectedVizId ? root.v3VenueViz.getSelectedVizId() : root.v3VenueViz.readVizSelection(); root.v3VenueViz.syncPlayerVizClass(id); } } catch (_) { /* visual-only */ } try { if (root && root.v3VenueMoodFx && typeof root.v3VenueMoodFx.onVenueVisualizationSelected === 'function' && isVenueViz()) { root.v3VenueMoodFx.onVenueVisualizationSelected(); } } catch (_) { /* visual-only */ } } function readArrangementSignal() { // Intentional karaoke/vocals signal: active arrangement name from the // highway WS (user selected Vocals in #arr-select). Do NOT use // window.highway.getLyricsVisible() — lyrics overlay stays on during normal // guitar practice and must not force vocals POV. try { const si = root.highway && typeof root.highway.getSongInfo === 'function' ? root.highway.getSongInfo() : null; if (si && si.arrangement) return si.arrangement; const cs = root.feedBack && root.feedBack.currentSong; if (cs && cs.arrangement) return cs.arrangement; if (cs && cs.arrangementSmartName) return cs.arrangementSmartName; } catch (_) { /* visual-only */ } return ''; } function syncInstrumentPov() { const fn = h3dApi('h3dVenueSceneSetInstrumentPov'); if (fn) fn(readArrangementSignal()); } function activate() { if (_active) { syncInstrumentPov(); syncVenueMotion(); syncCrowd(true); return; } _active = true; _assetsLoaded = false; _loadFailed = false; setH3dActive(true); setH3dMood(_lastMood); syncInstrumentPov(); syncVenueMotion(); syncCrowd(true); } function syncCrowd(on) { // Reactive crowd video layer (career mode) — inert without a pack. try { if (root && root.v3VenueCrowd && typeof root.v3VenueCrowd.setVenueActive === 'function') { root.v3VenueCrowd.setVenueActive(!!on); } } catch (_) { /* visual-only */ } } function syncVenueMotion() { const motionApi = root && root.v3VenueMoodFx; const mode = motionApi && typeof motionApi.getMotion === 'function' ? motionApi.getMotion() : 'subtle'; const fn = h3dApi('h3dVenueSceneSetMotionMode'); if (fn) fn(mode); else if (motionApi && typeof motionApi.syncMotionToRenderer === 'function') { motionApi.syncMotionToRenderer(mode); } } function deactivate() { if (!_active) { setH3dActive(false); return; } _active = false; _assetsLoaded = false; _loadFailed = false; setH3dActive(false); syncCrowd(false); syncPlaceholderVisibility(); } function syncViz(vizId) { const id = String(vizId || ''); // Venue selected is necessary but not sufficient — see shouldBeActive. if (id === 'venue' && isPlayerScreen()) { activate(); } else { deactivate(); } } function onPerformanceState(e) { if (!_active) return; const d = (e && e.detail) || {}; const state = String(d.state || 'idle').toLowerCase(); // v3:live-performance-state fires per note hit/miss; skip the renderer // push when the mood is unchanged (e.g. a run of hits all in 'fire'). if (state === _lastMood) return; _lastMood = state; setH3dMood(state); } function onAssetsLoaded() { _assetsLoaded = true; _loadFailed = false; syncPlaceholderVisibility(); } function onAssetsFailed() { _loadFailed = true; _assetsLoaded = false; syncPlaceholderVisibility(); } function bindRuntime() { if (_bound) return; _bound = true; const sm = root && root.feedBack; if (sm && typeof sm.on === 'function') { sm.on('v3:live-performance-state', onPerformanceState); sm.on('song:loaded', () => { if (_active) syncInstrumentPov(); }); sm.on('arrangement:changed', () => { if (_active) syncInstrumentPov(); }); sm.on('song:arrangement-changed', () => { if (_active) syncInstrumentPov(); }); sm.on('viz:renderer:ready', () => { if (shouldBeActive()) activate(); else deactivate(); }); sm.on('viz:reverted', () => deactivate()); // Leaving the player tears the venue down; coming back rebuilds it. // Without this the backdrop followed the renderer onto every other // surface that borrows it (Virtuoso's practice highway). sm.on('screen:changed', () => { if (shouldBeActive()) activate(); else deactivate(); }); } if (shouldBeActive()) activate(); } function getState() { const h3d = readH3dState(); const povApi = root && root.v3VenueInstrumentPov; const arrangement = readArrangementSignal(); const instrumentPov = povApi && typeof povApi.resolveVenueInstrumentPov === 'function' ? povApi.resolveVenueInstrumentPov(arrangement) : 'guitar'; return { active: _active, themeId: THEME_ID, assetBase: ASSET_BASE, arrangement, instrumentPov, assetsLoaded: _assetsLoaded || !!(h3d && h3d.assetsLoaded), loadFailed: _loadFailed || !!(h3d && h3d.loadFailed), mood: _lastMood, h3dVenueState: h3d, isVenueViz: isVenueViz(), }; } function shouldShowDomPlaceholder() { // V2: no on-screen construction badge during Venue playback. return false; } const api = { THEME_ID, ASSET_BASE, BG_PLATE, BG_PLATE_WEBP, activate, deactivate, syncViz, isPlayerScreen, shouldBeActive, onAssetsLoaded, onAssetsFailed, onPerformanceState, bindRuntime, getState, syncInstrumentPov, syncVenueMotion, readArrangementSignal, shouldShowDomPlaceholder, isSceneLoaded: () => { if (_assetsLoaded) return true; const h3d = readH3dState(); return !!(h3d && h3d.assetsLoaded); }, }; if (root) root.v3VenueScene3d = api; if (typeof module !== 'undefined' && module.exports) module.exports = api; if (typeof document !== 'undefined') { const boot = () => bindRuntime(); // `defer` runs this at readyState 'interactive' — later scripts have not // evaluated yet, so wait for DOMContentLoaded (see static/v3/index.html). if (document.readyState !== 'complete') { document.addEventListener('DOMContentLoaded', boot); } else { boot(); } } }(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : null)));