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. "use strict";
  2. import {
  3. getColorForDistanceAndToken,
  4. getMovedDistanceFromToken,
  5. getRangesFromSpeedProvider,
  6. initApi,
  7. registerModule,
  8. registerSystem,
  9. } from "./api.js";
  10. import {checkDependencies} from "./compatibility.js";
  11. import {moveEntities, onMouseMove} from "./foundry_imports.js";
  12. import {disableSnap, registerKeybindings} from "./keybindings.js";
  13. import {libWrapper} from "./libwrapper_shim.js";
  14. import {performMigrations} from "./migration.js";
  15. import {removeLastHistoryEntryIfAt, resetMovementHistory} from "./movement_tracking.js";
  16. import {extendRuler} from "./ruler.js";
  17. import {registerSettings, RightClickAction, settingsKey} from "./settings.js";
  18. import {recalculate} from "./socket.js";
  19. import {SpeedProvider} from "./speed_provider.js";
  20. import {getEntityCenter, setSnapParameterOnOptions} from "./util.js";
  21. CONFIG.debug.dragRuler = false;
  22. export let debugGraphics = undefined;
  23. Hooks.once("init", () => {
  24. registerSettings();
  25. registerKeybindings();
  26. initApi();
  27. hookDragHandlers(Token);
  28. hookDragHandlers(MeasuredTemplate);
  29. libWrapper.register(
  30. "drag-ruler",
  31. "TokenLayer.prototype.undoHistory",
  32. tokenLayerUndoHistory,
  33. "WRAPPER",
  34. );
  35. extendRuler();
  36. window.dragRuler = {
  37. getRangesFromSpeedProvider,
  38. getColorForDistanceAndToken,
  39. getMovedDistanceFromToken,
  40. registerModule,
  41. registerSystem,
  42. recalculate,
  43. resetMovementHistory,
  44. };
  45. });
  46. Hooks.once("ready", () => {
  47. performMigrations();
  48. checkDependencies();
  49. Hooks.callAll("dragRuler.ready", SpeedProvider);
  50. if (CONFIG.debug.dragRuler) debugGraphics = canvas.controls.addChild(new PIXI.Container());
  51. });
  52. Hooks.on("canvasReady", () => {
  53. canvas.controls.rulers.children.forEach(ruler => {
  54. ruler.draggedEntity = null;
  55. Object.defineProperty(ruler, "isDragRuler", {
  56. get: function isDragRuler() {
  57. return Boolean(this.draggedEntity) && this._state !== Ruler.STATES.INACTIVE;
  58. },
  59. });
  60. });
  61. });
  62. Hooks.on("getCombatTrackerEntryContext", function (html, menu) {
  63. const entry = {
  64. name: "drag-ruler.resetMovementHistory",
  65. icon: '<i class="fas fa-undo-alt"></i>',
  66. callback: li => resetMovementHistory(ui.combat.viewed, li.data("combatant-id")),
  67. };
  68. menu.splice(1, 0, entry);
  69. });
  70. function forwardIfUnahndled(newFn) {
  71. return function (oldFn, ...args) {
  72. const eventHandled = newFn(...args);
  73. if (!eventHandled) oldFn(...args);
  74. };
  75. }
  76. function hookDragHandlers(entityType) {
  77. const entityName = entityType.name;
  78. libWrapper.register(
  79. "drag-ruler",
  80. `${entityName}.prototype._onDragLeftStart`,
  81. onEntityLeftDragStart,
  82. "WRAPPER",
  83. );
  84. if (entityType === Token)
  85. libWrapper.register(
  86. "drag-ruler",
  87. `${entityName}.prototype._onDragLeftMove`,
  88. onEntityLeftDragMoveSnap,
  89. "WRAPPER",
  90. );
  91. else
  92. libWrapper.register(
  93. "drag-ruler",
  94. `${entityName}.prototype._onDragLeftMove`,
  95. onEntityLeftDragMove,
  96. "WRAPPER",
  97. );
  98. libWrapper.register(
  99. "drag-ruler",
  100. `${entityName}.prototype._onDragLeftDrop`,
  101. forwardIfUnahndled(onEntityDragLeftDrop),
  102. "MIXED",
  103. );
  104. libWrapper.register(
  105. "drag-ruler",
  106. `${entityName}.prototype._onDragLeftCancel`,
  107. forwardIfUnahndled(onEntityDragLeftCancel),
  108. "MIXED",
  109. );
  110. }
  111. async function tokenLayerUndoHistory(wrapped) {
  112. const historyEntry = this.history[this.history.length - 1];
  113. const returnValue = await wrapped();
  114. if (historyEntry.type === "update") {
  115. for (const entry of historyEntry.data) {
  116. const token = canvas.tokens.get(entry._id);
  117. removeLastHistoryEntryIfAt(token, entry.x, entry.y);
  118. }
  119. }
  120. return returnValue;
  121. }
  122. function onEntityLeftDragStart(wrapped, event) {
  123. wrapped(event);
  124. const isToken = this instanceof Token;
  125. const ruler = canvas.controls.ruler;
  126. ruler.draggedEntity = this;
  127. const entityCenter = getEntityCenter(this);
  128. ruler.rulerOffset = {
  129. x: entityCenter.x - event.data.origin.x,
  130. y: entityCenter.y - event.data.origin.y,
  131. };
  132. if (game.settings.get(settingsKey, "autoStartMeasurement")) {
  133. let options = {};
  134. setSnapParameterOnOptions(ruler, options);
  135. ruler.dragRulerStart(options, false);
  136. }
  137. }
  138. function onEntityLeftDragMoveSnap(wrapped, event) {
  139. applyGridlessSnapping.call(this, event);
  140. onEntityLeftDragMove.call(this, wrapped, event);
  141. }
  142. function onEntityLeftDragMove(wrapped, event) {
  143. wrapped(event);
  144. const ruler = canvas.controls.ruler;
  145. if (ruler.isDragRuler) onMouseMove.call(ruler, event);
  146. }
  147. function onEntityDragLeftDrop(event) {
  148. const ruler = canvas.controls.ruler;
  149. if (!ruler.isDragRuler) {
  150. ruler.draggedEntity = undefined;
  151. return false;
  152. }
  153. // When we're dragging a measured template no token will ever be selected,
  154. // resulting in only the dragged template to be moved as would be expected
  155. const selectedTokens = canvas.tokens.controlled;
  156. // This can happen if the user presses ESC during drag (maybe there are other ways too)
  157. if (selectedTokens.length === 0) selectedTokens.push(ruler.draggedEntity);
  158. // This can happen if the ruler is being dragged so rapidly that the drag move handler hasn't been called before dropping
  159. if (ruler._state === Ruler.STATES.STARTING) onMouseMove.call(ruler, event);
  160. ruler._state = Ruler.STATES.MOVING;
  161. moveEntities.call(ruler, ruler.draggedEntity, selectedTokens);
  162. return true;
  163. }
  164. function onEntityDragLeftCancel(event) {
  165. // This function is invoked by right clicking
  166. const ruler = canvas.controls.ruler;
  167. if (!ruler.draggedEntity || ruler._state === Ruler.STATES.MOVING) return false;
  168. const rightClickAction = game.settings.get(settingsKey, "rightClickAction");
  169. let options = {};
  170. setSnapParameterOnOptions(ruler, options);
  171. if (ruler._state === Ruler.STATES.INACTIVE) {
  172. if (rightClickAction !== RightClickAction.CREATE_WAYPOINT) return false;
  173. ruler.dragRulerStart(options);
  174. event.preventDefault();
  175. } else if (ruler._state === Ruler.STATES.MEASURING) {
  176. switch (rightClickAction) {
  177. case RightClickAction.CREATE_WAYPOINT:
  178. event.preventDefault();
  179. ruler.dragRulerAddWaypoint(ruler.destination, options);
  180. break;
  181. case RightClickAction.DELETE_WAYPOINT:
  182. ruler.dragRulerDeleteWaypoint(event, options);
  183. break;
  184. case RightClickAction.ABORT_DRAG:
  185. ruler.dragRulerAbortDrag();
  186. break;
  187. }
  188. }
  189. return true;
  190. }
  191. function applyGridlessSnapping(event) {
  192. const ruler = canvas.controls.ruler;
  193. if (!game.settings.get(settingsKey, "useGridlessRaster")) return;
  194. if (!ruler.isDragRuler) return;
  195. if (disableSnap) return;
  196. if (canvas.grid.type !== CONST.GRID_TYPES.GRIDLESS) return;
  197. const rasterWidth = 35 / canvas.stage.scale.x;
  198. const tokenX = event.data.destination.x;
  199. const tokenY = event.data.destination.y;
  200. const destination = {x: tokenX + ruler.rulerOffset.x, y: tokenY + ruler.rulerOffset.y};
  201. const ranges = getRangesFromSpeedProvider(ruler.draggedEntity);
  202. const terrainRulerAvailable = game.modules.get("terrain-ruler")?.active;
  203. if (terrainRulerAvailable) {
  204. const segments = ruler.constructor
  205. .dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
  206. .map(ray => {
  207. return {ray};
  208. });
  209. const pinpointDistances = new Map();
  210. for (const range of ranges) {
  211. pinpointDistances.set(range.range, null);
  212. }
  213. terrainRuler.measureDistances(segments, {pinpointDistances});
  214. const targetDistance = Array.from(pinpointDistances.entries())
  215. .filter(([_key, val]) => val)
  216. .reduce((value, current) => (value[0] > current[0] ? value : current), [0, null]);
  217. const rasterLocation = targetDistance[1];
  218. if (rasterLocation) {
  219. const deltaX = destination.x - rasterLocation.x;
  220. const deltaY = destination.y - rasterLocation.y;
  221. const rasterDistance = Math.hypot(deltaX, deltaY);
  222. if (rasterDistance < rasterWidth) {
  223. event.data.destination.x = rasterLocation.x - ruler.rulerOffset.x;
  224. event.data.destination.y = rasterLocation.y - ruler.rulerOffset.y;
  225. }
  226. }
  227. } else {
  228. let waypointDistance = 0;
  229. let origin = event.data.origin;
  230. if (ruler.waypoints.length > 1) {
  231. const segments = ruler.constructor
  232. .dragRulerGetRaysFromWaypoints(ruler.waypoints, destination)
  233. .map(ray => {
  234. return {ray};
  235. });
  236. origin = segments.pop().ray.A;
  237. waypointDistance = canvas.grid.measureDistances(segments).reduce((a, b) => a + b);
  238. origin = {x: origin.x - ruler.rulerOffset.x, y: origin.y - ruler.rulerOffset.y};
  239. }
  240. const deltaX = tokenX - origin.x;
  241. const deltaY = tokenY - origin.y;
  242. const distance = Math.hypot(deltaX, deltaY);
  243. // targetRange will be the largest range that's still smaller than distance
  244. let targetDistance = ranges
  245. .map(range => range.range)
  246. .map(range => range - waypointDistance)
  247. .map(range => (range * canvas.dimensions.size) / canvas.dimensions.distance)
  248. .filter(range => range < distance)
  249. .reduce((a, b) => Math.max(a, b), 0);
  250. if (targetDistance) {
  251. if (distance < targetDistance + rasterWidth) {
  252. event.data.destination.x = origin.x + (deltaX * targetDistance) / distance;
  253. event.data.destination.y = origin.y + (deltaY * targetDistance) / distance;
  254. }
  255. }
  256. }
  257. }