/*
* 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 .
*/
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} 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}
*/
#stack = [];
/** indicates if the stack has been duplicated for modification */
#locked = true;
/**
* Current stack, according to the remote server (immutable)
* @const
* @type {Array}
*/
get #liveStack() {
// @ts-ignore
return this.actor?.getFlag(MODULE.data.name, 'mutate') ?? []
}
/**
* Mutation stack according to its lock state.
* @type {Array}
*/
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} 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;
}
}