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.

620 lines
29 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 { logger } from './logger.js'
  18. import { Gateway } from './gateway.js'
  19. import { Mutator } from './mutator.js'
  20. import { MODULE } from './module.js'
  21. import { Comms } from './comms.js'
  22. import { Events } from './events.js'
  23. import { queueUpdate } from './update-queue.js'
  24. import { Crosshairs } from './crosshairs.js'
  25. import { MutationStack } from './mutation-stack.js'
  26. /** @typedef {import('./crosshairs.js').CrosshairsData} CrosshairsData */
  27. /** @typedef {import('./mutator.js').WorkflowOptions} WorkflowOptions */
  28. /** @typedef {import('./gateway.js').ParallelShow} ParallelShow */
  29. /**
  30. * string-string key-value pairs indicating which field to use for comparisons for each needed embeddedDocument type.
  31. * @typedef {Object<string,string>} ComparisonKeys
  32. * @example
  33. * const comparisonKeys = {
  34. * ActiveEffect: 'label',
  35. * Item: 'name'
  36. * }
  37. */
  38. /*
  39. * @private
  40. * @ignore
  41. * @todo Creating proper type and use in warpgate.dismiss
  42. * @typedef {{overrides: ?{includeRawData: ?WorkflowOptions['overrides']['includeRawData']}}} DismissOptions
  43. */
  44. /**
  45. * Configuration obect for pan and ping (i.e. Notice) operations
  46. * @typedef {Object} NoticeConfig
  47. * @prop {boolean|string} [ping=false] Creates an animated ping at designated location if a valid
  48. * ping style from the values contained in `CONFIG.Canvas.pings.types` is provided, or `'pulse'` if `true`
  49. * @prop {boolean|Number} [pan=false] Pans all receivers to designated location if value is `true`
  50. * using the configured default pan duration of `CONFIG.Canvas.pings.pullSpeed`. If a Number is
  51. * provided, it is used as the duration of the pan.
  52. * @prop {Number} [zoom] Alters zoom level of all receivers, independent of pan/ping
  53. * @prop {string} [sender = game.userId] The user who triggered the notice
  54. * @prop {Array<string>} [receivers = warpgate.USERS.SELF] An array of user IDs to send the notice to. If not
  55. * provided, the notice is only sent to the current user.
  56. */
  57. /**
  58. * Common 'shorthand' notation describing arbitrary data related to a spawn/mutate/revert process.
  59. *
  60. * The `token` and `actor` key values are standard update or options objects as one would use in
  61. * `Actor#update` and `TokenDocument#update`.
  62. *
  63. * The `embedded` key uses a shorthand notation to make creating the updates for embedded documents
  64. * (such as items) easier. Notably, it does not require the `_id` field to be part of the update object
  65. * for a given embedded document type.
  66. *
  67. * @typedef {Object} Shorthand
  68. * @prop {object} [token] Data related to the workflow TokenDocument.
  69. * @prop {object} [actor] Data related to the workflow Actor.
  70. * @prop {Object<string, object|string>} [embedded] Keyed by embedded document class name (e.g. `"Item"` or `"ActiveEffect"`), there are three operations that this object controls -- adding, updating, deleting (in that order).
  71. *
  72. * | Operation | Value Interpretation |
  73. * | :-- | :-- |
  74. * | Add | Given the identifier of a **non-existing** embedded document, the value contains the data object for document creation compatible with `createEmbeddedDocuments`. This object can be constructed in-place by hand, or gotten from a template document and modified using `"Item To Add": game.items.getName("Name of Item").data`. As an example. Note: the name contained in the key will override the corresponding identifier field in the final creation data. |
  75. * | Update | Given a key of an existing document, the value contains the data object compatible with `updateEmbeddedDocuments`|
  76. * | Delete | A value of {@link warpgate.CONST.DELETE} will remove this document (if it exists) from the spawned actor. e.g. `{"Item Name To Delete": warpgate.CONST.DELETE}`|
  77. *
  78. * @see ComparisonKeys
  79. */
  80. /**
  81. * Pre spawn callback. After a location is chosen or provided, but before any
  82. * spawning for _this iteration_ occurs. Used for modifying the spawning data prior to
  83. * each spawning iteration and for potentially skipping certain iterations.
  84. *
  85. * @callback PreSpawn
  86. * @param {{x: number, y: number}} location Desired centerpoint of spawned token.
  87. * @param {Object} updates Current working "updates" object, which is modified for every iteration
  88. * @param {number} iteration Current iteration number (0-indexed) in the case of 'duplicates'
  89. *
  90. * @returns {Promise<boolean>|boolean} Indicating if the _current_ spawning iteration should continue.
  91. */
  92. /**
  93. * Post spawn callback. After the spawning and updating for _this iteration_ occurs.
  94. * Used for modifying the spawning for the next iteration, operations on the TokenDocument directly
  95. * (such as animations or chat messages), and potentially aborting the spawning process entirely.
  96. *
  97. * @callback PostSpawn
  98. * @param {{x: number, y: number}} location Actual centerpoint of spawned token (affected by collision options).
  99. * @param {TokenDocument} spawnedToken Resulting token created for this spawning iteration
  100. * @param {Object} updates Current working "updates" object, which is modified for every iteration
  101. * @param {number} iteration Current iteration number (0-indexed) in the case of 'duplicates'
  102. *
  103. * @returns {Promise<boolean>|boolean} Indicating if this entire spawning process should be aborted (including any remaining duplicates)
  104. */
  105. /**
  106. * This object controls how the crosshairs will be displayed and decorated.
  107. * Each field is optional with its default value listed.
  108. *
  109. * @typedef {Object} CrosshairsConfig
  110. * @property {number} [x=currentMousePosX] Initial x location for display
  111. * @property {number} [y=currentMousePosY] Initial y location for display
  112. * @property {number} [size=1] The initial diameter of the crosshairs outline in grid squares
  113. * @property {string} [icon = 'icons/svg/dice-target.svg'] The icon displayed in the center of the crosshairs
  114. * @property {number} [direction = 0] Initial rotation angle (in degrees) of the displayed icon (if any). 0 degrees corresponds to <0, 1> unit vector (y+ in screen space, or 'down' in "monitor space"). If this is included within a {@link WarpOptions} object, it is treated as a delta change to the token/update's current rotation value. Positive values rotate clockwise; negative values rotate counter-clockwise.
  115. * @property {string} [label = ''] The text to display below the crosshairs outline
  116. * @property {{x:number, y:number}} [labelOffset={x:0,y:0}] Pixel offset from the label's initial relative position below the outline
  117. * @property {*} [tag='crosshairs'] Arbitrary value used to identify this crosshairs object
  118. * @property {boolean} [drawIcon=true] Controls the display of the center icon of the crosshairs
  119. * @property {boolean} [drawOutline=true] Controls the display of the outline circle of the crosshairs
  120. * @property {number} [interval=2] Sub-grid granularity per square. Snap points will be created every 1/`interval`
  121. * grid spaces. Positive values begin snapping at grid intersections. Negative values begin snapping at the
  122. * center of the square. Ex. the default value of 2 produces two snap points -- one at the edge and one at the
  123. * center; `interval` of 1 will snap to grid intersections; `interval` of -1 will snap to grid centers.
  124. * Additionally, a value of `0` will turn off grid snapping completely for this instance of crosshairs.
  125. * @property {number} [fillAlpha=0] Alpha (opacity) of the template's fill color (if any).
  126. * @property {string} [fillColor=game.user.color] Color of the template's fill when no texture is used.
  127. * @property {boolean} [rememberControlled=false] Will restore the previously selected tokens after using crosshairs.
  128. * @property {boolean} [tileTexture=false] Indicates if the texture is tileable and does not need specific
  129. * offset/scaling to be drawn correctly. By default, the chosen texture will be position and scaled such
  130. * that the center of the texture image resides at the center of the crosshairs template.
  131. * @property {boolean} [lockSize=true] Controls the ability of the user to scale the size of the crosshairs
  132. * using shift+scroll. When locked, shift+scroll acts as a "coarse rotation" step for rotating the center icon.
  133. * @property {boolean} [lockPosition=false] Prevents updating the position of the crosshair based on mouse movement. Typically used in combination with the `show` callback to lock position conditionally.
  134. * @property {string} [texture] Asset path of the texture to draw inside the crosshairs border.
  135. */
  136. /**
  137. * @typedef {Object} SpawningOptions
  138. * @property {ComparisonKeys} [comparisonKeys] Data paths relative to root document data used for comparisons of embedded
  139. * shorthand identifiers
  140. * @property {Shorthand} [updateOpts] Options for the creation/deletion/updating of (embedded) documents related to this spawning
  141. * @property {Actor} [controllingActor] will minimize this actor's open sheet (if any) for a clearer view of the canvas
  142. * during placement. Also flags the created token with this actor's id. Default `null`
  143. * @property {number} [duplicates=1] will spawn multiple tokens from a single placement. See also {@link SpawningOptions.collision}
  144. * @property {boolean} [collision=duplicates>1] controls whether the placement of a token collides with any other token
  145. * or wall and finds a nearby unobstructed point (via a radial search) to place the token. If `duplicates` is greater
  146. * than 1, default is `true`; otherwise `false`.
  147. * @property {NoticeConfig} [notice] will pan or ping the canvas to the token's position after spawning.
  148. * @property {object} [overrides] See corresponding property descriptions in {@link WorkflowOptions}
  149. * @property {boolean} [overrides.includeRawData = false]
  150. * @property {boolean} [overrides.preserveData = false]
  151. */
  152. /**
  153. * @typedef {Object} WarpOptions
  154. * @prop {CrosshairsConfig} [crosshairs] A crosshairs configuration object to be used for this spawning process
  155. */
  156. /**
  157. * @class
  158. * @private
  159. */
  160. export class api {
  161. static register() {
  162. api.globals();
  163. }
  164. static settings() {
  165. }
  166. static globals() {
  167. /**
  168. * @global
  169. * @summary Top level (global) symbol providing access to all Warp Gate API functions
  170. * @static
  171. * @namespace warpgate
  172. * @property {warpgate.CONST} CONST
  173. * @property {warpgate.EVENT} EVENT
  174. * @property {warpgate.USERS} USERS
  175. * @borrows api._spawn as spawn
  176. * @borrows api._spawnAt as spawnAt
  177. * @borrows Gateway.dismissSpawn as dismiss
  178. * @borrows Mutator.mutate as mutate
  179. * @borrows Mutator.revertMutation as revert
  180. * @borrows MODULE.wait as wait
  181. * @borrows MODULE.dialog as dialog
  182. * @borrows MODULE.buttonDialog as buttonDialog
  183. * @borrows MODULE.menu as menu
  184. */
  185. window[MODULE.data.name] = {
  186. spawn : api._spawn,
  187. spawnAt : api._spawnAt,
  188. dismiss : Gateway.dismissSpawn,
  189. mutate : Mutator.mutate,
  190. revert : Mutator.revertMutation,
  191. /**
  192. * Factory method for creating a new mutation stack class from
  193. * the provided token document
  194. *
  195. * @memberof warpgate
  196. * @static
  197. * @param {TokenDocument} tokenDoc
  198. * @return {MutationStack} Locked instance of a token actor's mutation stack.
  199. *
  200. * @see {@link MutationStack}
  201. */
  202. mutationStack : (tokenDoc) => new MutationStack(tokenDoc),
  203. wait : MODULE.wait,
  204. dialog : MODULE.dialog,
  205. menu: MODULE.menu,
  206. buttonDialog : MODULE.buttonDialog,
  207. /**
  208. * @summary Utility functions for common queries and operations
  209. * @namespace
  210. * @alias warpgate.util
  211. * @borrows MODULE.firstGM as firstGM
  212. * @borrows MODULE.isFirstGM as isFirstGM
  213. * @borrows MODULE.firstOwner as firstOwner
  214. * @borrows MODULE.isFirstOwner as isFirstOwner
  215. */
  216. util: {
  217. firstGM : MODULE.firstGM,
  218. isFirstGM : MODULE.isFirstGM,
  219. firstOwner : MODULE.firstOwner,
  220. isFirstOwner : MODULE.isFirstOwner,
  221. },
  222. /**
  223. * @summary Crosshairs API Functions
  224. * @namespace
  225. * @alias warpgate.crosshairs
  226. * @borrows Gateway.showCrosshairs as show
  227. * @borrows Crosshairs.getTag as getTag
  228. * @borrows Gateway.collectPlaceables as collectPlaceables
  229. */
  230. crosshairs: {
  231. show: Gateway.showCrosshairs,
  232. getTag: Crosshairs.getTag,
  233. collect: Gateway.collectPlaceables,
  234. },
  235. /**
  236. * @summary APIs intended for warp gate "pylons" (e.g. Warp Gate-dependent modules)
  237. * @namespace
  238. * @alias warpgate.plugin
  239. * @borrows api._notice as notice
  240. * @borrows Mutator.batchMutate as batchMutate
  241. * @borrows Mutator.batchRevert as batchRevert
  242. */
  243. plugin: {
  244. queueUpdate,
  245. notice: api._notice,
  246. batchMutate: Mutator.batchMutate,
  247. batchRevert: Mutator.batchRevert,
  248. },
  249. /**
  250. * @summary System specific helpers
  251. * @namespace
  252. * @alias warpgate.dnd5e
  253. * @borrows Gateway._rollItemGetLevel as rollItem
  254. */
  255. dnd5e : {
  256. rollItem : Gateway._rollItemGetLevel
  257. },
  258. /**
  259. * @description Constants and enums for use in embedded shorthand fields
  260. * @alias warpgate.CONST
  261. * @readonly
  262. * @enum {string}
  263. */
  264. CONST : {
  265. /** Instructs warpgate to delete the identified embedded document. Used in place of the update or create data objects. */
  266. DELETE : 'delete',
  267. },
  268. /**
  269. * @description Helper enums for retrieving user IDs
  270. * @alias warpgate.USERS
  271. * @readonly
  272. * @enum {Array<string>}
  273. * @property {Array<string>} ALL All online users
  274. * @property {Array<string>} SELF The current user
  275. * @property {Array<string>} GM All online GMs
  276. * @property {Array<string>} PLAYERS All online players (non-gms)
  277. */
  278. USERS: {
  279. /** All online users */
  280. get ALL() { return game.users.filter(user => user.active).map( user => user.id ) },
  281. /** The current user */
  282. get SELF() { return [game.userId] },
  283. /** All online GMs */
  284. get GM() { return game.users.filter(user => user.active && user.isGM).map( user => user.id ) },
  285. /** All online players */
  286. get PLAYERS() { return game.users.filter(user => user.active && !user.isGM).map( user => user.id ) }
  287. },
  288. /**
  289. *
  290. * The following table describes the stock event type payloads that are broadcast during {@link warpgate.event.notify}
  291. *
  292. * | Event | Payload | Notes |
  293. * | :-- | -- | -- |
  294. * | `<any>` | `{sceneId: string, userId: string}` | userId is the initiator |
  295. * | {@link warpgate.EVENT.PLACEMENT} | `{templateData: {@link CrosshairsData}|Object, tokenData: TokenData|String('omitted'), options: {@link WarpOptions}} | The final Crosshairs data used to spawn the token, and the final token data that will be spawned. There is no actor data provided. In the case of omitting raw data, `template` data will be of type `{x: number, y: number, size: number, cancelled: boolean}` |
  296. * | SPAWN | `{uuid: string, updates: {@link Shorthand}|String('omitted'), options: {@link WarpOptions}|{@link SpawningOptions}, iteration: number}` | UUID of created token, updates applied to the token, options used for spawning, and iteration this token was spawned on.|
  297. * | DISMISS | `{actorData: {@link PackedActorData}|string}` | `actorData` is a customized version of `Actor#toObject` with its `token` field containing the actual token document data dismissed, instead of its prototype data. |
  298. * | MUTATE | `{uuid: string, updates: {@link Shorthand}, options: {@link WorkflowOptions} & {@link MutationOptions}` | UUID of modified token, updates applied to the token, options used for mutation. When raw data is omitted, `updates` will be `String('omitted')`|
  299. * | REVERT | `{uuid: string, updates: {@link Shorthand}, options: {@link WorkflowOptions}} | UUID is that of reverted token and updates applied to produce the final reverted state (or `String('omitted') if raw data is omitted). |
  300. * | REVERT\_RESPONSE | `{accepted: bool, tokenId: string, mutationId: string, options: {@link WorkflowOptions}` | Indicates acceptance/rejection of the remote revert request, including target identifiers and options |
  301. * | MUTATE\_RESPONSE | `{accepted: bool, tokenId: string, mutationId: string, options: {@link WorkflowOptions}` | `mutationId` is the name provided in `options.name` OR a randomly assigned ID if not provided. Callback functions provided for remote mutations will be internally converted to triggers for this event and do not need to be registered manually by the user. `accepted` is a bool field that indicates if the remote user accepted the mutation. |
  302. *
  303. * @description Event name constants for use with the {@link warpgate.event} system.
  304. * @alias warpgate.EVENT
  305. * @enum {string}
  306. */
  307. EVENT : {
  308. /** After placement is chosen */
  309. PLACEMENT: 'wg_placement',
  310. /** After each token has been spawned and fully updated */
  311. SPAWN: 'wg_spawn',
  312. /** After a token has been dismissed via warpgate */
  313. DISMISS: 'wg_dismiss',
  314. /** After a token has been fully reverted */
  315. REVERT: 'wg_revert',
  316. /** After a token has been fully modified */
  317. MUTATE: 'wg_mutate',
  318. /** Feedback of mutation acceptance/rejection from the remote owning player in
  319. * the case of an "unowned" or remote mutation operation
  320. */
  321. MUTATE_RESPONSE: 'wg_response_mutate',
  322. /** Feedback of mutation revert acceptance/rejection from the remote owning player in
  323. * the case of an "unowned" or remote mutation operation
  324. */
  325. REVERT_RESPONSE: 'wg_response_revert'
  326. },
  327. /**
  328. * Warp Gate includes a hook-like event system that can be used to respond to stages of the
  329. * spawning and mutation process. Additionally, the event system is exposed so that users
  330. * and module authors can create custom events in any context.
  331. *
  332. * @summary Event system API functions.
  333. * @see warpgate.event.notify
  334. *
  335. * @namespace
  336. * @alias warpgate.event
  337. * @borrows Events.watch as watch
  338. * @borrows Events.trigger as trigger
  339. * @borrows Events.remove as remove
  340. * @borrows Comms.notifyEvent as notify
  341. *
  342. */
  343. event : {
  344. watch : Events.watch,
  345. trigger : Events.trigger,
  346. remove : Events.remove,
  347. notify : Comms.notifyEvent,
  348. },
  349. /**
  350. * @summary Warp Gate classes suitable for extension
  351. * @namespace
  352. * @alias warpgate.abstract
  353. * @property {Crosshairs} Crosshairs
  354. * @property {MutationStack} MutationStack
  355. */
  356. abstract : {
  357. Crosshairs,
  358. MutationStack
  359. }
  360. }
  361. }
  362. /**
  363. *
  364. * The primary function of Warp Gate. When executed, it will create a custom MeasuredTemplate
  365. * that is used to place the spawned token and handle any customizations provided in the `updates`
  366. * object. `warpgate#spawn` will return a Promise that can be awaited, which can be used in loops
  367. * to spawn multiple tokens, one after another (or use the `duplicates` options). The player spawning
  368. * the token will also be given Owner permissions for that specific token actor.
  369. * This means that players can spawn any creature available in the world.
  370. *
  371. * @param {String|PrototypeTokenDocument} spawnName Name of actor to spawn or the actual TokenData
  372. * that should be used for spawning.
  373. * @param {Shorthand} [updates] - embedded document, actor, and token document updates. embedded updates use
  374. * a "shorthand" notation.
  375. * @param {Object} [callbacks] The callbacks object as used by spawn and spawnAt provide a way to execute custom
  376. * code during the spawning process. If the callback function modifies updates or location, it is often best
  377. * to do this via `mergeObject` due to pass by reference restrictions.
  378. * @param {PreSpawn} [callbacks.pre]
  379. * @param {PostSpawn} [callbacks.post]
  380. * @param {ParallelShow} [callbacks.show]
  381. * @param {WarpOptions & SpawningOptions} [options]
  382. *
  383. * @return {Promise<Array<String>>} list of created token ids
  384. */
  385. static async _spawn(spawnName, updates = {}, callbacks = {}, options = {}) {
  386. /* check for needed spawning permissions */
  387. const neededPerms = MODULE.canSpawn(game.user);
  388. if(neededPerms.length > 0) {
  389. logger.warn(MODULE.format('error.missingPerms', {permList: neededPerms.join(', ')}));
  390. return [];
  391. }
  392. /* create permissions for this user */
  393. const ownershipKey = MODULE.isV10 ? "ownership" : "permission";
  394. const actorData = {
  395. [ownershipKey]: {[game.user.id]: CONST.DOCUMENT_PERMISSION_LEVELS.OWNER}
  396. }
  397. /* the provided update object will be mangled for our use -- copy it to
  398. * preserve the user's original input if requested (default).
  399. */
  400. if(!options.overrides?.preserveData) {
  401. updates = MODULE.copy(updates, 'error.badUpdate.complex');
  402. if(!updates) return [];
  403. options = foundry.utils.mergeObject(options, {overrides: {preserveData: true}}, {inplace: false});
  404. }
  405. /* insert token updates to modify token actor permission */
  406. MODULE.shimUpdate(updates);
  407. foundry.utils.mergeObject(updates, {token: mergeObject(updates.token ?? {}, {actorData}, {overwrite:false})});
  408. /* Detect if the protoData is actually a name, and generate token data */
  409. let protoData;
  410. if (typeof spawnName == 'string'){
  411. protoData = await MODULE.getTokenData(spawnName, updates.token);
  412. } else {
  413. protoData = spawnName;
  414. protoData.updateSource(updates.token ?? {});
  415. }
  416. if (!protoData) return;
  417. if(options.controllingActor?.sheet?.rendered) options.controllingActor.sheet.minimize();
  418. /* gather data needed for configuring the display of the crosshairs */
  419. const tokenImg = MODULE.isV10 ? protoData.texture.src : protoData.img;
  420. const rotation = updates.token?.rotation ?? protoData.rotation ?? 0;
  421. const crosshairsConfig = foundry.utils.mergeObject(options.crosshairs ?? {}, {
  422. size: protoData.width,
  423. icon: tokenImg,
  424. name: protoData.name,
  425. direction: 0,
  426. }, {inplace: true, overwrite: false});
  427. crosshairsConfig.direction += rotation;
  428. /** @type {CrosshairsData} */
  429. const templateData = await Gateway.showCrosshairs(crosshairsConfig, callbacks);
  430. const eventPayload = {
  431. templateData: (options.overrides?.includeRawData ?? false) ? templateData : {x: templateData.x, y: templateData.y, size: templateData.size, cancelled: templateData.cancelled},
  432. tokenData: (options.overrides?.includeRawData ?? false) ? protoData.toObject() : 'omitted',
  433. options,
  434. }
  435. await warpgate.event.notify(warpgate.EVENT.PLACEMENT, eventPayload);
  436. if (templateData.cancelled) return;
  437. let spawnLocation = {x: templateData.x, y:templateData.y}
  438. /* calculate any scaling that may have happened */
  439. const scale = templateData.size / protoData.width;
  440. /* insert changes from the template into the updates data */
  441. mergeObject(updates, {token: {rotation: templateData.direction, width: templateData.size, height: protoData.height*scale}});
  442. return api._spawnAt(spawnLocation, protoData, updates, callbacks, options);
  443. }
  444. /**
  445. * An alternate, more module friendly spawning function. Will create a token from the provided token data and updates at the designated location.
  446. *
  447. * @param {{x: number, y: number}} spawnLocation Centerpoint of spawned token
  448. * @param {String|PrototypeTokenData|TokenData|PrototypeTokenDocument} protoData Any token data or the name of a world-actor. Serves as the base data for all operations.
  449. * @param {Shorthand} [updates] As {@link warpgate.spawn}
  450. * @param {Object} [callbacks] see {@link warpgate.spawn}
  451. * @param {PreSpawn} [callbacks.pre]
  452. * @param {PostSpawn} [callbacks.post]
  453. * @param {SpawningOptions} [options] Modifies behavior of the spawning process.
  454. *
  455. * @return {Promise<Array<string>>} list of created token ids
  456. *
  457. */
  458. static async _spawnAt(spawnLocation, protoData, updates = {}, callbacks = {}, options = {}) {
  459. /* check for needed spawning permissions */
  460. const neededPerms = MODULE.canSpawn(game.user);
  461. if(neededPerms.length > 0) {
  462. logger.warn(MODULE.format('error.missingPerms', {permList: neededPerms.join(', ')}));
  463. return [];
  464. }
  465. /* the provided update object will be mangled for our use -- copy it to
  466. * preserve the user's original input if requested (default).
  467. */
  468. if(!options.overrides?.preserveData) {
  469. updates = MODULE.copy(updates, 'error.badUpdate.complex');
  470. if(!updates) return [];
  471. options = foundry.utils.mergeObject(options, {overrides: {preserveData: true}}, {inplace: false});
  472. }
  473. MODULE.shimUpdate(updates);
  474. /* Detect if the protoData is actually a name, and generate token data */
  475. if (typeof protoData == 'string'){
  476. protoData = await MODULE.getTokenData(protoData, updates.token ?? {});
  477. }
  478. if (!protoData) return [];
  479. let createdIds = [];
  480. /* flag this user as the tokens's creator */
  481. const actorFlags = {
  482. [MODULE.data.name]: {
  483. control: {user: game.user.id, actor: options.controllingActor?.uuid},
  484. }
  485. }
  486. /* create permissions for this user */
  487. const actorData = {
  488. ownership: {[game.user.id]: CONST.DOCUMENT_PERMISSION_LEVELS.OWNER}
  489. }
  490. updates.token = mergeObject({actorData}, updates.token ?? {}, {inplace: false})
  491. updates.actor = mergeObject({flags: actorFlags}, updates.actor ?? {}, {inplace: false})
  492. const duplicates = options.duplicates > 0 ? options.duplicates : 1;
  493. Mutator.clean(null, options);
  494. if(options.notice) warpgate.plugin.notice({...spawnLocation, scene: canvas.scene}, options.notice);
  495. for (let iteration = 0; iteration < duplicates; iteration++) {
  496. /** pre creation callback */
  497. if (callbacks.pre) {
  498. const response = await callbacks.pre(spawnLocation, updates, iteration);
  499. /* pre create callbacks can skip this spawning iteration */
  500. if(response === false) continue;
  501. }
  502. await Mutator.clean(updates);
  503. /* merge in changes to the prototoken */
  504. if(iteration == 0){
  505. /* first iteration, potentially from a spawn with a determined image,
  506. * apply our changes to this version */
  507. await MODULE.updateProtoToken(protoData, updates.token);
  508. } else {
  509. /* get a fresh copy */
  510. protoData = await MODULE.getTokenData(game.actors.get(protoData.actorId), updates.token)
  511. }
  512. logger.debug(`Spawn iteration ${iteration} using`, protoData, updates);
  513. /* pan to token if first iteration */
  514. //TODO integrate into stock event data instead of hijacking mutate events
  515. /** @type Object */
  516. const spawnedTokenDoc = (await Gateway._spawnTokenAtLocation(protoData,
  517. spawnLocation,
  518. options.collision ?? (options.duplicates > 1)))[0];
  519. createdIds.push(spawnedTokenDoc.id);
  520. logger.debug('Spawned token with data: ', MODULE.isV10 ? spawnedTokenDoc : spawnedTokenDoc.data);
  521. await Mutator._updateActor(spawnedTokenDoc.actor, updates, options.comparisonKeys ?? {});
  522. const eventPayload = {
  523. uuid: spawnedTokenDoc.uuid,
  524. updates: (options.overrides?.includeRawData ?? false) ? updates : 'omitted',
  525. options,
  526. iteration
  527. }
  528. await warpgate.event.notify(warpgate.EVENT.SPAWN, eventPayload);
  529. /* post creation callback */
  530. if (callbacks.post) {
  531. const response = await callbacks.post(spawnLocation, spawnedTokenDoc, updates, iteration);
  532. if(response === false) break;
  533. }
  534. }
  535. if (options.controllingActor?.sheet?.rendered) options.controllingActor?.sheet?.maximize();
  536. return createdIds;
  537. }
  538. /**
  539. * Helper function for displaying pings for or panning the camera of specific users. If no scene is provided, the user's current
  540. * is assumed.
  541. *
  542. * @param {{x: Number, y: Number, scene: Scene} | CrosshairsData} placement Information for the physical placement of the notice containing at least `{x: Number, y: Number, scene: Scene}`
  543. * @param {NoticeConfig} [config] Configuration for the notice
  544. */
  545. static _notice({x, y, scene}, config = {}){
  546. config.sender ??= game.userId;
  547. config.receivers ??= warpgate.USERS.SELF;
  548. scene ??= canvas.scene;
  549. return Comms.requestNotice({x,y}, scene.id, config);
  550. }
  551. }