All user data for FoundryVTT. Includes worlds, systems, modules, and any asset in the "foundryuserdata" directory. Does NOT include the FoundryVTT installation itself.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

279 lines
8.7 KiB

1 year ago
  1. /*
  2. * This file is part of the warpgate module (https://github.com/trioderegion/warpgate)
  3. * Copyright (c) 2021 Matthew Haentschke.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, version 3.
  8. *
  9. * This program is distributed in the hope that it will be useful, but
  10. * WITHOUT ANY WARRANTY; without even the implied warranty of
  11. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. * General Public License for more details.
  13. *
  14. * You should have received a copy of the GNU General Public License
  15. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. */
  17. import {
  18. logger
  19. } from './logger.js';
  20. import {
  21. MODULE
  22. } from './module.js'
  23. /** @typedef {import('./api.js').Shorthand} Shorthand */
  24. /**
  25. * @typedef {Object} MutationData
  26. * @property {Shorthand} delta
  27. * @property {string} user
  28. * @property {Object<string, string>} comparisonKeys
  29. * @property {Shorthand} updateOpts
  30. * @property {object} overrides
  31. * @property {string} name
  32. */
  33. /**
  34. * The following class and its utility methods allows safer and more direct modification of the mutation stack,
  35. * which is stored on a token's actor. This mutation stack stores the information needed to _revert_ the changes
  36. * made by a mutation. This could be used, for example, to deal with rollover damage where the hit point value
  37. * being reverted to is lower than when the mutation was first applied.
  38. *
  39. * Searching and querying a given mutation is a quick, read-only process. When the mutation stack is modified
  40. * via one of its class methods, the actor's mutation data at that point in time will be copied for fast, local updates.
  41. *
  42. * No changes will be made to the actor's serialized data until the changes have been commited ({@link MutationStack#commit}).
  43. * The MutationStack object will then be locked back into a read-only state sourced with this newly updated data.
  44. */
  45. export class MutationStack {
  46. constructor(tokenDoc) {
  47. this.actor = tokenDoc instanceof TokenDocument ? tokenDoc.actor :
  48. tokenDoc instanceof Token ? tokenDoc.document.actor :
  49. tokenDoc instanceof Actor ? tokenDoc :
  50. null;
  51. if(!this.actor) {
  52. throw new Error(MODULE.localize('error.stack.noActor'));
  53. }
  54. }
  55. /**
  56. * Private copy of the working stack (mutable)
  57. * @type {Array<MutationData>}
  58. */
  59. #stack = [];
  60. /** indicates if the stack has been duplicated for modification */
  61. #locked = true;
  62. /**
  63. * Current stack, according to the remote server (immutable)
  64. * @const
  65. * @type {Array<MutationData>}
  66. */
  67. get #liveStack() {
  68. // @ts-ignore
  69. return this.actor?.getFlag(MODULE.data.name, 'mutate') ?? []
  70. }
  71. /**
  72. * Mutation stack according to its lock state.
  73. * @type {Array<MutationData>}
  74. */
  75. get stack() {
  76. return this.#locked ? this.#liveStack : this.#stack ;
  77. }
  78. /**
  79. * @callback FilterFn
  80. * @param {MutationData} mutation
  81. * @returns {boolean} provided mutation meets criteria
  82. * @memberof MutationStack
  83. */
  84. /**
  85. * Searches for an element of the mutation stack that satisfies the provided predicate
  86. *
  87. * @param {FilterFn} predicate Receives the argments of `Array.prototype.find`
  88. * and should return a boolean indicating if the current element satisfies the predicate condition
  89. * @return {MutationData|undefined} Element of the mutation stack that matches the predicate, or undefined if none.
  90. */
  91. find(predicate) {
  92. if (this.#locked) return this.#liveStack.find(predicate);
  93. return this.#stack.find(predicate);
  94. }
  95. /**
  96. * Searches for an element of the mutation stack that satisfies the provided predicate and returns its
  97. * stack index
  98. *
  99. * @param {FilterFn} predicate Receives the argments of {@link Array.findIndex} and returns a Boolean indicating if the current
  100. * element satisfies the predicate condition
  101. * @return {Number} Index of the element of the mutation stack that matches the predicate, or undefined if none.
  102. */
  103. #findIndex( predicate ) {
  104. if (this.#locked) return this.#liveStack.findIndex(predicate);
  105. return this.#stack.findIndex(predicate);
  106. }
  107. /**
  108. * Retrieves an element of the mutation stack that matches the provided name
  109. *
  110. * @param {String} name Name of mutation (serves as a unique identifier)
  111. * @return {MutationData|undefined} Element of the mutation stack matching the provided name, or undefined if none
  112. */
  113. getName(name) {
  114. return this.find((element) => element.name === name);
  115. }
  116. /**
  117. * Retrieves that last mutation added to the mutation stack (i.e. the "newest"),
  118. * or undefined if none present
  119. * @type {MutationData}
  120. */
  121. get last() {
  122. return this.stack[this.stack.length - 1];
  123. }
  124. /**
  125. * Updates the mutation matching the provided name with the provided mutation info.
  126. * The mutation info can be a subset of the full object if (and only if) overwrite is false.
  127. *
  128. * @param {string} name name of mutation to update
  129. * @param {MutationData} data New information, can include 'name'.
  130. * @param {object} options
  131. * @param {boolean} [options.overwrite = false] default will merge the provided info
  132. * with the current values. True will replace the entire entry and requires
  133. * at least the 'name' field.
  134. *
  135. * @return {MutationStack} self, unlocked for writing and updates staged if update successful
  136. */
  137. update(name, data, {
  138. overwrite = false
  139. }) {
  140. const index = this.#findIndex((element) => element.name === name);
  141. if (index < 0) {
  142. return this;
  143. }
  144. this.#unlock();
  145. if (overwrite) {
  146. /* we need at LEAST a name to identify by */
  147. if (!data.name) {
  148. logger.error(MODULE.localize('error.incompleteMutateInfo'));
  149. this.#locked=true;
  150. return this;
  151. }
  152. /* if no user is provided, input current user. */
  153. if (!data.user) data.user = game.user.id;
  154. this.#stack[index] = data;
  155. } else {
  156. /* incomplete mutations are fine with merging */
  157. mergeObject(this.#stack[index], data);
  158. }
  159. return this;
  160. }
  161. /**
  162. * Applies a given change or tranform function to the current buffer,
  163. * unlocking if needed.
  164. *
  165. * @param {MutationData|function(MutationData) : MutationData} transform Object to merge or function to generate an object to merge from provided {@link MutationData}
  166. * @param {FilterFn} [filterFn = () => true] Optional function returning a boolean indicating
  167. * if this element should be modified. By default, affects all elements of the mutation stack.
  168. * @return {MutationStack} self, unlocked for writing and updates staged.
  169. */
  170. updateAll(transform, filterFn = () => true) {
  171. const innerUpdate = (transform) => {
  172. if (typeof transform === 'function') {
  173. /* if we are applying a transform function */
  174. return (element) => mergeObject(element, transform(element));
  175. } else {
  176. /* if we are applying a constant change */
  177. return (element) => mergeObject(element, transform);
  178. }
  179. }
  180. this.#unlock();
  181. this.#stack.forEach((element) => {
  182. if (filterFn(element)) {
  183. innerUpdate(transform)(element);
  184. }
  185. });
  186. return this;
  187. }
  188. /**
  189. * Deletes all mutations from this actor's stack, effectively making
  190. * the current changes permanent.
  191. *
  192. * @param {function(MutationData):boolean} [filterFn = () => true] Optional function returning a boolean indicating if this
  193. * element should be delete. By default, deletes all elements of the mutation stack.
  194. * @return {MutationStack} self, unlocked for writing and updates staged.
  195. */
  196. deleteAll(filterFn = () => true) {
  197. this.#unlock();
  198. this.#stack = this.#stack.filter((element) => !filterFn(element))
  199. return this;
  200. }
  201. /**
  202. * Updates the owning actor with the mutation stack changes made. Will not commit a locked buffer.
  203. *
  204. * @return {Promise<MutationStack>} self, locked for writing
  205. */
  206. async commit() {
  207. if(this.#locked) {
  208. logger.error(MODULE.localize('error.stackLockedOrEmpty'))
  209. }
  210. await this.actor.update({
  211. flags: {
  212. [MODULE.data.name]: {
  213. 'mutate': this.#stack
  214. }
  215. }
  216. });
  217. /* return to a locked read-only state */
  218. this.#locked = true;
  219. this.#stack.length = 0;
  220. return this;
  221. }
  222. /**
  223. * Unlocks the current buffer for writing by copying the mutation stack into this object.
  224. *
  225. * @return {boolean} Indicates if the unlock occured. False indicates the buffer was already unlocked.
  226. */
  227. #unlock() {
  228. if (!this.#locked) {
  229. return false;
  230. }
  231. this.#stack = duplicate(this.#liveStack)
  232. this.#locked = false;
  233. return true;
  234. }
  235. }