commit 4b73cd93bf14c85bf602169cc9a004af0d6b3676 Author: Manuel Vögele Date: Mon Feb 1 13:17:23 2021 +0100 Initial commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..35c5fb2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..5f0f6fc --- /dev/null +++ b/lang/en.json @@ -0,0 +1,23 @@ +{ + "drag-ruler": { + "settings": { + "dashMultiplier": { + "name": "Dash Multiplier", + "hint": "This can be used to give tokens a secondary speed during coloring of the measured path. Set it to 0 to disable the secondary speed." + }, + "speedAttribute": { + "name": "Speed Attribute", + "hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path." + }, + "speedProvider": { + "name": "Speed Settings Provider", + "hint": "Select who provides speed information for tokens duing coloring. Using a game system or module may provide more flexible coloring than sticking to the options provided by Drag Ruler.", + "choices": { + "module": "Module", + "native": "Drag Ruler", + "system": "System" + } + } + } + } +} diff --git a/module.json b/module.json new file mode 100644 index 0000000..ada8fae --- /dev/null +++ b/module.json @@ -0,0 +1,30 @@ +{ + "name": "drag-ruler", + "title": "Drag Ruler", + "description": "When dragging a token displays a ruler showing how far you've moved that token.", + "version": "0.0.1", + "minimumCoreVersion" : "0.7.9", + "compatibleCoreVersion" : "0.7.9", + "authors": [ + { + "name": "Manuel Vögele", + "email": "develop@manuel-voegele.de", + "discord": "Stäbchenfisch#5107" + } + ], + "esmodules": [ + "src/main.js" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "url": "https://github.com/manuelVo/foundryvtt-drag-ruler", + "download": "https://github.com/manuelVo/foundryvtt-drag-ruler/archive/master.zip", + "manifest": "https://raw.githubusercontent.com/manuelVo/foundryvtt-drag-ruler/master/module.json", + "readme": "https://github.com/manuelVo/foundryvtt-drag-ruler/blob/master/README.md", + "bugs": "https://github.com/manuelVo/foundryvtt-drag-ruler/issues" +} diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..0d1a8d1 --- /dev/null +++ b/src/api.js @@ -0,0 +1,85 @@ +export const availableSpeedProviders = {} +export let currentSpeedProvider = undefined + +function register(module, type, speedProvider) { + const providerSetting = game.settings.settings.get("drag-ruler.speedProvider") + + // Add the registered module to the settings entry + providerSetting.config = true + const moduleName = module.data.title + const typeTitle = game.i18n.localize(`drag-ruler.settings.speedProvider.choices.${type}`) + providerSetting.choices[`${type}.${module.id}`] = `${typeTitle} ${moduleName}` + availableSpeedProviders[`${type}.${module.id}`] = speedProvider + providerSetting.default = getDefaultSpeedProvider() + + updateSpeedProvider() +} + +function getDefaultSpeedProvider() { + const providerSetting = game.settings.settings.get("drag-ruler.speedProvider") + const settingKeys = Object.keys(providerSetting.choices) + // Game systems take the highest precedence for the being the default + const gameSystem = settingKeys.find(key => key.startsWith("system.")) + if (gameSystem) + return gameSystem + + // If no game system is registered modules are next up. + // For lack of a method to select the best module we're just falling back to taking the next best module + // settingKeys should always be sorted the same way so this should achive a stable default + const module = settingKeys.find(key => key.startsWith("module.")) + if (module) + return module + + // If neither a game system or a module is found fall back to the native implementation + return settingKeys[0] +} + +export function updateSpeedProvider() { + // If the configured provider is registered use that one. If not use the default provider + const configuredProvider = game.settings.get("drag-ruler", "speedProvider") + currentSpeedProvider = availableSpeedProviders[configuredProvider] ?? availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default] +} + +export function setCurrentSpeedProvider(newSpeedProvider) { + currentSpeedProvider = newSpeedProvider +} + +export function registerModule(moduleId, speedProvider) { + // Check if a module with the given id exists and is currently enabled + const module = game.modules.get(moduleId) + // If it doesn't the calling module did something wrong. Log a warning and ignore this module + if (!module) { + console.warn( + `Drag Ruler | A module tried to register with the id "${moduleId}". However no active module with this id was found.` + + "This api registration call was ignored. " + + "If you are the author of that module please check that the id passed to `registerModule` matches the id in your manifest exactly." + + "If this call was made form a game system instead of a module please use `registerSystem` instead.") + return + } + // Using Drag Ruler's id is not allowed + if (moduleId === "drag-ruler") { + console.warn( + `Drag Ruler | A module tried to register with the id "${moduleId}", which is not allowed. This api registration call was ignored. ` + + "If you're the author of the module please use the id of your own module as it's specified in your manifest to register to this api. " + + "If this call was made form a game system instead of a module please use `registerSystem` instead." + ) + return + } + + register(module, "module", speedProvider) +} + +export function registerSystem(systemId, speedProvider) { + const system = game.system + // If the current system id doesn't match the provided id something went wrong. Log a warning and ignore this module + if (system.id != systemId) { + console.warn( + `Drag Ruler | A system tried to register with the id "${systemId}". However the active system has a different id.` + + "This api registration call was ignored. " + + "If you are the author of that system please check that the id passed to `registerSystem` matches the id in your manifest exactly." + + "If this call was made form a module instead of a game system please use `registerModule` instead.") + return + } + + register(system, "system", speedProvider) +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..75fe4db --- /dev/null +++ b/src/main.js @@ -0,0 +1,178 @@ +"use strict" + +import {availableSpeedProviders, currentSpeedProvider, registerModule, registerSystem, setCurrentSpeedProvider} from "./api.js" +import {registerSettings, settingsKey} from "./settings.js" + +Hooks.once("init", () => { + registerSettings() + hookTokenDragHandlers() + hookRulerHandlers() + patchRulerMeasure() + patchRulerHighlightMeasurement() + + availableSpeedProviders["native"] = nativeSpeedProvider + setCurrentSpeedProvider(nativeSpeedProvider) + + window.dragRuler = { + getColorForDistance, + registerModule, + registerSystem + } + +}) + +Hooks.once("ready", () => { + Hooks.callAll("dragRuler.ready") +}) + +Hooks.on("canvasReady", () => { + canvas.controls.ruler.draggedToken = null + Object.defineProperty(canvas.controls.ruler, "isDragRuler", { + get: function isDragRuler() { + return Boolean(this.draggedToken) // If draggedToken is set this is a drag ruler + } + }) +}) + +function hookTokenDragHandlers() { + const originalDragLeftStartHandler = Token.prototype._onDragLeftStart + Token.prototype._onDragLeftStart = function(event) { + originalDragLeftStartHandler.call(this, event) + onTokenLeftDragStart.call(this, event) + } + + const originalDragLeftMoveHandler = Token.prototype._onDragLeftMove + Token.prototype._onDragLeftMove = function (event) { + originalDragLeftMoveHandler.call(this, event) + onTokenLeftDragMove.call(this, event) + } + + const originalDragLeftDropHandler = Token.prototype._onDragLeftDrop + Token.prototype._onDragLeftDrop = function (event) { + const eventHandled = onTokenDragLeftDrop.call(this, event) + if (!eventHandled) + originalDragLeftDropHandler.call(this, event) + } + + const originalDragLeftCancelHandler = Token.prototype._onDragLeftCancel + Token.prototype._onDragLeftCancel = function (event) { + const eventHandled = onTokenDragLeftCancel.call(this, event) + if (!eventHandled) + originalDragLeftCancelHandler.call(this, event) + } +} + +function hookRulerHandlers() { + const originalMoveTokenHandler = Ruler.prototype.moveToken + Ruler.prototype.moveToken = function (event) { + const eventHandled = onRulerMoveToken.call(this, event) + if (!eventHandled) + return originalMoveTokenHandler.call(this, event) + return true + } +} + +function onTokenLeftDragStart(event) { + canvas.controls.ruler._onDragStart(event) + canvas.controls.ruler.draggedToken = this +} + +function onTokenLeftDragMove(event) { + if (canvas.controls.ruler.isDragRuler) + canvas.controls.ruler._onMouseMove(event) +} + +function onTokenDragLeftDrop(event) { + if (!canvas.controls.ruler.isDragRuler) + return false + canvas.controls.ruler.draggedToken = null + canvas.controls.ruler.moveToken(event) + return true +} + +function onTokenDragLeftCancel(event) { + if (!canvas.controls.ruler.isDragRuler) + return false + if (canvas.controls.ruler._state === Ruler.STATES.MEASURING) { + if (canvas.controls.ruler.waypoints.length > 1) { + canvas.controls.ruler._removeWaypoint(canvas.app.renderer.plugins.interaction.mouse.getLocalPosition(canvas.tokens), {snap: !event.shiftKey}) + game.user.broadcastActivity({ruler: canvas.controls.ruler}) + event.preventDefault() + } + else { + canvas.controls.ruler._endMeasurement() + canvas.controls.ruler.draggedToken = null + return false + } + } + return true +} + +function onRulerMoveToken(event) { + if (!this.isDragRuler) + return false + this._addWaypoint(this.destination) + return true +} + +function strInsertAfter(haystack, needle, strToInsert) { + const pos = haystack.indexOf(needle) + needle.length + return haystack.slice(0, pos) + strToInsert + haystack.slice(pos) +} + +// These patches were written with foundry-0.7.9.js as reference +function patchRulerMeasure() { + let code = Ruler.prototype.measure.toString() + // Remove function signature and closing curly bracket (those are on the first and last line) + code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n")) + code = strInsertAfter(code, "for ( let [i, d] of distances.entries() ) {\n", "segments[i].startDistance = totalDistance\n") + code = strInsertAfter(code, "this._highlightMeasurement(ray", ", s.startDistance") + Ruler.prototype.measure = new Function("destination", "{gridSpaces=true}={}", code) +} + +function nativeSpeedProvider(token, playercolor) { + const speedAttribute = game.settings.get(settingsKey, "speedAttribute") + if (!speedAttribute) + return [] + const tokenSpeed = getProperty(token, speedAttribute) + if (tokenSpeed === undefined) { + console.warn(`Drag Ruler | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`) + return [] + } + const dashMultiplier = game.settings.get(settingsKey, "dashMultiplier") + if (!dashMultiplier) + return [{range: tokenSpeed, color: playercolor}] + return [{range: tokenSpeed, color: playercolor}, {range: tokenSpeed * dashMultiplier, color: 0xFFFF00}] +} + +function getColorForDistance(startDistance, subDistance) { + if (!this.isDragRuler) + return this.color + const distance = startDistance + subDistance + const ranges = currentSpeedProvider(this.draggedToken, this.color) + if (ranges.length === 0) + return this.color + const currentRange = ranges.reduce((minRange, currentRange) => { + if (distance <= currentRange.range && currentRange.range < minRange.range) + return currentRange + return minRange + }, {range: Infinity, color: 0xFF0000}) + return currentRange.color +} + +// These patches were written with foundry-0.7.9.js as reference +function patchRulerHighlightMeasurement() { + let code = Ruler.prototype._highlightMeasurement.toString() + // Remove function signature and closing curly bracket (those are on the first and last line) + code = code.slice(code.indexOf("\n"), code.lastIndexOf("\n")) + + const calcColorCode = ` + let subDistance = canvas.grid.measureDistances([{ray: new Ray(ray.A, {x: xg, y: yg})}], {gridSpaces: true})[0] + let color = dragRuler.getColorForDistance.call(this, startDistance, subDistance) + ` + + code = strInsertAfter(code, "Position(x1, y1);\n", calcColorCode) + code = strInsertAfter(code, "Position(x1h, y1h);\n", calcColorCode.replace("x: xg, y: yg", "x: xgh, y: ygh")) + code = code.replace(/color: this\.color\}/g, "color}") + Ruler.prototype._highlightMeasurement = new Function("ray", "startDistance=undefined", code) +} diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000..8ef52f9 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,38 @@ +import {updateSpeedProvider} from "./api.js"; +import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js" + +export const settingsKey = "drag-ruler"; + +export function registerSettings() { + // This setting will be modified by the api if modules register to it + game.settings.register(settingsKey, "speedProvider", { + name: "drag-ruler.settings.speedProvider.name", + hint: "drag-ruler.settings.speedProvider.hint", + scope: "world", + config: false, + type: Object, + choices: { + "native": game.i18n.localize("drag-ruler.settings.speedProvider.choices.native") + }, + default: "native", + onChange: updateSpeedProvider, + }) + + game.settings.register(settingsKey, "speedAttribute", { + name: "drag-ruler.settings.speedAttribute.name", + hint: "drag-ruler.settings.speedAttribute.hint", + scope: "world", + config: true, + type: String, + default: getDefaultSpeedAttribute(), + }) + + game.settings.register(settingsKey, "dashMultiplier", { + name: "drag-ruler.settings.dashMultiplier.name", + hint: "drag-ruler.settings.dashMultiplier.hint", + scope: "world", + config: true, + type: Number, + default: getDefaultDashMultiplier(), + }) +} diff --git a/src/systems.js b/src/systems.js new file mode 100644 index 0000000..fa93ea7 --- /dev/null +++ b/src/systems.js @@ -0,0 +1,19 @@ + +export function getDefaultSpeedAttribute() { + switch (game.system.id) { + case "dnd5e": + return "actor.data.data.attributes.movement.walk" + case "pf1": + return "actor.data.data.attributes.speed.land.total" + } + return "" +} + +export function getDefaultDashMultiplier() { + switch (game.system.id) { + case "dnd5e": + case "pf1": + return 2 + } + return 0 +}