/** * @typedef {Object} BasseEffectSourceOptions * @property {PlaceableObject} [options.object] An optional PlaceableObject which is responsible for this source * @property {string} [options.sourceId] A unique ID for this source. This will be set automatically if an * object is provided, otherwise is required. */ /** * @typedef {Object} BaseEffectSourceData * @property {number} x The x-coordinate of the source location * @property {number} y The y-coordinate of the source location * @property {number} elevation The elevation of the point source * @property {boolean} disabled Whether or not the source is disabled */ /** * TODO - Re-document after ESM refactor. * An abstract base class which defines a framework for effect sources which originate radially from a specific point. * This abstraction is used by the LightSource, VisionSource, SoundSource, and MovementSource subclasses. * * @example A standard PointSource lifecycle: * ```js * const source = new PointSource({object}); // Create the point source * source.initialize(data); // Configure the point source with new data * source.refresh(); // Refresh the point source * source.destroy(); // Destroy the point source * ``` * * @template {BaseEffectSourceData} SourceData * @template {PIXI.Polygon} SourceShape * @abstract */ export default class BaseEffectSource { /** * An effect source is constructed by providing configuration options. * @param {BasseEffectSourceOptions} [options] Options which modify the base effect source instance */ constructor(options={}) { if ( options instanceof PlaceableObject ) { const warning = "The constructor PointSource(PlaceableObject) is deprecated. " + "Use new PointSource({ object }) instead."; foundry.utils.logCompatibilityWarning(warning, {since: 11, until: 13}); this.object = options; this.sourceId = this.object.sourceId; } else { this.object = options.object ?? null; this.sourceId = options.sourceId; } } /** * The type of source represented by this data structure. * Each subclass must implement this attribute. * @type {string} */ static sourceType; /** * The target collection into the effects canvas group. * @type {string} * @abstract */ static effectsCollection; /** * Effect source default data. * @type {SourceData} */ static defaultData = { x: 0, y: 0, elevation: 0, disabled: false } /* -------------------------------------------- */ /* Source Data */ /* -------------------------------------------- */ /** * Some other object which is responsible for this source. * @type {object|null} */ object; /** * The source id linked to this effect source. * @type {Readonly} */ sourceId; /** * The data of this source. * @type {SourceData} */ data = foundry.utils.deepClone(this.constructor.defaultData); /** * The geometric shape of the effect source which is generated later. * @type {SourceShape} */ shape; /** * A collection of boolean flags which control rendering and refresh behavior for the source. * @type {Record} * @protected */ _flags = {}; /** * The x-coordinate of the point source origin. * @type {number} */ get x() { return this.data.x; } /** * The y-coordinate of the point source origin. * @type {number} */ get y() { return this.data.y; } /** * The elevation bound to this source. * @type {number} */ get elevation() { return this.data.elevation; } /* -------------------------------------------- */ /* Source State */ /* -------------------------------------------- */ /** * The EffectsCanvasGroup collection linked to this effect source. * @type {Collection} */ get effectsCollection() { return canvas.effects[this.constructor.effectsCollection]; } /** * Returns the update ID associated with this source. * The update ID is increased whenever the shape of the source changes. * @type {number} */ get updateId() { return this.#updateId; } #updateId = 0; /** * Is this source currently active? * A source is active if it is attached to an effect collection and is not disabled or suppressed. * @type {boolean} */ get active() { return this.#attached && !this.data.disabled && !this.suppressed; } /** * Is this source attached to an effect collection? * @type {boolean} */ get attached() { return this.#attached; } #attached = false; /* -------------------------------------------- */ /* Source Suppression Management */ /* -------------------------------------------- */ /** * Is this source temporarily suppressed? * @type {boolean} */ get suppressed() { return Object.values(this.suppression).includes(true); }; /** * Records of suppression strings with a boolean value. * If any of this record is true, the source is suppressed. * @type {Record} */ suppression = {}; /* -------------------------------------------- */ /* Source Initialization */ /* -------------------------------------------- */ /** * Initialize and configure the source using provided data. * @param {Partial} data Provided data for configuration * @param {object} options Additional options which modify source initialization * @param {object} [options.behaviors] An object containing optional behaviors to apply. * @param {boolean} [options.reset=false] Should source data be reset to default values before applying changes? * @returns {BaseEffectSource} The initialized source */ initialize(data={}, {reset=false}={}) { // Reset the source back to default data if ( reset ) data = Object.assign(foundry.utils.deepClone(this.constructor.defaultData), data); // Update data for the source let changes = {}; if ( !foundry.utils.isEmpty(data) ) { const prior = foundry.utils.deepClone(this.data) || {}; for ( const key in data ) { if ( !(key in this.data) ) continue; this.data[key] = data[key] ?? this.constructor.defaultData[key]; } this._initialize(data); changes = foundry.utils.flattenObject(foundry.utils.diffObject(prior, this.data)); } // Update shapes for the source try { this._createShapes(); this.#updateId++; } catch (err) { console.error(err); this.remove(); } // Configure attached and non disabled sources if ( this.#attached && !this.data.disabled ) this._configure(changes); return this; } /* -------------------------------------------- */ /** * Subclass specific data initialization steps. * @param {Partial} data Provided data for configuration * @abstract */ _initialize(data) {} /* -------------------------------------------- */ /** * Create the polygon shape (or shapes) for this source using configured data. * @protected * @abstract */ _createShapes() {} /* -------------------------------------------- */ /** * Subclass specific configuration steps. Occurs after data initialization and shape computation. * Only called if the source is attached and not disabled. * @param {Partial} changes Changes to the source data which were applied * @protected */ _configure(changes) {} /* -------------------------------------------- */ /* Source Refresh */ /* -------------------------------------------- */ /** * Refresh the state and uniforms of the source. * Only active sources are refreshed. */ refresh() { if ( !this.active ) return; this._refresh(); } /* -------------------------------------------- */ /** * Subclass-specific refresh steps. * @protected * @abstract */ _refresh() {} /* -------------------------------------------- */ /* Source Destruction */ /* -------------------------------------------- */ /** * Steps that must be performed when the source is destroyed. */ destroy() { this.remove(); this._destroy(); } /* -------------------------------------------- */ /** * Subclass specific destruction steps. * @protected */ _destroy() {} /* -------------------------------------------- */ /* Source Management */ /* -------------------------------------------- */ /** * Add this BaseEffectSource instance to the active collection. */ add() { if ( !this.sourceId ) throw new Error("A BaseEffectSource cannot be added to the active collection unless it has" + " a sourceId assigned."); this.effectsCollection.set(this.sourceId, this); const wasConfigured = this.#attached && !this.data.disabled; this.#attached = true; if ( !wasConfigured && !this.data.disabled ) this._configure({}); } /* -------------------------------------------- */ /** * Remove this BaseEffectSource instance from the active collection. */ remove() { if ( !this.effectsCollection.has(this.sourceId) ) return; this.effectsCollection.delete(this.sourceId); this.#attached = false; } /* -------------------------------------------- */ /* Deprecations and Compatibility */ /* -------------------------------------------- */ /** * @deprecated since v11 * @ignore */ get sourceType() { const msg = "BaseEffectSource#sourceType is deprecated. Use BaseEffectSource.sourceType instead."; foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13}); return this.constructor.sourceType; } /** * @deprecated since v12 * @ignore */ _createShape() { const msg = "BaseEffectSource#_createShape is deprecated in favor of BaseEffectSource#_createShapes."; foundry.utils.logCompatibilityWarning(msg, { since: 11, until: 13}); return this._createShapes(); } /** * @deprecated since v12 * @ignore */ get disabled() { foundry.utils.logCompatibilityWarning("BaseEffectSource#disabled is deprecated in favor of " + "BaseEffectSource#data#disabled or BaseEffectSource#active depending on your use case.", { since: 11, until: 13}); return this.data.disabled; } }