Initial
This commit is contained in:
370
resources/app/client-esm/canvas/sources/base-effect-source.mjs
Normal file
370
resources/app/client-esm/canvas/sources/base-effect-source.mjs
Normal file
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user