Files
2025-01-04 00:34:03 +01:00

371 lines
10 KiB
JavaScript

/**
* @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<string>}
*/
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<string, boolean|number>}
* @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<string, BaseEffectSource>}
*/
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<string, boolean>}
*/
suppression = {};
/* -------------------------------------------- */
/* Source Initialization */
/* -------------------------------------------- */
/**
* Initialize and configure the source using provided data.
* @param {Partial<SourceData>} 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<SourceData>} 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<SourceData>} 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;
}
}