/*
|
|
* This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
|
|
* Copyright (c) 2021 Matthew Haentschke.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, version 3.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {
|
|
logger
|
|
} from './logger.js';
|
|
import {
|
|
MODULE
|
|
} from './module.js'
|
|
|
|
/** @typedef {import('./api.js').Shorthand} Shorthand */
|
|
|
|
/**
|
|
* @typedef {Object} MutationData
|
|
* @property {Shorthand} delta
|
|
* @property {string} user
|
|
* @property {Object<string, string>} comparisonKeys
|
|
* @property {Shorthand} updateOpts
|
|
* @property {object} overrides
|
|
* @property {string} name
|
|
*/
|
|
|
|
/**
|
|
* The following class and its utility methods allows safer and more direct modification of the mutation stack,
|
|
* which is stored on a token's actor. This mutation stack stores the information needed to _revert_ the changes
|
|
* made by a mutation. This could be used, for example, to deal with rollover damage where the hit point value
|
|
* being reverted to is lower than when the mutation was first applied.
|
|
*
|
|
* Searching and querying a given mutation is a quick, read-only process. When the mutation stack is modified
|
|
* via one of its class methods, the actor's mutation data at that point in time will be copied for fast, local updates.
|
|
*
|
|
* No changes will be made to the actor's serialized data until the changes have been commited ({@link MutationStack#commit}).
|
|
* The MutationStack object will then be locked back into a read-only state sourced with this newly updated data.
|
|
*/
|
|
export class MutationStack {
|
|
constructor(tokenDoc) {
|
|
|
|
this.actor = tokenDoc instanceof TokenDocument ? tokenDoc.actor :
|
|
tokenDoc instanceof Token ? tokenDoc.document.actor :
|
|
tokenDoc instanceof Actor ? tokenDoc :
|
|
null;
|
|
|
|
if(!this.actor) {
|
|
throw new Error(MODULE.localize('error.stack.noActor'));
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Private copy of the working stack (mutable)
|
|
* @type {Array<MutationData>}
|
|
*/
|
|
#stack = [];
|
|
|
|
/** indicates if the stack has been duplicated for modification */
|
|
#locked = true;
|
|
|
|
/**
|
|
* Current stack, according to the remote server (immutable)
|
|
* @const
|
|
* @type {Array<MutationData>}
|
|
*/
|
|
get #liveStack() {
|
|
// @ts-ignore
|
|
return this.actor?.getFlag(MODULE.data.name, 'mutate') ?? []
|
|
}
|
|
|
|
/**
|
|
* Mutation stack according to its lock state.
|
|
* @type {Array<MutationData>}
|
|
*/
|
|
get stack() {
|
|
return this.#locked ? this.#liveStack : this.#stack ;
|
|
}
|
|
|
|
/**
|
|
* @callback FilterFn
|
|
* @param {MutationData} mutation
|
|
* @returns {boolean} provided mutation meets criteria
|
|
* @memberof MutationStack
|
|
*/
|
|
|
|
/**
|
|
* Searches for an element of the mutation stack that satisfies the provided predicate
|
|
*
|
|
* @param {FilterFn} predicate Receives the argments of `Array.prototype.find`
|
|
* and should return a boolean indicating if the current element satisfies the predicate condition
|
|
* @return {MutationData|undefined} Element of the mutation stack that matches the predicate, or undefined if none.
|
|
*/
|
|
find(predicate) {
|
|
if (this.#locked) return this.#liveStack.find(predicate);
|
|
|
|
return this.#stack.find(predicate);
|
|
}
|
|
|
|
/**
|
|
* Searches for an element of the mutation stack that satisfies the provided predicate and returns its
|
|
* stack index
|
|
*
|
|
* @param {FilterFn} predicate Receives the argments of {@link Array.findIndex} and returns a Boolean indicating if the current
|
|
* element satisfies the predicate condition
|
|
* @return {Number} Index of the element of the mutation stack that matches the predicate, or undefined if none.
|
|
*/
|
|
#findIndex( predicate ) {
|
|
|
|
if (this.#locked) return this.#liveStack.findIndex(predicate);
|
|
|
|
return this.#stack.findIndex(predicate);
|
|
}
|
|
|
|
/**
|
|
* Retrieves an element of the mutation stack that matches the provided name
|
|
*
|
|
* @param {String} name Name of mutation (serves as a unique identifier)
|
|
* @return {MutationData|undefined} Element of the mutation stack matching the provided name, or undefined if none
|
|
*/
|
|
getName(name) {
|
|
return this.find((element) => element.name === name);
|
|
}
|
|
|
|
/**
|
|
* Retrieves that last mutation added to the mutation stack (i.e. the "newest"),
|
|
* or undefined if none present
|
|
* @type {MutationData}
|
|
*/
|
|
get last() {
|
|
return this.stack[this.stack.length - 1];
|
|
}
|
|
|
|
/**
|
|
* Updates the mutation matching the provided name with the provided mutation info.
|
|
* The mutation info can be a subset of the full object if (and only if) overwrite is false.
|
|
*
|
|
* @param {string} name name of mutation to update
|
|
* @param {MutationData} data New information, can include 'name'.
|
|
* @param {object} options
|
|
* @param {boolean} [options.overwrite = false] default will merge the provided info
|
|
* with the current values. True will replace the entire entry and requires
|
|
* at least the 'name' field.
|
|
*
|
|
* @return {MutationStack} self, unlocked for writing and updates staged if update successful
|
|
*/
|
|
update(name, data, {
|
|
overwrite = false
|
|
}) {
|
|
const index = this.#findIndex((element) => element.name === name);
|
|
|
|
if (index < 0) {
|
|
return this;
|
|
}
|
|
|
|
this.#unlock();
|
|
|
|
if (overwrite) {
|
|
|
|
/* we need at LEAST a name to identify by */
|
|
if (!data.name) {
|
|
logger.error(MODULE.localize('error.incompleteMutateInfo'));
|
|
this.#locked=true;
|
|
return this;
|
|
}
|
|
|
|
/* if no user is provided, input current user. */
|
|
if (!data.user) data.user = game.user.id;
|
|
this.#stack[index] = data;
|
|
|
|
} else {
|
|
/* incomplete mutations are fine with merging */
|
|
mergeObject(this.#stack[index], data);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Applies a given change or tranform function to the current buffer,
|
|
* unlocking if needed.
|
|
*
|
|
* @param {MutationData|function(MutationData) : MutationData} transform Object to merge or function to generate an object to merge from provided {@link MutationData}
|
|
* @param {FilterFn} [filterFn = () => true] Optional function returning a boolean indicating
|
|
* if this element should be modified. By default, affects all elements of the mutation stack.
|
|
* @return {MutationStack} self, unlocked for writing and updates staged.
|
|
*/
|
|
updateAll(transform, filterFn = () => true) {
|
|
|
|
const innerUpdate = (transform) => {
|
|
if (typeof transform === 'function') {
|
|
/* if we are applying a transform function */
|
|
return (element) => mergeObject(element, transform(element));
|
|
} else {
|
|
/* if we are applying a constant change */
|
|
return (element) => mergeObject(element, transform);
|
|
}
|
|
}
|
|
|
|
this.#unlock();
|
|
|
|
this.#stack.forEach((element) => {
|
|
if (filterFn(element)) {
|
|
innerUpdate(transform)(element);
|
|
}
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Deletes all mutations from this actor's stack, effectively making
|
|
* the current changes permanent.
|
|
*
|
|
* @param {function(MutationData):boolean} [filterFn = () => true] Optional function returning a boolean indicating if this
|
|
* element should be delete. By default, deletes all elements of the mutation stack.
|
|
* @return {MutationStack} self, unlocked for writing and updates staged.
|
|
*/
|
|
deleteAll(filterFn = () => true) {
|
|
this.#unlock();
|
|
|
|
this.#stack = this.#stack.filter((element) => !filterFn(element))
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Updates the owning actor with the mutation stack changes made. Will not commit a locked buffer.
|
|
*
|
|
* @return {Promise<MutationStack>} self, locked for writing
|
|
*/
|
|
async commit() {
|
|
|
|
if(this.#locked) {
|
|
logger.error(MODULE.localize('error.stackLockedOrEmpty'))
|
|
}
|
|
|
|
await this.actor.update({
|
|
flags: {
|
|
[MODULE.data.name]: {
|
|
'mutate': this.#stack
|
|
}
|
|
}
|
|
});
|
|
|
|
/* return to a locked read-only state */
|
|
this.#locked = true;
|
|
this.#stack.length = 0;
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Unlocks the current buffer for writing by copying the mutation stack into this object.
|
|
*
|
|
* @return {boolean} Indicates if the unlock occured. False indicates the buffer was already unlocked.
|
|
*/
|
|
#unlock() {
|
|
|
|
if (!this.#locked) {
|
|
return false;
|
|
}
|
|
|
|
this.#stack = duplicate(this.#liveStack)
|
|
this.#locked = false;
|
|
return true;
|
|
}
|
|
|
|
}
|