/** * GMActorMappingPanel — GM settings submenu for assigning actors to users * for the Token Video Overlay feature. * * The GM maps an actor to a user's webcam feed. When "Show Webcam Video on * Tokens" is enabled, any token of the mapped actor will display that user's * webcam instead of the token icon. * * Extends ApplicationV2 via HandlebarsApplicationMixin. * Uses module-level _adapter for DI (same pattern as PlayerPrivacyPanelMenu). * * @module ui/gm/GMActorMappingPanel */ /** @type {import('../../foundry/FoundryAdapter.js').FoundryAdapter|null} */ let _adapter = null; /** * Initialize static adapter reference. Called once from module.js ready hook. * @param {import('../../foundry/FoundryAdapter.js').FoundryAdapter} adapter */ export function initGMActorMappingPanel(adapter) { _adapter = adapter; } const _AppBase = typeof foundry !== 'undefined' && foundry.applications?.api?.HandlebarsApplicationMixin && foundry.applications?.api?.ApplicationV2 ? foundry.applications.api.HandlebarsApplicationMixin( foundry.applications.api.ApplicationV2 ) : class _FallbackApp { static DEFAULT_OPTIONS = {}; static PARTS = {}; get rendered() { return this._rendered ?? false; } set rendered(v) { this._rendered = v; } get element() { return this._element ?? null; } set element(v) { this._element = v; } async render() { this._rendered = true; } async close() { this._rendered = false; } async _prepareContext() { return {}; } _onRender() {} _onClose() {} _onPosition() {} }; /** * GM Actor Mapping Panel — assign actors to user webcams for token overlay. */ export class GMActorMappingPanel extends _AppBase { static DEFAULT_OPTIONS = { id: 'scrying-pool-actor-mapping', classes: ['scrying-pool', 'actor-mapping'], window: { title: 'SCRYING_POOL.ActorMapping.title', resizable: false, width: 450, height: 'auto', }, position: {}, }; static PARTS = { form: { template: 'modules/scrying-pool/templates/actor-mapping.hbs', }, }; async _prepareContext() { const mapping = _adapter?.settings?.get('userActorMapping') ?? {}; const selectedId = (uid) => mapping[uid] ?? ''; const sortedActors = _adapter.actors.all() .map(a => ({ id: a.id, name: a.name })) .sort((a, b) => a.name.localeCompare(b.name)); const users = _adapter.users.all() .map(u => ({ id: u.id, name: u.name, isGM: u.isGM, avatar: u.avatar ?? 'icons/svg/mystery-man.svg', actors: sortedActors.map(a => ({ id: a.id, name: a.name, selected: a.id === selectedId(u.id), })), })) .sort((a, b) => { if (a.isGM && !b.isGM) return -1; if (!a.isGM && b.isGM) return 1; return a.name.localeCompare(b.name); }); return { hasNoUsers: users.length === 0, users, }; } _onRender(context, options) { if (this._formHandlerAttached) return; this._formHandlerAttached = true; const form = this.element?.querySelector('form'); if (!form) return; form.addEventListener('change', (event) => { const select = event.target; if (select.tagName !== 'SELECT') return; const mapping = { ...(_adapter?.settings?.get('userActorMapping') ?? {}) }; if (select.value) { mapping[select.name] = select.value; } else { delete mapping[select.name]; } _adapter?.settings?.set('userActorMapping', mapping).catch(err => { console.error('[ScryingPool] Failed to save actor mapping:', err); }); }); } }