125 lines
3.6 KiB
JavaScript
125 lines
3.6 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
}
|
|
}
|