// @ts-nocheck — Module entry point with FoundryVTT globals, no exports needed /** * module.js — Entry point and wiring diagram for Video View Manager (Scrying Pool). * * This file is the wiring diagram ONLY. It imports all modules, constructs them * with injected dependencies, and holds NO business logic. * * Initialisation order: * Hooks.once('init') → register world settings → construct FoundryAdapter * → StateStore → SocketHandler (queue+drain) * Hooks.once('ready') → hydrate StateStore → probe WebRTC * → VisibilityManager → SocketHandler.setReady() * → ScryingPoolController * → AVTileAdapter → RoleRenderer → openStrip (GM only) * → VisibilityBadge (player only) — Story 1.6 * → NotificationBus (all clients) — Story 2.1 * → Story 2.2: DirectorsBoard (lazy, GM only) */ import { FoundryAdapter } from './src/foundry/FoundryAdapter.js'; import { StateStore } from './src/core/StateStore.js'; import { SocketHandler } from './src/core/SocketHandler.js'; import { VisibilityManager } from './src/core/VisibilityManager.js'; import { ScryingPoolController } from './src/core/ScryingPoolController.js'; import { AVTileAdapter } from './src/ui/shared/AVTileAdapter.js'; import { RoleRenderer } from './src/ui/RoleRenderer.js'; import { VisibilityBadge } from './src/ui/player/VisibilityBadge.js'; import { NotificationBus } from './src/notifications/NotificationBus.js'; import { DirectorsBoard } from './src/ui/gm/DirectorsBoard.js'; // Module-level references — constructed in init hook, used across hooks let adapter; let stateStore; let socketHandler; let visibilityManager; let scryingPoolController; let avTileAdapter; let roleRenderer; let visibilityBadge; let notificationBus; let directorsBoard; /** @type {boolean} Flag to prevent duplicate scene control button addition */ let directorsBoardButtonAdded = false; Hooks.once("init", () => { console.log("[ScryingPool] init — module loading"); // OQ-1 resolved (Story 1.2 spike): probe result is 'css-fallback' — see FoundryAdapter.js game.settings.register("scrying-pool", "webrtcMode", { scope: "world", config: false, type: String, default: "css-fallback", choices: { "track-disable": "Track Disable (bandwidth-saving)", "css-fallback": "CSS Fallback (cosmetic hiding)", "unsupported": "Unsupported (AV not available)", }, }); // Construct adapter first — needed for settings registration adapter = new FoundryAdapter(game); // Story 1.3: register remaining world settings via adapter adapter.settings.register("visibilityMatrix", { scope: "world", config: false, type: Object, default: { _version: 1, matrix: {} }, }); adapter.settings.register("showGMSelfFeed", { scope: "world", config: true, type: Boolean, default: true, }); // Story 2.1: per-user notification verbosity preference (client-scoped) adapter.settings.register("notificationVerbosity", { scope: "client", config: true, type: String, choices: { all: "All", "gm-only": "GM Only", silent: "Silent", }, default: "all", }); // Construct data layer — constructors are side-effect-free stateStore = new StateStore(adapter.settings); socketHandler = new SocketHandler(adapter.socket, adapter.hooks); // Story 2.2: GM-only keyboard shortcut to open/close Director's Board game.keybindings.register('scrying-pool', 'openDirectorsBoard', { name: "Open/Close Director's Board", hint: "Toggles the Director's Board window", editable: [{ key: 'KeyV', modifiers: ['Control', 'Shift'] }], restricted: true, // GM only onDown: () => directorsBoard?.toggle(), }); // Story 2.3: Bulk-action keybindings (GM only, migrated to scrying-pool namespace) game.keybindings.register('scrying-pool', 'showAll', { name: game.i18n.localize('video-view-manager.keybindings.showAll.name'), hint: game.i18n.localize('video-view-manager.keybindings.showAll.hint'), editable: [{ key: 'KeyS', modifiers: ['Control', 'Shift'] }], restricted: true, onDown: () => directorsBoard?.showAll(), }); game.keybindings.register('scrying-pool', 'hideAll', { name: game.i18n.localize('video-view-manager.keybindings.hideAll.name'), hint: game.i18n.localize('video-view-manager.keybindings.hideAll.hint'), editable: [{ key: 'KeyH', modifiers: ['Control', 'Shift'] }], restricted: true, onDown: () => directorsBoard?.hideAll(), }); game.keybindings.register('scrying-pool', 'spotlightParticipant', { name: game.i18n.localize('video-view-manager.keybindings.spotlightParticipant.name'), hint: game.i18n.localize('video-view-manager.keybindings.spotlightParticipant.hint'), editable: [{ key: 'KeyP', modifiers: ['Control', 'Shift'] }], restricted: true, onDown: () => directorsBoard?.spotlightFocused(), }); // Story 2.2: Inject GM-only sidebar button via scene controls hook. // Uses the strip footer CTA pattern if getSceneControlButtons API is unavailable. Hooks.on('getSceneControlButtons', (controls) => { // Prevent duplicate button addition if (directorsBoardButtonAdded) return; if (!game.user?.isGM) return; const tokenGroup = controls.find?.(c => c.name === 'token'); if (!tokenGroup?.tools) return; // Check if button already exists if (tokenGroup.tools.some(t => t.name === 'directors-board')) return; tokenGroup.tools.push({ name: 'directors-board', title: "Director's Board", icon: 'fas fa-border-all', onClick: () => directorsBoard?.toggle(), button: true, }); directorsBoardButtonAdded = true; }); }); Hooks.once("ready", () => { console.log("[ScryingPool] ready — module active"); // Hydrate StateStore from persisted world setting (AC-6, AC-7) stateStore.init(); // Probe WebRTC capability and set adapter.webrtc (AC-8) const outcome = FoundryAdapter.probeCapability(game.webrtc); adapter.webrtc = outcome === 'track-disable' ? FoundryAdapter.buildWebRTCSurface(game.webrtc) : null; adapter.settings.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome).catch(err => { console.error('[ScryingPool] Failed to set webrtcMode setting:', err); }); // Wire core managers — construct both before setReady so handler can reference both visibilityManager = new VisibilityManager(stateStore, adapter); scryingPoolController = new ScryingPoolController(stateStore, socketHandler, adapter); // Set up composite handler for SocketHandler timeout callbacks // This allows cleanup of ScryingPoolController._pendingOps when onRevert fires socketHandler.setReady({ onRevert: (pendingOp) => { visibilityManager.onRevert(pendingOp); scryingPoolController.cleanupPendingOp(pendingOp.userId); } }); // Initialize both managers — must come after setReady so queue is drained // before echo listener is registered (prevents early echo loss) try { visibilityManager.init(); scryingPoolController.init(); // Story 1.5: AV tile integration + GM control UI avTileAdapter = new AVTileAdapter(adapter); roleRenderer = new RoleRenderer(stateStore, scryingPoolController, avTileAdapter, adapter); roleRenderer.init(); if (adapter.users.isGM() && game.webrtc !== null) { roleRenderer.openStrip(); } if (!adapter.users.isGM()) { visibilityBadge = new VisibilityBadge(stateStore, scryingPoolController, avTileAdapter, adapter); visibilityBadge.init(); } // Story 2.1: NotificationBus — runs for all clients (GM and players) notificationBus = new NotificationBus(adapter); notificationBus.init(); // Story 2.2: DirectorsBoard (lazy, GM only) if (adapter.users.isGM()) { directorsBoard = new DirectorsBoard(stateStore, scryingPoolController, adapter); directorsBoard.init(); } } catch (err) { console.error('[ScryingPool] Module initialization failed:', err); throw err; // Re-throw to prevent module from loading in broken state } });