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.

304 lines
10 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 {MODULE} from './module.js'
  19. import {Crosshairs} from './crosshairs.js'
  20. import { Comms } from './comms.js'
  21. import {Propagator} from './propagator.js'
  22. const NAME = "Gateway";
  23. /** @typedef {import('./api.js').CrosshairsConfig} CrosshairsConfig */
  24. /** @typedef {import('./crosshairs.js').CrosshairsData} CrosshairsData */
  25. /**
  26. * Callback started just prior to the crosshairs template being drawn. Is not awaited. Used for modifying
  27. * how the crosshairs is displayed and for responding to its displayed position
  28. *
  29. * All of the fields in the {@link CrosshairsConfig} object can be modified directly. Any fields owned by
  30. * MeasuredTemplate must be changed via `update|updateSource` as other DocumentData|DataModel classes.
  31. * Async functions will run in parallel while the user is moving the crosshairs. Serial functions will
  32. * block detection of the left and right click operations until return.
  33. *
  34. * @typedef {function(Crosshairs):any} ParallelShow
  35. * @param {Crosshairs} crosshairs The live Crosshairs instance associated with this callback
  36. *
  37. * @returns {any}
  38. */
  39. /**
  40. * @class
  41. * @private
  42. */
  43. export class Gateway {
  44. static register() {
  45. this.settings();
  46. this.defaults();
  47. }
  48. static settings() {
  49. const config = true;
  50. const settingsData = {
  51. openDelete : {
  52. scope: "world", config, default: false, type: Boolean,
  53. },
  54. updateDelay : {
  55. scope: "client", config, default: 0, type: Number
  56. }
  57. };
  58. MODULE.applySettings(settingsData);
  59. }
  60. static defaults() {
  61. MODULE[NAME] = {
  62. /**
  63. * type {CrosshairsConfig}
  64. * @const
  65. */
  66. get crosshairsConfig() {
  67. return {
  68. size: 1,
  69. icon: 'icons/svg/dice-target.svg',
  70. label: '',
  71. labelOffset: {
  72. x: 0,
  73. y: 0
  74. },
  75. tag: 'crosshairs',
  76. drawIcon: true,
  77. drawOutline: true,
  78. interval: 2,
  79. fillAlpha: 0,
  80. tileTexture: false,
  81. lockSize: true,
  82. lockPosition: false,
  83. rememberControlled: false,
  84. //Measured template defaults
  85. texture: null,
  86. //x: 0,
  87. //y: 0,
  88. direction: 0,
  89. fillColor: game.user.color,
  90. }
  91. }
  92. }
  93. }
  94. /**
  95. * dnd5e helper function
  96. * @param { Item5e } item
  97. * @param {Object} [options={}]
  98. * @param {Object} [config={}] V10 Only field
  99. * @todo abstract further out of core code
  100. */
  101. static async _rollItemGetLevel(item, options = {}, config = {}) {
  102. const result = MODULE.isV10 ? await item.use(config, options) : await item.roll(options);
  103. // extract the level at which the spell was cast
  104. if (!result) return 0;
  105. const content = MODULE.isV10 ? result.content : result.data.content;
  106. const level = content.charAt(content.indexOf("data-spell-level") + 18);
  107. return parseInt(level);
  108. }
  109. /**
  110. * Displays a circular template attached to the mouse cursor that snaps to grid centers
  111. * and grid intersections.
  112. *
  113. * Its size is in grid squares/hexes and can be scaled up and down via shift+mouse scroll.
  114. * Resulting data indicates the final position and size of the template. Note: Shift+Scroll
  115. * will increase/decrease the size of the crosshairs outline, which increases or decreases
  116. * the size of the token spawned, independent of other modifications.
  117. *
  118. * @param {CrosshairsConfig} [config] Configuration settings for how the crosshairs template should be displayed.
  119. * @param {Object} [callbacks] Functions executed at certain stages of the crosshair display process.
  120. * @param {ParallelShow} [callbacks.show]
  121. *
  122. * @returns {Promise<CrosshairsData>} All fields contained by `MeasuredTemplateDocument#toObject`. Notably `x`, `y`,
  123. * `width` (in pixels), and the addition of `size` (final size, in grid units, e.g. "2" for a final diameter of 2 squares).
  124. *
  125. */
  126. static async showCrosshairs(config = {}, callbacks = {}) {
  127. /* add in defaults */
  128. mergeObject(config, MODULE[NAME].crosshairsConfig, {overwrite: false})
  129. /* store currently controlled tokens */
  130. let controlled = [];
  131. if (config.rememberControlled) {
  132. controlled = canvas.tokens.controlled;
  133. }
  134. /* if a specific initial location is not provided, grab the current mouse location */
  135. if(!config.hasOwnProperty('x') && !config.hasOwnProperty('y')) {
  136. let mouseLoc = MODULE.getMouseStagePos();
  137. mouseLoc = Crosshairs.getSnappedPosition(mouseLoc, config.interval);
  138. config.x = mouseLoc.x;
  139. config.y = mouseLoc.y;
  140. }
  141. const template = new Crosshairs(config, callbacks);
  142. await template.drawPreview();
  143. const dataObj = template.toObject();
  144. /* if we have stored any controlled tokens,
  145. * restore that control now
  146. */
  147. for( const token of controlled ){
  148. token.control({releaseOthers: false});
  149. }
  150. return dataObj;
  151. }
  152. /* tests if a placeable's center point is within
  153. * the radius of the crosshairs
  154. */
  155. static _containsCenter(placeable, crosshairsData) {
  156. const calcDistance = (A, B) => { return Math.hypot(A.x-B.x, A.y-B.y) };
  157. const distance = calcDistance(placeable.center, crosshairsData);
  158. return distance <= crosshairsData.radius;
  159. }
  160. /**
  161. * Returns desired types of placeables whose center point
  162. * is within the crosshairs radius.
  163. *
  164. * @param {Object} crosshairsData Requires at least {x,y,radius,parent} (all in pixels, parent is a Scene)
  165. * @param {String|Array<String>} [types='Token'] Collects the desired embedded placeable types.
  166. * @param {Function} [containedFilter=Gateway._containsCenter]. Optional function for determining if a placeable
  167. * is contained by the crosshairs. Default function tests for centerpoint containment. {@link Gateway._containsCenter}
  168. *
  169. * @return {Object<String,PlaceableObject>} List of collected placeables keyed by embeddedName
  170. */
  171. static collectPlaceables( crosshairsData, types = 'Token', containedFilter = Gateway._containsCenter ) {
  172. const isArray = types instanceof Array;
  173. types = isArray ? types : [types];
  174. const result = types.reduce( (acc, embeddedName) => {
  175. const collection = crosshairsData.scene.getEmbeddedCollection(embeddedName);
  176. let contained = collection.filter( (document) => {
  177. return containedFilter(document.object, crosshairsData);
  178. });
  179. acc[embeddedName] = contained;
  180. return acc;
  181. }, {});
  182. /* if we are only collecting one kind of placeable, only return one kind of placeable */
  183. return isArray ? result : result[types[0]];
  184. }
  185. /**
  186. * Deletes the specified token from the specified scene. This function allows anyone
  187. * to delete any specified token unless this functionality is restricted to only
  188. * owned tokens in Warp Gate's module settings. This is the same function called
  189. * by the "Dismiss" header button on owned actor sheets.
  190. *
  191. * @param {string} tokenId
  192. * @param {string} [sceneId = canvas.scene.id] Needed if the dismissed token does not reside
  193. * on the currently viewed scene
  194. * @param {string} [onBehalf = game.user.id] Impersonate another user making this request
  195. */
  196. static async dismissSpawn(tokenId, sceneId = canvas.scene?.id, onBehalf = game.user.id) {
  197. if (!tokenId || !sceneId){
  198. logger.debug("Cannot dismiss null token or from a null scene.", tokenId, sceneId);
  199. return;
  200. }
  201. const tokenData = game.scenes.get(sceneId)?.getEmbeddedDocument("Token",tokenId);
  202. if(!tokenData){
  203. logger.debug(`Token [${tokenId}] no longer exists on scene [${sceneId}]`);
  204. return;
  205. }
  206. /* check for permission to delete freely */
  207. if (!MODULE.setting('openDelete')) {
  208. /* check permissions on token */
  209. if (!tokenData.isOwner) {
  210. logger.error(MODULE.localize('error.unownedDelete'));
  211. return;
  212. }
  213. }
  214. logger.debug("Deleting token =>", tokenId, "from scene =>", sceneId);
  215. if (!MODULE.firstGM()){
  216. logger.error(MODULE.localize('error.noGm'));
  217. return;
  218. }
  219. /** first gm drives */
  220. if (MODULE.isFirstGM()) {
  221. const tokenDocs = await game.scenes.get(sceneId).deleteEmbeddedDocuments("Token",[tokenId]);
  222. const actorData = Comms.packToken(tokenDocs[0]);
  223. await warpgate.event.notify(warpgate.EVENT.DISMISS, {actorData}, onBehalf);
  224. } else {
  225. /** otherwise, we need to send a request for deletion */
  226. Comms.requestDismissSpawn(tokenId, sceneId);
  227. }
  228. return;
  229. }
  230. /**
  231. * returns promise of token creation
  232. * @param {PrototypeTokenDocument} protoToken
  233. * @param {{ x: number, y: number }} spawnPoint
  234. * @param {boolean} collision
  235. */
  236. static async _spawnTokenAtLocation(protoToken, spawnPoint, collision) {
  237. // Increase this offset for larger summons
  238. const gridSize = MODULE.isV10 ? canvas.scene.grid.size : canvas.scene.data.grid;
  239. let internalSpawnPoint = {x: spawnPoint.x - (gridSize * (protoToken.width/2)),
  240. y:spawnPoint.y - (gridSize * (protoToken.height/2))}
  241. /* call ripper's placement algorithm for collision checks
  242. * which will try to avoid tokens and walls
  243. */
  244. if (collision) {
  245. const openPosition = Propagator.getFreePosition(protoToken, internalSpawnPoint);
  246. if(!openPosition) {
  247. logger.info(MODULE.localize('error.noOpenLocation'));
  248. } else {
  249. internalSpawnPoint = openPosition
  250. }
  251. }
  252. if ( MODULE.isV10 ) protoToken.updateSource(internalSpawnPoint);
  253. else protoToken.update(internalSpawnPoint);
  254. return canvas.scene.createEmbeddedDocuments("Token", [protoToken])
  255. }
  256. }