655 lines
21 KiB
JavaScript
655 lines
21 KiB
JavaScript
/**
|
|
* ScenePresetManager — Manages scene preset CRUD operations.
|
|
*
|
|
* Owns: preset creation, loading, deletion, renaming, listing.
|
|
* Story 3.2: Added auto-apply logic with per-scene configuration.
|
|
* Persists presets to Scene document flags.
|
|
* Emits socket messages for preset apply operations.
|
|
*
|
|
* Import rule: may only import from src/contracts/ and src/utils/.
|
|
* Constructors are side-effect free — call init() from module.js Hooks.once('ready').
|
|
*
|
|
* @module core/ScenePresetManager
|
|
*/
|
|
|
|
import { createScenePreset, isValidScenePreset, MAX_PRESETS_PER_WORLD, SCENE_PRESET_VERSION } from '../contracts/scene-preset.js';
|
|
import { SOCKET_EVENTS } from '../contracts/socket-message.js';
|
|
|
|
// Story 3.2: Auto-apply constants
|
|
/** @type {number} Maximum pre-delay in milliseconds */
|
|
const MAX_PREDELAY_MS = 5000;
|
|
/** @type {number} Minimum pre-delay in milliseconds */
|
|
const MIN_PREDELAY_MS = 0;
|
|
|
|
/**
|
|
* Manages scene preset CRUD operations.
|
|
* Persists presets to Scene document flags.
|
|
* Emits socket messages for preset apply operations.
|
|
* Story 3.2: Extended with auto-apply on scene activation.
|
|
*/
|
|
export class ScenePresetManager {
|
|
/**
|
|
* @param {import('../foundry/FoundryAdapter.js').FoundryAdapter} adapter
|
|
* Injected FoundryAdapter surface.
|
|
* @param {import('./StateStore.js').StateStore} stateStore
|
|
* Injected StateStore for visibility matrix access.
|
|
* @param {import('./SocketHandler.js').SocketHandler} socketHandler
|
|
* Injected SocketHandler for socket message emission.
|
|
* @param {import('./VisibilityManager.js').VisibilityManager|null} visibilityManager
|
|
* Injected VisibilityManager for applying visibility matrix (Story 3.2).
|
|
*/
|
|
constructor(adapter, stateStore, socketHandler, visibilityManager = null) {
|
|
// Validate dependencies
|
|
if (!adapter || typeof adapter !== 'object') {
|
|
throw new TypeError('ScenePresetManager: adapter argument is required and must be an object');
|
|
}
|
|
if (!stateStore || typeof stateStore !== 'object') {
|
|
throw new TypeError('ScenePresetManager: stateStore argument is required and must be an object');
|
|
}
|
|
if (!socketHandler || typeof socketHandler !== 'object') {
|
|
throw new TypeError('ScenePresetManager: socketHandler argument is required and must be an object');
|
|
}
|
|
|
|
this._adapter = adapter;
|
|
this._stateStore = stateStore;
|
|
this._socketHandler = socketHandler;
|
|
this._visibilityManager = visibilityManager;
|
|
/** @type {Map<string, import('../contracts/scene-preset.js').ScenePreset>} name → ScenePreset */
|
|
this._presetsCache = new Map();
|
|
/** @type {Map<string, number>} sceneId → timeoutId for active pre-delay timers */
|
|
this._activeTimers = new Map();
|
|
}
|
|
|
|
/**
|
|
* Initializes the manager by loading presets from the current scene.
|
|
* No hooks registered here for Story 3.1 — hooks for auto-apply come in Story 3.2.
|
|
* Side-effect: loads presets into _presetsCache.
|
|
*/
|
|
init() {
|
|
this._loadCurrentScenePresets();
|
|
}
|
|
|
|
/**
|
|
* Cleans up internal state.
|
|
* Safe to call multiple times.
|
|
*/
|
|
teardown() {
|
|
this._presetsCache.clear();
|
|
}
|
|
|
|
/**
|
|
* Saves the current Visibility Matrix as a named preset.
|
|
*
|
|
* @param {string} name - Preset name (non-empty string).
|
|
* @returns {Promise<import('../contracts/scene-preset.js').ScenePreset>} The created preset.
|
|
* @throws {TypeError} If name is invalid, duplicate, or max presets reached.
|
|
*/
|
|
async save(name) {
|
|
// Validate name
|
|
if (typeof name !== 'string' || name.length === 0) {
|
|
throw new TypeError('ScenePresetManager.save: name must be a non-empty string');
|
|
}
|
|
|
|
// Get current scene
|
|
const currentScene = this._adapter.scenes.current?.();
|
|
if (!currentScene) {
|
|
throw new TypeError('ScenePresetManager.save: no active scene');
|
|
}
|
|
|
|
// Check max presets limit
|
|
if (this._presetsCache.size >= MAX_PRESETS_PER_WORLD) {
|
|
throw new TypeError(
|
|
`ScenePresetManager.save: maximum of ${MAX_PRESETS_PER_WORLD} presets reached. Delete an existing preset to save a new one.`
|
|
);
|
|
}
|
|
|
|
// Check for duplicate name
|
|
if (this._presetsCache.has(name)) {
|
|
throw new TypeError(`ScenePresetManager.save: a preset with name "${name}" already exists`);
|
|
}
|
|
|
|
// Get current visibility matrix
|
|
const matrixData = this._stateStore.getMatrix();
|
|
const matrix = matrixData.matrix;
|
|
|
|
// Create preset
|
|
const preset = createScenePreset(name, matrix);
|
|
|
|
// Add to cache
|
|
this._presetsCache.set(name, preset);
|
|
|
|
// Persist to scene flag
|
|
await this._saveScenePresets();
|
|
|
|
// Emit notification
|
|
this._adapter.notifications.info(
|
|
this._adapter.i18n.localize('video-view-manager.presets.notifications.saved')
|
|
.replace('{name}', name)
|
|
);
|
|
|
|
return preset;
|
|
}
|
|
|
|
/**
|
|
* Loads a preset by name, applying its matrix to the current visibility state.
|
|
*
|
|
* @param {string} name - Preset name to load.
|
|
* @param {object} options - Options object.
|
|
* @param {boolean} [options.emitSocket=true] - Whether to emit socket message.
|
|
* Set to false when called in response to a socket message to prevent loops.
|
|
* @returns {Promise<void>}
|
|
* @throws {TypeError} If name is invalid or preset not found.
|
|
*/
|
|
async load(name, options = {}) {
|
|
const { emitSocket = true } = options;
|
|
// Validate name
|
|
if (typeof name !== 'string' || name.length === 0) {
|
|
throw new TypeError('ScenePresetManager.load: name must be a non-empty string');
|
|
}
|
|
|
|
// Get preset from cache (loaded via init or save)
|
|
const preset = this._presetsCache.get(name);
|
|
if (!preset) {
|
|
throw new TypeError(`ScenePresetManager.load: preset "${name}" not found`);
|
|
}
|
|
|
|
// Validate preset structure
|
|
isValidScenePreset(preset);
|
|
|
|
// Apply via VisibilityManager if available (Story 3.2), otherwise fall back to StateStore
|
|
if (this._visibilityManager) {
|
|
await this._visibilityManager.applyMatrix(preset.matrix);
|
|
} else {
|
|
await this._stateStore.setMatrix({
|
|
_version: preset._version,
|
|
matrix: { ...preset.matrix },
|
|
});
|
|
}
|
|
|
|
// Emit socket message only if requested (prevents loops when called from socket handler)
|
|
if (emitSocket) {
|
|
const now = Date.now();
|
|
this._socketHandler.emit(SOCKET_EVENTS.PRESET_APPLIED, {
|
|
presetName: name,
|
|
timestamp: now,
|
|
});
|
|
}
|
|
|
|
// Emit notification
|
|
this._adapter.notifications.info(
|
|
this._adapter.i18n.localize('video-view-manager.presets.notifications.applied')
|
|
.replace('{name}', name)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Deletes a preset by name.
|
|
*
|
|
* @param {string} name - Preset name to delete.
|
|
* @returns {Promise<void>}
|
|
* @throws {TypeError} If name is invalid or preset not found.
|
|
*/
|
|
async delete(name) {
|
|
// Validate name
|
|
if (typeof name !== 'string' || name.length === 0) {
|
|
throw new TypeError('ScenePresetManager.delete: name must be a non-empty string');
|
|
}
|
|
|
|
// Check if preset exists
|
|
if (!this._presetsCache.has(name)) {
|
|
throw new TypeError(`ScenePresetManager.delete: preset "${name}" not found`);
|
|
}
|
|
|
|
// Delete from cache
|
|
this._presetsCache.delete(name);
|
|
|
|
// Persist changes
|
|
await this._saveScenePresets();
|
|
}
|
|
|
|
/**
|
|
* Renames a preset.
|
|
*
|
|
* @param {string} oldName - Current preset name.
|
|
* @param {string} newName - New preset name.
|
|
* @returns {Promise<import('../contracts/scene-preset.js').ScenePreset>} The renamed preset.
|
|
* @throws {TypeError} If names are invalid, oldName not found, or newName conflicts.
|
|
*/
|
|
async rename(oldName, newName) {
|
|
// Validate oldName
|
|
if (typeof oldName !== 'string' || oldName.length === 0) {
|
|
throw new TypeError('ScenePresetManager.rename: oldName must be a non-empty string');
|
|
}
|
|
|
|
// Validate newName
|
|
if (typeof newName !== 'string' || newName.length === 0) {
|
|
throw new TypeError('ScenePresetManager.rename: newName must be a non-empty string');
|
|
}
|
|
|
|
// Check if oldName exists
|
|
if (!this._presetsCache.has(oldName)) {
|
|
throw new TypeError(`ScenePresetManager.rename: preset "${oldName}" not found`);
|
|
}
|
|
|
|
// Check if newName conflicts
|
|
if (oldName !== newName && this._presetsCache.has(newName)) {
|
|
throw new TypeError(
|
|
`ScenePresetManager.rename: a preset with name "${newName}" already exists`
|
|
);
|
|
}
|
|
|
|
// Get preset
|
|
const preset = this._presetsCache.get(oldName);
|
|
|
|
// Delete old entry
|
|
this._presetsCache.delete(oldName);
|
|
|
|
// Update preset name and timestamps
|
|
const now = Date.now();
|
|
const renamedPreset = {
|
|
...preset,
|
|
name: newName,
|
|
updatedAt: now,
|
|
};
|
|
// Validate the renamed preset structure
|
|
isValidScenePreset(/** @type {import('../contracts/scene-preset.js').ScenePreset} */ (renamedPreset));
|
|
|
|
// Add with new name
|
|
this._presetsCache.set(newName, /** @type {import('../contracts/scene-preset.js').ScenePreset} */ (renamedPreset));
|
|
|
|
// Persist changes
|
|
await this._saveScenePresets();
|
|
|
|
return /** @type {import('../contracts/scene-preset.js').ScenePreset} */ (renamedPreset);
|
|
}
|
|
|
|
/**
|
|
* Returns all presets for the current scene.
|
|
*
|
|
* @returns {Array<import('../contracts/scene-preset.js').ScenePreset>} Array of preset objects.
|
|
*/
|
|
list() {
|
|
return Array.from(this._presetsCache.values());
|
|
}
|
|
|
|
/**
|
|
* Returns a specific preset by name, or null if not found.
|
|
*
|
|
* @param {string} name - Preset name.
|
|
* @returns {import('../contracts/scene-preset.js').ScenePreset|null}
|
|
*/
|
|
get(name) {
|
|
return this._presetsCache.get(name) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Loads presets from the current scene's flag.
|
|
* @private
|
|
*/
|
|
_loadCurrentScenePresets() {
|
|
this._presetsCache.clear();
|
|
|
|
const currentScene = this._adapter.scenes.current?.();
|
|
if (!currentScene) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (currentScene).getFlag?.('video-view-manager', 'presets');
|
|
if (!flagData || typeof flagData !== 'object') {
|
|
return; // No presets or invalid format
|
|
}
|
|
|
|
// Type assert flagData as having _version and presets
|
|
const flag = /** @type {{ _version: number; presets?: Record<string, unknown> }} */ (flagData);
|
|
if (flag._version !== SCENE_PRESET_VERSION) {
|
|
console.warn(
|
|
`[ScryingPool] ScenePresetManager: unsupported presets schema version ${flag._version}, expected ${SCENE_PRESET_VERSION}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const { presets } = flag;
|
|
if (!presets || typeof presets !== 'object') {
|
|
return;
|
|
}
|
|
|
|
for (const [name, presetData] of Object.entries(presets)) {
|
|
try {
|
|
const validated = isValidScenePreset(presetData);
|
|
this._presetsCache.set(name, validated);
|
|
} catch (err) {
|
|
console.warn(
|
|
`[ScryingPool] ScenePresetManager: invalid preset "${name}" in scene flag, skipping: ${err instanceof Error ? err.message : String(err)}`
|
|
);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.warn(
|
|
'[ScryingPool] ScenePresetManager: failed to load scene presets',
|
|
err instanceof Error ? err.message : String(err)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves presets to the current scene's flag.
|
|
* @private
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async _saveScenePresets() {
|
|
const currentScene = this._adapter.scenes.current?.();
|
|
if (!currentScene) {
|
|
throw new TypeError('ScenePresetManager._saveScenePresets: no active scene');
|
|
}
|
|
|
|
/** @type {Record<string, import('../contracts/scene-preset.js').ScenePreset>} */
|
|
const presetsObj = {};
|
|
for (const [name, preset] of this._presetsCache) {
|
|
presetsObj[name] = preset;
|
|
}
|
|
|
|
// Get existing flag data to preserve autoApply config
|
|
const existingFlag = this._getSceneFlagData(currentScene);
|
|
const autoApply = existingFlag?.autoApply ?? null;
|
|
|
|
const flagData = {
|
|
_version: SCENE_PRESET_VERSION,
|
|
presets: presetsObj,
|
|
...(autoApply && { autoApply }),
|
|
};
|
|
|
|
try {
|
|
await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (currentScene).setFlag?.('video-view-manager', 'presets', flagData);
|
|
} catch (err) {
|
|
console.error(
|
|
'[ScryingPool] ScenePresetManager: failed to save scene presets',
|
|
err instanceof Error ? err.message : String(err)
|
|
);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Story 3.2: Auto-Apply Methods
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* Handles scene activation for auto-apply functionality.
|
|
* Checks global enable, per-scene config, and applies preset after pre-delay.
|
|
*
|
|
* @param {object} scene - The activated FoundryVTT Scene document.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async onSceneActivate(scene) {
|
|
// Check if auto-apply is globally enabled
|
|
const globalEnabled = this._adapter.settings.get?.('video-view-manager.autoApplyEnabled') ?? true;
|
|
if (!globalEnabled) {
|
|
return; // Global disable
|
|
}
|
|
|
|
// Get current scene's flag data
|
|
const flagData = this._getSceneFlagData(scene);
|
|
if (!flagData) {
|
|
return; // No preset data for this scene
|
|
}
|
|
|
|
// Get auto-apply config (with defaults)
|
|
const autoApplyConfig = this._getAutoApplyConfig(flagData);
|
|
if (!autoApplyConfig.enabled) {
|
|
return; // Per-scene disable
|
|
}
|
|
|
|
// Check if preset exists
|
|
const preset = this._presetsCache.get(autoApplyConfig.presetName);
|
|
if (!preset) {
|
|
console.warn(
|
|
`[ScryingPool] ScenePresetManager.onSceneActivate: preset "${autoApplyConfig.presetName}" not found in cache`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Clear ALL pending timers when any scene is activated (prevents old scene timer from firing)
|
|
this._clearAllTimers();
|
|
|
|
// Apply preset after pre-delay
|
|
await this._applyWithDelay(scene, autoApplyConfig.presetName, autoApplyConfig.preDelay);
|
|
}
|
|
|
|
/**
|
|
* Applies a preset with optional auto-apply flag.
|
|
* Story 3.2: Extended to support auto-applied presets.
|
|
*
|
|
* @param {string} presetName - Name of the preset to apply.
|
|
* @param {object} options - Options object.
|
|
* @param {boolean} options.autoApplied - Whether this is an auto-apply operation.
|
|
* @returns {Promise<void>}
|
|
* @throws {TypeError} If preset not found.
|
|
*/
|
|
async applyPreset(presetName, options = {}) {
|
|
const { autoApplied = false } = options;
|
|
|
|
// Validate presetName
|
|
if (typeof presetName !== 'string' || presetName.length === 0) {
|
|
throw new TypeError('ScenePresetManager.applyPreset: presetName must be a non-empty string');
|
|
}
|
|
|
|
// Get preset from cache
|
|
const preset = this._presetsCache.get(presetName);
|
|
if (!preset) {
|
|
throw new TypeError(`ScenePresetManager.applyPreset: preset "${presetName}" not found`);
|
|
}
|
|
|
|
// Validate preset structure
|
|
isValidScenePreset(preset);
|
|
|
|
// Apply via VisibilityManager if available (Story 3.2), otherwise fall back to StateStore
|
|
if (this._visibilityManager) {
|
|
await this._visibilityManager.applyMatrix(preset.matrix);
|
|
} else {
|
|
await this._stateStore.setMatrix({
|
|
_version: preset._version,
|
|
matrix: { ...preset.matrix },
|
|
});
|
|
}
|
|
|
|
// Emit socket message for preset apply
|
|
const now = Date.now();
|
|
this._socketHandler.emit(SOCKET_EVENTS.PRESET_APPLIED, {
|
|
presetName,
|
|
timestamp: now,
|
|
autoApplied,
|
|
});
|
|
|
|
// Also emit hook for local confirmation (ConfirmationBar)
|
|
this._adapter.hooks.callAll('scrying-pool:presetApplied', {
|
|
presetName,
|
|
matrix: preset.matrix,
|
|
autoApplied,
|
|
timestamp: now,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Configures auto-apply settings for a scene.
|
|
*
|
|
* @param {object} scene - The FoundryVTT Scene document.
|
|
* @param {object} config - Auto-apply configuration.
|
|
* @param {boolean} config.enabled - Whether auto-apply is enabled.
|
|
* @param {string|null} config.presetName - Name of preset to auto-apply.
|
|
* @param {number} config.preDelay - Pre-delay in milliseconds (0-5000).
|
|
* @returns {Promise<void>}
|
|
* @throws {TypeError} If validation fails.
|
|
*/
|
|
async configureAutoApply(scene, config) {
|
|
// Validate scene
|
|
if (!scene || typeof scene !== 'object') {
|
|
throw new TypeError('ScenePresetManager.configureAutoApply: scene argument is required and must be an object');
|
|
}
|
|
|
|
// Validate config
|
|
if (!config || typeof config !== 'object') {
|
|
throw new TypeError('ScenePresetManager.configureAutoApply: config argument is required and must be an object');
|
|
}
|
|
|
|
const { enabled, presetName, preDelay } = config;
|
|
|
|
// Validate enabled
|
|
if (typeof enabled !== 'boolean') {
|
|
throw new TypeError('ScenePresetManager.configureAutoApply: enabled must be a boolean');
|
|
}
|
|
|
|
// Validate presetName
|
|
if (presetName !== null && typeof presetName !== 'string') {
|
|
throw new TypeError('ScenePresetManager.configureAutoApply: presetName must be a string or null');
|
|
}
|
|
|
|
// Validate preDelay
|
|
if (typeof preDelay !== 'number' || preDelay < MIN_PREDELAY_MS || preDelay > MAX_PREDELAY_MS) {
|
|
throw new TypeError(
|
|
`ScenePresetManager.configureAutoApply: preDelay must be a number between ${MIN_PREDELAY_MS} and ${MAX_PREDELAY_MS}`
|
|
);
|
|
}
|
|
|
|
// Get current flag data
|
|
const currentFlag = this._getSceneFlagData(scene);
|
|
const currentPresets = currentFlag?.presets ?? {};
|
|
|
|
// Build new flag data
|
|
const newFlagData = {
|
|
_version: SCENE_PRESET_VERSION,
|
|
presets: currentPresets,
|
|
autoApply: {
|
|
enabled,
|
|
presetName: presetName ?? null,
|
|
preDelay,
|
|
},
|
|
};
|
|
|
|
// Persist to scene flag
|
|
try {
|
|
await /** @type {object & { setFlag?: (scope: string, key: string, value: unknown) => Promise<unknown> }} */ (scene).setFlag?.('video-view-manager', 'presets', newFlagData);
|
|
} catch (err) {
|
|
console.error(
|
|
'[ScryingPool] ScenePresetManager: failed to configure auto-apply',
|
|
err instanceof Error ? err.message : String(err)
|
|
);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets auto-apply configuration from scene flag data.
|
|
* Returns defaults if autoApply field is missing.
|
|
*
|
|
* @param {object} flagData - The scene flag data.
|
|
* @returns {object} Auto-apply configuration with defaults.
|
|
* @private
|
|
*/
|
|
_getAutoApplyConfig(flagData) {
|
|
if (!flagData || typeof flagData !== 'object') {
|
|
return { enabled: false, presetName: null, preDelay: 0 };
|
|
}
|
|
|
|
const { autoApply } = flagData;
|
|
if (!autoApply || typeof autoApply !== 'object') {
|
|
return { enabled: false, presetName: null, preDelay: 0 };
|
|
}
|
|
|
|
return {
|
|
enabled: Boolean(autoApply.enabled),
|
|
presetName: typeof autoApply.presetName === 'string' ? autoApply.presetName : null,
|
|
preDelay: typeof autoApply.preDelay === 'number' ? Math.max(MIN_PREDELAY_MS, Math.min(autoApply.preDelay, MAX_PREDELAY_MS)) : 0,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Applies a preset after a specified delay.
|
|
*
|
|
* @param {object} scene - The FoundryVTT Scene document.
|
|
* @param {string} presetName - Name of the preset to apply.
|
|
* @param {number} delayMs - Delay in milliseconds.
|
|
* @returns {number} Timeout ID for potential cancellation.
|
|
* @private
|
|
*/
|
|
_applyWithDelay(scene, presetName, delayMs) {
|
|
const timerId = setTimeout(async () => {
|
|
try {
|
|
await this.applyPreset(presetName, { autoApplied: true });
|
|
|
|
// Notify via ui.notifications
|
|
this._adapter.notifications.info(
|
|
this._adapter.i18n.localize('video-view-manager.presets.notifications.scene-applied')
|
|
.replace('{name}', presetName)
|
|
);
|
|
} catch (err) {
|
|
console.error(
|
|
'[ScryingPool] ScenePresetManager: failed to apply preset on scene activation',
|
|
err instanceof Error ? err.message : String(err)
|
|
);
|
|
} finally {
|
|
// Clean up timer reference
|
|
this._activeTimers.delete(scene.id);
|
|
}
|
|
}, delayMs);
|
|
|
|
// Store timer reference for cancellation
|
|
if (scene?.id) {
|
|
this._activeTimers.set(scene.id, timerId);
|
|
}
|
|
|
|
return timerId;
|
|
}
|
|
|
|
/**
|
|
* Clears the active timer for a scene.
|
|
*
|
|
* @param {object} scene - The FoundryVTT Scene document.
|
|
* @private
|
|
*/
|
|
_clearSceneTimer(scene) {
|
|
if (!scene?.id) return;
|
|
|
|
const timerId = this._activeTimers.get(scene.id);
|
|
if (timerId) {
|
|
clearTimeout(timerId);
|
|
this._activeTimers.delete(scene.id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears ALL active timers.
|
|
* Called when a new scene is activated to prevent old scene timers from firing.
|
|
* @private
|
|
*/
|
|
_clearAllTimers() {
|
|
for (const [, timerId] of this._activeTimers) {
|
|
clearTimeout(timerId);
|
|
}
|
|
this._activeTimers.clear();
|
|
}
|
|
|
|
/**
|
|
* Gets the complete flag data from a scene.
|
|
*
|
|
* @param {object} scene - The FoundryVTT Scene document.
|
|
* @returns {object|null} The flag data or null.
|
|
* @private
|
|
*/
|
|
_getSceneFlagData(scene) {
|
|
try {
|
|
const flagData = /** @type {object & { getFlag?: (scope: string, key: string) => unknown }} */ (scene).getFlag?.('video-view-manager', 'presets');
|
|
if (!flagData || typeof flagData !== 'object') {
|
|
return null;
|
|
}
|
|
return /** @type {object} */ (flagData);
|
|
} catch (err) {
|
|
console.warn(
|
|
'[ScryingPool] ScenePresetManager: failed to get scene flag data',
|
|
err instanceof Error ? err.message : String(err)
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
}
|