// @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 { ScenePresetManager } from './src/core/ScenePresetManager.js'; import { PlayerPrivacyManager } from './src/core/PlayerPrivacyManager.js'; import { PortraitFallbackHandler } from './src/core/PortraitFallbackHandler.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'; import { ConfirmationBar } from './src/ui/gm/ConfirmationBar.js'; import { StripOverlayLayer } from './src/ui/shared/StripOverlayLayer.js'; import { PlayerPrivacyPanelMenu, initPlayerPrivacyPanelMenu } from './src/ui/player/PlayerPrivacyPanelMenu.js'; import { initGMPlayerPrivacySelector } from './src/ui/gm/GMPlayerPrivacySelector.js'; import { SOCKET_EVENTS } from './src/contracts/socket-message.js'; // Module-level references — constructed in init hook, used across hooks let adapter; let stateStore; let socketHandler; let visibilityManager; let scryingPoolController; let scenePresetManager; let playerPrivacyManager; let portraitFallbackHandler; let avTileAdapter; let roleRenderer; let visibilityBadge; let notificationBus; let directorsBoard; let stripOverlayLayer; let confirmationBar; /** @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", }); // Story 3.2: Global auto-apply enable/disable setting adapter.settings.register("autoApplyEnabled", { scope: "world", config: true, type: Boolean, default: true, name: "Enable Scene Preset Auto-Apply", hint: "When enabled, scenes with configured presets will automatically apply them on activation", }); // Construct data layer — constructors are side-effect-free // Note: ScenePresetManager is constructed in 'ready' hook after visibilityManager is available 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); // Story 3.2: Re-construct ScenePresetManager with visibilityManager for auto-apply scenePresetManager = new ScenePresetManager(adapter, stateStore, socketHandler, visibilityManager); // Story 4.1: Create PlayerPrivacyManager for automation opt-ins playerPrivacyManager = new PlayerPrivacyManager(adapter); // Story 4.2: Create PortraitFallbackHandler for custom portrait fallback portraitFallbackHandler = new PortraitFallbackHandler(adapter, playerPrivacyManager); portraitFallbackHandler.init(); // Story 3.2: Register updateScene hook for auto-apply adapter.hooks.on('updateScene', (scene) => { if (adapter.users.isGM()) { scenePresetManager.onSceneActivate(scene); } }); // Story 3.1: Initialize ScenePresetManager to load presets from current scene scenePresetManager.init(); // 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); // Story 4.2: Pass portraitFallbackHandler for custom portrait display roleRenderer = new RoleRenderer(stateStore, scryingPoolController, avTileAdapter, adapter, portraitFallbackHandler); roleRenderer.init(); if (adapter.users.isGM() && game.webrtc !== null) { roleRenderer.openStrip(); // Story 3.2: Create ConfirmationBar for preset apply feedback (GM only) // Pass roleRenderer to access ScryingPoolStrip.stripOverlayLayer (created lazily) confirmationBar = new ConfirmationBar(adapter, visibilityManager, socketHandler, roleRenderer); confirmationBar.init(); } 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 3.1: Register socket listener for preset apply echo (all clients receive) // Note: In Foundry, socket messages are automatically broadcast to all clients. // The GM emits PRESET_APPLIED, and all clients (including GM) receive it. // We skip processing on the GM since they already applied it locally. adapter.socket.on(SOCKET_EVENTS.PRESET_APPLIED, async (payload) => { try { // Validate payload if (!payload || typeof payload !== 'object' || typeof payload.presetName !== 'string') { console.warn('[ScryingPool] Invalid PRESET_APPLIED payload:', payload); return; } // Skip on GM — they already applied the preset locally if (adapter.users.isGM()) { return; } // Load the preset on this client (emitSocket: false to prevent loop) await scenePresetManager.load(payload.presetName, { emitSocket: false }); } catch (err) { console.error('[ScryingPool] Failed to handle PRESET_APPLIED:', err); } }); // Story 2.2: DirectorsBoard (lazy, GM only) // Story 3.1: Pass scenePresetManager for preset save/load functionality // Story 4.1: Pass playerPrivacyManager for Reaction Cam badge display if (adapter.users.isGM()) { directorsBoard = new DirectorsBoard(stateStore, scryingPoolController, adapter, scenePresetManager, playerPrivacyManager); directorsBoard.init(); } // Story 4.1: Initialize PlayerPrivacyPanelMenu with DI dependencies // Story 4.2: Pass portraitFallbackHandler for portrait selection initPlayerPrivacyPanelMenu(adapter, playerPrivacyManager, portraitFallbackHandler); // Story 4.1: Register GM-only Player Privacy Selector (Task 5.2) // Allows GM to select any player and view their privacy settings in read-only mode // Story 4.2: Pass portraitFallbackHandler for portrait display initGMPlayerPrivacySelector(adapter, playerPrivacyManager, portraitFallbackHandler); // Story 4.1: Register PlayerPrivacyPanel in module settings (Task 5.1) // Note: Must be registered AFTER init calls to avoid race conditions game.settings.registerMenu('scrying-pool', 'playerPrivacyPanel', { name: 'SCRYING_POOL.Settings.PlayerPrivacyPanel', label: 'SCRYING_POOL.Settings.PlayerPrivacyPanelLabel', hint: 'SCRYING_POOL.Settings.PlayerPrivacyPanelHint', icon: 'fa-solid fa-user-shield', type: PlayerPrivacyPanelMenu, restricted: false, }); } catch (err) { console.error('[ScryingPool] Module initialization failed:', err); throw err; // Re-throw to prevent module from loading in broken state } });