Files
scrying-pool/module.js
T
uberwald 7918792f4e Fix Story 2.3 code review findings: remove duplicate ParticipantCard.js, fix lint in ScryingPoolStrip.js
- Delete src/ui/shared/ParticipantCard.js (duplicate of boardUtils.js with conflicting implementations)
- Delete tests/unit/ui/shared/ParticipantCard.test.js (tests for deleted file)
- Add directorsBoard to global declarations in ScryingPoolStrip.js to fix lint errors

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-05-23 11:31:01 +02:00

208 lines
8.1 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 { 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
}
});