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.

529 lines
16 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. /** @typedef {import('@league-of-foundry-developers/foundry-vtt-types/src/foundry/common/data/data.mjs/measuredTemplateData.js').MeasuredTemplateDataProperties} MeasuredTemplateProperties */
  20. /**
  21. * Contains all fields from `MeasuredTemplate#toObject`, plus the following.
  22. *
  23. * @typedef {Object} CrosshairsData
  24. * @borrows MeasuredTemplateProperties
  25. * @prop {boolean} cancelled Workflow cancelled via right click (true)
  26. * @prop {Scene} scene Scene on this crosshairs was last active
  27. * @prop {number} radius Final radius of template, in pixels
  28. * @prop {number} size Final diameter of template, in grid units
  29. */
  30. /**
  31. * @class
  32. */
  33. export class Crosshairs extends MeasuredTemplate {
  34. //constructor(gridSize = 1, data = {}){
  35. constructor(config, callbacks = {}) {
  36. const templateData = {
  37. t: "circle",
  38. user: game.user.id,
  39. distance: config.size,
  40. x: config.x,
  41. y: config.y,
  42. fillColor: config.fillColor,
  43. width: 1,
  44. texture: config.texture,
  45. direction: config.direction,
  46. }
  47. const template = new CONFIG.MeasuredTemplate.documentClass(templateData, {parent: canvas.scene});
  48. super(template);
  49. /** @TODO all of these fields should be part of the source data schema for this class **/
  50. /** image path to display in the center (under mouse cursor) */
  51. this.icon = config.icon ?? Crosshairs.ERROR_TEXTURE;
  52. /** text to display below crosshairs' circle */
  53. this.label = config.label;
  54. /** Offsets the default position of the label (in pixels) */
  55. this.labelOffset = config.labelOffset;
  56. /**
  57. * Arbitrary field used to identify this instance
  58. * of a Crosshairs in the canvas.templates.preview
  59. * list
  60. */
  61. this.tag = config.tag;
  62. /** Should the center icon be shown? */
  63. this.drawIcon = config.drawIcon;
  64. /** Should the outer circle be shown? */
  65. this.drawOutline = config.drawOutline;
  66. /** Opacity of the fill color */
  67. this.fillAlpha = config.fillAlpha;
  68. /** Should the texture (if any) be tiled
  69. * or scaled and offset? */
  70. this.tileTexture = config.tileTexture;
  71. /** locks the size of crosshairs (shift+scroll) */
  72. this.lockSize = config.lockSize;
  73. /** locks the position of crosshairs */
  74. this.lockPosition = config.lockPosition;
  75. /** Number of quantization steps along
  76. * a square's edge (N+1 snap points
  77. * along each edge, conting endpoints)
  78. */
  79. this.interval = config.interval;
  80. /** Callback functions to execute
  81. * at particular times
  82. */
  83. this.callbacks = callbacks;
  84. /** Indicates if the user is actively
  85. * placing the crosshairs.
  86. * Setting this to true in the show
  87. * callback will stop execution
  88. * and report the current mouse position
  89. * as the chosen location
  90. */
  91. this.inFlight = false;
  92. /** indicates if the placement of
  93. * crosshairs was canceled (with
  94. * a right click)
  95. */
  96. this.cancelled = true;
  97. /**
  98. * Indicators on where cancel was initiated
  99. * for determining if it was a drag or a cancel
  100. */
  101. this.rightX = 0;
  102. this.rightY = 0;
  103. /** @type {number} */
  104. this.radius = this.document.distance * this.scene.grid.size / 2;
  105. }
  106. /**
  107. * @returns {CrosshairsData} Current Crosshairs class data
  108. */
  109. toObject() {
  110. /** @type {CrosshairsData} */
  111. const data = foundry.utils.mergeObject(this.document.toObject(), {
  112. cancelled: this.cancelled,
  113. scene: this.scene,
  114. radius: this.radius,
  115. size: this.document.distance,
  116. });
  117. delete data.width;
  118. return data;
  119. }
  120. static ERROR_TEXTURE = 'icons/svg/hazard.svg'
  121. /**
  122. * Will retrieve the active crosshairs instance with the defined tag identifier.
  123. * @param {string} key Crosshairs identifier. Will be compared against the Crosshairs `tag` field for strict equality.
  124. * @returns {PIXI.DisplayObject|undefined}
  125. */
  126. static getTag(key) {
  127. return canvas.templates.preview.children.find( child => child.tag === key )
  128. }
  129. static getSnappedPosition({x,y}, interval){
  130. const offset = interval < 0 ? canvas.grid.size/2 : 0;
  131. const snapped = canvas.grid.getSnappedPosition(x - offset, y - offset, interval);
  132. return {x: snapped.x + offset, y: snapped.y + offset};
  133. }
  134. /* -----------EXAMPLE CODE FROM MEASUREDTEMPLATE.JS--------- */
  135. /* Portions of the core package (MeasuredTemplate) repackaged
  136. * in accordance with the "Limited License Agreement for Module
  137. * Development, found here: https://foundryvtt.com/article/license/
  138. * Changes noted where possible
  139. */
  140. /**
  141. * Set the displayed ruler tooltip text and position
  142. * @private
  143. */
  144. //BEGIN WARPGATE
  145. _setRulerText() {
  146. this.ruler.text = this.label;
  147. /** swap the X and Y to use the default dx/dy of a ray (pointed right)
  148. //to align the text to the bottom of the template */
  149. this.ruler.position.set(-this.ruler.width / 2 + this.labelOffset.x, this.template.height / 2 + 5 + this.labelOffset.y);
  150. //END WARPGATE
  151. }
  152. /** @override */
  153. async draw() {
  154. this.clear();
  155. // Load the texture
  156. const texture = this.document.texture;
  157. if ( texture ) {
  158. this._texture = await loadTexture(texture, {fallback: 'icons/svg/hazard.svg'});
  159. } else {
  160. this._texture = null;
  161. }
  162. // Template shape
  163. this.template = this.addChild(new PIXI.Graphics());
  164. // Rotation handle
  165. //BEGIN WARPGATE
  166. //this.handle = this.addChild(new PIXI.Graphics());
  167. //END WARPGATE
  168. // Draw the control icon
  169. //if(this.drawIcon)
  170. this.controlIcon = this.addChild(this._drawControlIcon());
  171. // Draw the ruler measurement
  172. this.ruler = this.addChild(this._drawRulerText());
  173. // Update the shape and highlight grid squares
  174. this.refresh();
  175. //BEGIN WARPGATE
  176. this._setRulerText();
  177. //this.highlightGrid();
  178. //END WARPGATE
  179. // Enable interactivity, only if the Tile has a true ID
  180. if ( this.id ) this.activateListeners();
  181. return this;
  182. }
  183. /**
  184. * Draw the Text label used for the MeasuredTemplate
  185. * @return {PreciseText}
  186. * @protected
  187. */
  188. _drawRulerText() {
  189. const style = CONFIG.canvasTextStyle.clone();
  190. style.fontSize = Math.max(Math.round(canvas.dimensions.size * 0.36 * 12) / 12, 36);
  191. const text = new PreciseText(null, style);
  192. //BEGIN WARPGATE
  193. //text.anchor.set(0.5, 0);
  194. text.anchor.set(0, 0);
  195. //END WARPGATE
  196. return text;
  197. }
  198. /**
  199. * Draw the ControlIcon for the MeasuredTemplate
  200. * @return {ControlIcon}
  201. * @protected
  202. */
  203. _drawControlIcon() {
  204. const size = Math.max(Math.round((canvas.dimensions.size * 0.5) / 20) * 20, 40);
  205. //BEGIN WARPGATE
  206. let icon = new ControlIcon({texture: this.icon, size: size});
  207. icon.visible = this.drawIcon;
  208. //END WARPGATE
  209. icon.pivot.set(size*0.5, size*0.5);
  210. //icon.x -= (size * 0.5);
  211. //icon.y -= (size * 0.5);
  212. icon.angle = this.document.direction;
  213. return icon;
  214. }
  215. /** @override */
  216. refresh() {
  217. if (!this.template) return;
  218. let d = canvas.dimensions;
  219. const document = this.document;
  220. this.position.set(document.x, document.y);
  221. // Extract and prepare data
  222. let {direction, distance} = document;
  223. distance *= (d.size/2);
  224. //BEGIN WARPGATE
  225. //width *= (d.size / d.distance);
  226. //END WARPGATE
  227. direction = Math.toRadians(direction);
  228. // Create ray and bounding rectangle
  229. this.ray = Ray.fromAngle(document.x, document.y, direction, distance);
  230. // Get the Template shape
  231. switch (document.t) {
  232. case "circle":
  233. this.shape = this._getCircleShape(distance);
  234. break;
  235. default: logger.error("Non-circular Crosshairs is unsupported!");
  236. }
  237. // Draw the Template outline
  238. this.template.clear()
  239. .lineStyle(this._borderThickness, this.borderColor, this.drawOutline ? 0.75 : 0)
  240. // Fill Color or Texture
  241. if (this._texture) {
  242. /* assume 0,0 is top left of texture
  243. * and scale/offset this texture (due to origin
  244. * at center of template). tileTexture indicates
  245. * that this texture is tilable and does not
  246. * need to be scaled/offset */
  247. const scale = this.tileTexture ? 1 : distance * 2 / this._texture.width;
  248. const offset = this.tileTexture ? 0 : distance;
  249. this.template.beginTextureFill({
  250. texture: this._texture,
  251. matrix: new PIXI.Matrix().scale(scale, scale).translate(-offset, -offset)
  252. });
  253. } else {
  254. this.template.beginFill(this.fillColor, this.fillAlpha);
  255. }
  256. // Draw the shape
  257. this.template.drawShape(this.shape);
  258. // Draw origin and destination points
  259. //BEGIN WARPGATE
  260. //this.template.lineStyle(this._borderThickness, 0x000000, this.drawOutline ? 0.75 : 0)
  261. // .beginFill(0x000000, 0.5)
  262. //.drawCircle(0, 0, 6)
  263. //.drawCircle(this.ray.dx, this.ray.dy, 6);
  264. //END WARPGATE
  265. // Update visibility
  266. if (this.drawIcon) {
  267. this.controlIcon.visible = true;
  268. this.controlIcon.border.visible = this._hover
  269. this.controlIcon.angle = document.direction;
  270. }
  271. // Draw ruler text
  272. //BEGIN WARPGATE
  273. this._setRulerText()
  274. //END WARPGATE
  275. return this;
  276. }
  277. /* END MEASUREDTEMPLATE.JS USAGE */
  278. /* -----------EXAMPLE CODE FROM ABILITY-TEMPLATE.JS--------- */
  279. /* Foundry VTT 5th Edition
  280. * Copyright (C) 2019 Foundry Network
  281. *
  282. * This program is free software: you can redistribute it and/or modify
  283. * it under the terms of the GNU General Public License as published by
  284. * the Free Software Foundation, either version 3 of the License, or
  285. * (at your option) any later version.
  286. *
  287. * This program is distributed in the hope that it will be useful,
  288. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  289. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  290. * GNU General Public License for more details.
  291. *
  292. * Original License:
  293. * https://gitlab.com/foundrynet/dnd5e/-/blob/master/LICENSE.txt
  294. */
  295. /**
  296. * Creates a preview of the spell template
  297. */
  298. async drawPreview() {
  299. // Draw the template and switch to the template layer
  300. this.initialLayer = canvas.activeLayer;
  301. this.layer.activate();
  302. this.draw();
  303. this.layer.preview.addChild(this);
  304. this.layer.interactiveChildren = false;
  305. // Hide the sheet that originated the preview
  306. //BEGIN WARPGATE
  307. this.inFlight = true;
  308. // Activate interactivity
  309. this.activatePreviewListeners();
  310. // Callbacks
  311. this.callbacks?.show?.(this);
  312. /* wait _indefinitely_ for placement to be decided. */
  313. await MODULE.waitFor(() => !this.inFlight, -1)
  314. if (this.activeHandlers) {
  315. this.clearHandlers();
  316. }
  317. //END WARPGATE
  318. return this;
  319. }
  320. /* -------------------------------------------- */
  321. _mouseMoveHandler(event) {
  322. event.stopPropagation();
  323. /* if our position is locked, do not update it */
  324. if (this.lockPosition) return;
  325. // Apply a 20ms throttle
  326. let now = Date.now();
  327. if (now - this.moveTime <= 20) return;
  328. const center = event.data.getLocalPosition(this.layer);
  329. const {x,y} = Crosshairs.getSnappedPosition(center, this.interval);
  330. this.document.updateSource({x, y});
  331. this.refresh();
  332. this.moveTime = now;
  333. canvas._onDragCanvasPan(event.data.originalEvent);
  334. }
  335. _leftClickHandler(event) {
  336. const document = this.document;
  337. const thisSceneSize = this.scene.grid.size;
  338. const destination = Crosshairs.getSnappedPosition(this.document, this.interval);
  339. this.radius = document.distance * thisSceneSize / 2;
  340. this.cancelled = false;
  341. this.document.updateSource({ ...destination });
  342. this.clearHandlers(event);
  343. }
  344. // Rotate the template by 3 degree increments (mouse-wheel)
  345. // none = rotate 5 degrees
  346. // shift = scale size
  347. // ctrl = rotate 30 or 15 degrees (square/hex)
  348. // alt = zoom canvas
  349. _mouseWheelHandler(event) {
  350. if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
  351. if (!event.altKey) event.stopPropagation();
  352. const delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
  353. const snap = event.ctrlKey ? delta : 5;
  354. //BEGIN WARPGATE
  355. const document = this.document;
  356. const thisSceneSize = this.scene.grid.size;
  357. if (event.shiftKey && !this.lockSize) {
  358. let distance = document.distance + 0.25 * (Math.sign(event.deltaY));
  359. distance = Math.max(distance, 0.25);
  360. this.document.updateSource({ distance });
  361. this.radius = document.distance * thisSceneSize / 2;
  362. } else if (!event.altKey) {
  363. const direction = document.direction + (snap * Math.sign(event.deltaY));
  364. this.document.updateSource({ direction });
  365. }
  366. //END WARPGATE
  367. this.refresh();
  368. }
  369. _rightDownHandler(event) {
  370. if (event.button !== 2) return;
  371. this.rightX = event.screenX;
  372. this.rightY = event.screenY;
  373. }
  374. _rightUpHandler(event) {
  375. if (event.button !== 2) return;
  376. const isWithinThreshold = (current, previous) => Math.abs(current - previous) < 10;
  377. if (isWithinThreshold(this.rightX, event.screenX)
  378. && isWithinThreshold(this.rightY, event.screenY)
  379. ) {
  380. this.cancelled = true;
  381. this.clearHandlers(event);
  382. }
  383. }
  384. _clearHandlers(event) {
  385. //WARPGATE BEGIN
  386. /* remove only ourselves, in case of multiple */
  387. this.layer.preview.removeChild(this);
  388. canvas.stage.off("mousemove", this.activeMoveHandler);
  389. canvas.stage.off("mousedown", this.activeLeftClickHandler);
  390. canvas.app.view.onmousedown = null;
  391. canvas.app.view.onmouseup = null;
  392. canvas.app.view.onwheel = null;
  393. //WARPGATE END
  394. /* re-enable interactivity on this layer */
  395. this.layer.interactiveChildren = true;
  396. /* moving off this layer also deletes ALL active previews?
  397. * unexpected, but manageable
  398. */
  399. if (this.layer.preview.children.length == 0) {
  400. this.initialLayer.activate();
  401. }
  402. //BEGIN WARPGATE
  403. // Show the sheet that originated the preview
  404. if (this.actorSheet) this.actorSheet.maximize();
  405. this.activeHandlers = false;
  406. this.inFlight = false;
  407. /* mark this pixi element as destroyed */
  408. this._destroyed = true;
  409. //END WARPGATE
  410. }
  411. /**
  412. * Activate listeners for the template preview
  413. */
  414. activatePreviewListeners() {
  415. this.moveTime = 0;
  416. //BEGIN WARPGATE
  417. this.activeHandlers = true;
  418. /* Activate listeners */
  419. this.activeMoveHandler = this._mouseMoveHandler.bind(this);
  420. this.activeLeftClickHandler = this._leftClickHandler.bind(this);
  421. this.rightDownHandler = this._rightDownHandler.bind(this);
  422. this.rightUpHandler = this._rightUpHandler.bind(this);
  423. this.activeWheelHandler = this._mouseWheelHandler.bind(this);
  424. this.clearHandlers = this._clearHandlers.bind(this);
  425. // Update placement (mouse-move)
  426. canvas.stage.on("mousemove", this.activeMoveHandler);
  427. // Confirm the workflow (left-click)
  428. canvas.stage.on("mousedown", this.activeLeftClickHandler);
  429. // Mouse Wheel rotate
  430. canvas.app.view.onwheel = this.activeWheelHandler;
  431. // Right click cancel
  432. canvas.app.view.onmousedown = this.rightDownHandler;
  433. canvas.app.view.onmouseup = this.rightUpHandler;
  434. // END WARPGATE
  435. }
  436. /** END ABILITY-TEMPLATE.JS USAGE */
  437. }