272 lines
11 KiB
JavaScript
272 lines
11 KiB
JavaScript
// @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 { 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 { 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 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 3.2: Create StripOverlayLayer (shared infrastructure for UI components)
|
|
stripOverlayLayer = new StripOverlayLayer(adapter);
|
|
stripOverlayLayer.init();
|
|
|
|
// Story 3.2: Create ConfirmationBar for preset apply feedback
|
|
confirmationBar = new ConfirmationBar(adapter, visibilityManager, socketHandler, stripOverlayLayer);
|
|
confirmationBar.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);
|
|
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 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
|
|
if (adapter.users.isGM()) {
|
|
directorsBoard = new DirectorsBoard(stateStore, scryingPoolController, adapter, scenePresetManager);
|
|
directorsBoard.init();
|
|
}
|
|
} catch (err) {
|
|
console.error('[ScryingPool] Module initialization failed:', err);
|
|
throw err; // Re-throw to prevent module from loading in broken state
|
|
}
|
|
});
|
|
|