Story 3.2 done
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user