New API, allwing color configuration (resolves #7) and Speed Provider Settings

This commit is contained in:
Manuel Vögele
2021-02-17 16:25:17 +01:00
parent 6ea03579b9
commit 75d59171d6
8 changed files with 514 additions and 96 deletions
+68 -16
View File
@@ -1,37 +1,60 @@
import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js"
import {settingsKey} from "./settings.js"
export const availableSpeedProviders = {}
export let currentSpeedProvider = undefined
function register(module, type, speedProvider) {
const providerSetting = game.settings.settings.get("drag-ruler.speedProvider")
const id = `${type}.${module.id}`
let providerInstance
if (speedProvider.prototype instanceof SpeedProvider) {
providerInstance = new speedProvider(id)
}
else {
speedProvider.id = id
providerInstance = speedProvider
}
setupProvider(providerInstance)
}
// 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()
function setupProvider(speedProvider) {
if (speedProvider instanceof SpeedProvider) {
const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
for (const color of speedProvider.colors.concat([unreachableColor])) {
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, {
config: false,
scope: "client",
type: Number,
default: color.default,
})
}
for (const setting of speedProvider.settings) {
setting.config = false
game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.setting.${setting.id}`, setting)
}
}
availableSpeedProviders[speedProvider.id] = speedProvider
game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider()
updateSpeedProvider()
}
function getDefaultSpeedProvider() {
const providerSetting = game.settings.settings.get("drag-ruler.speedProvider")
const settingKeys = Object.keys(providerSetting.choices)
export function getDefaultSpeedProvider() {
const providerIds = Object.keys(availableSpeedProviders)
// Game systems take the highest precedence for the being the default
const gameSystem = settingKeys.find(key => key.startsWith("system."))
const gameSystem = providerIds.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."))
const module = providerIds.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]
return providerIds[0]
}
export function updateSpeedProvider() {
@@ -40,8 +63,37 @@ export function updateSpeedProvider() {
currentSpeedProvider = availableSpeedProviders[configuredProvider] ?? availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default]
}
export function setCurrentSpeedProvider(newSpeedProvider) {
currentSpeedProvider = newSpeedProvider
export function initApi() {
const genericSpeedProviderInstance = new GenericSpeedProvider("native")
setupProvider(genericSpeedProviderInstance)
}
export function getRangesFromSpeedProvider(token) {
try {
if (currentSpeedProvider instanceof Function)
return currentSpeedProvider(token, 0x00FF00)
const ranges = currentSpeedProvider.getRanges(token)
for (const range of ranges) {
range.color = game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.${range.color}`)
}
return ranges
}
catch (e) {
console.error(e)
return []
}
}
export function getUnreachableColorFromSpeedProvider() {
if (currentSpeedProvider instanceof Function)
return 0xFF0000
try {
return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`)
}
catch (e) {
console.error(e)
return 0xFF0000
}
}
export function registerModule(moduleId, speedProvider) {
+7 -25
View File
@@ -1,30 +1,28 @@
"use strict"
import {availableSpeedProviders, currentSpeedProvider, registerModule, registerSystem, setCurrentSpeedProvider} from "./api.js"
import {getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js"
import {getHexSizeSupportTokenGridCenter} from "./compatibility.js"
import {measure, moveTokens, onMouseMove} from "./foundry_imports.js"
import {registerSettings, settingsKey} from "./settings.js"
import {SpeedProvider} from "./speed_provider.js"
Hooks.once("init", () => {
registerSettings()
initApi()
hookTokenDragHandlers()
hookRulerFunctions()
hookKeyboardManagerFunctions()
patchRulerHighlightMeasurement()
availableSpeedProviders["native"] = nativeSpeedProvider
setCurrentSpeedProvider(nativeSpeedProvider)
window.dragRuler = {
getColorForDistance,
registerModule,
registerSystem
registerSystem,
}
})
Hooks.once("ready", () => {
Hooks.callAll("dragRuler.ready")
Hooks.callAll("dragRuler.ready", SpeedProvider)
})
Hooks.on("canvasReady", () => {
@@ -241,21 +239,6 @@ function strInsertAfter(haystack, needle, strToInsert) {
return haystack.slice(0, pos) + strToInsert + haystack.slice(pos)
}
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}]
}
export function getColorForDistance(startDistance, subDistance=0) {
if (!this.isDragRuler)
return this.color
@@ -266,15 +249,14 @@ export function getColorForDistance(startDistance, subDistance=0) {
return this.color
}
const distance = startDistance + subDistance
const firstColor = game.settings.get(settingsKey, "staticFirstColor") ? 0x00FF00 : this.color
const ranges = currentSpeedProvider(this.draggedToken, firstColor)
const ranges = getRangesFromSpeedProvider(this.draggedToken)
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})
}, {range: Infinity, color: getUnreachableColorFromSpeedProvider()})
return currentRange.color
}
+181 -34
View File
@@ -1,5 +1,5 @@
import {updateSpeedProvider} from "./api.js";
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
import {availableSpeedProviders, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js";
import {SpeedProvider} from "./speed_provider.js"
export const settingsKey = "drag-ruler";
@@ -24,42 +24,189 @@ 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",
type: String,
default: getDefaultSpeedProvider(),
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(),
})
game.settings.register(settingsKey, "staticFirstColor", {
name: "drag-ruler.settings.staticFirstColor.name",
hint: "drag-ruler.settings.staticFirstColor.hint",
scope: "world",
config: true,
type: Boolean,
default: false,
game.settings.registerMenu(settingsKey, "speedProviderSettings", {
name: "drag-ruler.settings.speedProviderSettings.name",
hint: "drag-ruler.settings.speedProviderSettings.hint",
label: "drag-ruler.settings.speedProviderSettings.button",
icon: "fas fa-tachometer-alt",
type: SpeedProviderSettings,
restricted: false,
})
}
class SpeedProviderSettings extends FormApplication {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
id: "drag-ruler-speed-provider-settings",
title: game.i18n.localize("drag-ruler.settings.speedProviderSettings.windowTitle"),
template: "modules/drag-ruler/templates/speed_provider_settings.html",
width: 600,
})
}
getData(options={}) {
const data = {}
data.isGM = game.user.isGM
const selectedProvider = game.settings.get(settingsKey, "speedProvider")
// Insert all speed providers into the template data
data.providers = Object.values(availableSpeedProviders).map(speedProvider => {
const provider = {}
provider.id = speedProvider.id
provider.hasSettings = speedProvider instanceof SpeedProvider
if (provider.hasSettings)
provider.settings = enumerateProviderSettings(speedProvider)
let dotPosition = provider.id.indexOf(".")
if (dotPosition === -1)
dotPosition = provider.id.length
const type = provider.id.substring(0, dotPosition)
const id = provider.id.substring(dotPosition + 1)
if (type === "native") {
provider.selectTitle = game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.choices.native")
}
else {
let name
if (type === "module") {
name = game.modules.get(id).data.title
}
else {
name = game.system.data.title
}
provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name})
}
provider.isSelected = provider.id === selectedProvider
return provider
})
data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle
data.providerSelection = {
id: "speedProvider",
name: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.name"),
hint: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.hint"),
type: String,
choices: data.providers.reduce((choices, provider) => {
choices[provider.id] = provider.selectTitle
return choices
}, {}),
value: selectedProvider,
isCheckbox: false,
isSelect: true,
isRange: false,
}
return data
}
async _updateObject(event, formData) {
for (let [key, value] of Object.entries(formData)) {
// Check if this is color, convert the value to an integer
const splitKey = key.split(".", 3)
if (splitKey[0] !== "native")
splitKey.shift()
if (splitKey.length >= 2 && splitKey[1] == "color") {
value = parseInt(value.substring(1), 16)
}
// Don't change settings for speed providers that aren't currently active
if (key !== "speedProvider" && !key.startsWith(formData.speedProvider))
continue
// Get the key for the current setting
let setting
if (key === "speedProvider")
setting = "speedProvider"
else
setting = `speedProviders.${key}`
// Get the old setting value
const oldValue = game.settings.get(settingsKey, setting)
// Only update the setting if it has been changed (this leaves the default in place if it hasn't been touched)
if (value !== oldValue)
game.settings.set(settingsKey, setting, value)
}
// Activate the configured speed provider
updateSpeedProvider()
}
activateListeners(html) {
super.activateListeners(html)
html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this))
}
onSpeedProviderChange(event) {
// Hide all module settings
document.querySelectorAll(".drag-ruler-provider-settings").forEach(element => element.style.display = "none")
// Show the settings block for the currently selected module
document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = ""
// Recalculate window height
this.element[0].style.height = null
this.position.height = undefined
}
}
function toDomHex(value) {
const hex = value.toString(16)
return "#" + "0".repeat(Math.max(0, 6 - hex.length)) + hex
}
function enumerateProviderSettings(provider) {
const colorSettings = []
const unreachableColor = {id: "unreachable", name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"}
// Resolve settings for the colors
for (const color of provider.colors.concat([unreachableColor])) {
// Localize the name, if avaliable. If no name is available use the id as name
const colorName = color.name ? game.i18n.localize(color.name) : color.id
let hint
if (color === unreachableColor)
hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint")
else
hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName})
colorSettings.push({
id: `${provider.id}.color.${color.id}`,
name: game.i18n.format("drag-ruler.settings.speedProviderSettings.color.name", {colorName}),
hint: hint,
type: Number,
value: toDomHex(game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`)),
isCheckbox: false,
isSelect: false,
isRange: false,
isColor: true,
})
}
// Prepare regular settings
const settings = []
for (const setting of provider.settings) {
try {
if (setting.scope === "world" && !game.user.isGM)
continue
const s = duplicate(setting)
s.id = `${provider.id}.setting.${s.id}`
s.name = game.i18n.localize(s.name)
s.hint = game.i18n.localize(s.hint)
s.value = provider.getSetting(setting.id)
s.type = setting.type instanceof Function ? setting.type.name : "String"
s.isCheckbox = setting.type === Boolean
s.isSelect = s.choices !== undefined
s.isRange = (setting.type === Number) && s.range
s.isColor = false
settings.push(s)
}
catch (e) {
console.warn(`Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`)
console.error(e)
}
}
return settings.concat(colorSettings)
}
+132
View File
@@ -0,0 +1,132 @@
import {settingsKey} from "./settings.js"
import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js"
/**
* Base class for all speed providers.
* If you want to offer a speed provider in your system/module you must derive this class.
* Each speed provider must at least implement
*/
export class SpeedProvider {
/**
* Returns an array of colors used by this speed provider. Each color corresponds to one speed that a token may have.
* Each color must be an object with the following properties:
* - id: A value that identfies the color. Must be unique for each color returned.
* - default: The color that is used to highlight that speed by default.
* - name: A user readable name for the speed represented by the color. This name is used in the color configuration dialog. Drag Ruler will attempt to localize this string using `game.i18n`
*
* Of these properties, id and defaultColor are required. name is optional, but it's recommended to set it
*
* Implementing this method is required for all speed providers
*/
get colors() {
throw new Error("A SpeedProvider must implement the colors function")
}
/**
* Returns an array of speeds that the token passed in the arguments this token can reach.
* Each range is an object that with the following properties:
* - range: A number indicating the distance that the token can travel with this speed
* - color: The id (as defined in the `colors` getter) of the color that should be used to represent this range
*
* Implementing this method is required for all speed providers
*/
getRanges(token) {
throw new Error("A SpeedProvider must implement the getRanges function")
}
/**
* Returns an array of configuration options for this module. The settings will be shown in the Speed Provider Settings of Drag Ruler.
* Each configuration option is an object that has the same attributes as a native foundry setting passed to `game.settings.register`,
* except for these exceptions:
* - id: A string that identifies the setting. Must be unique for each setting returned. This id will be used to fetch the setting.
* - config: This property is not supported by Drag Ruler module settings. Use foundries native settings instead if you need settings that don't show up in the configuration dialog.
*
* Implementing this method is optional and only needs to be done if you want to provide custom provider settings
*/
get settings() {
return []
}
/**
* Returns the default color for ranges that a token cannot reach.
*
* Implementing this method is optional and only needs to be done if you want to provide a custom default for that color.
*/
get defaultUnreachableColor() {
return 0xFF0000
}
/**
* Returns the value that is currently set for the setting registered with the provided settingId.
*
* This function shouldn't be overridden by speed provider implementations. It can be called to fetch speed provider specific settings.
*/
getSetting(settingId) {
try {
return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`)
}
catch (e) {
if (this.settings.some(setting => setting.id === settingId)) {
throw e
}
throw new Error(`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`)
}
}
/**
* Constructs a new instance of he speed provider
*
* This function should neither be called or overridden by speed provider implementations
*/
constructor(id) {
this.id = id
}
}
export class GenericSpeedProvider extends SpeedProvider {
get colors() {
return [
{id: "walk", default: 0x00FF00, name: "drag-ruler.genericSpeedProvider.speeds.walk"},
{id: "dash", default: 0xFFFF00, name: "drag-ruler.genericSpeedProvider.speeds.dash"}
]
}
getRanges(token) {
const speedAttribute = this.getSetting("speedAttribute")
if (!speedAttribute)
return []
const tokenSpeed = getProperty(token, speedAttribute)
if (tokenSpeed === undefined) {
console.warn(`Drag Ruler (Generic Speed Provider) | 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 = this.getSetting("dashMultiplier")
if (!dashMultiplier)
return [{range: tokenSpeed, color: playercolor}]
return [{range: tokenSpeed, color: "walk"}, {range: tokenSpeed * dashMultiplier, color: "dash"}]
}
get settings() {
return [
{
id: "speedAttribute",
name: "drag-ruler.genericSpeedProvider.settings.speedAttribute.name",
hint: "drag-ruler.genericSpeedProvider.settings.speedAttribute.hint",
scope: "world",
config: true,
type: String,
default: getDefaultSpeedAttribute(),
},
{
id: "dashMultiplier",
name: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.name",
hint: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.hint",
scope: "world",
config: true,
type: Number,
default: getDefaultDashMultiplier(),
}
]
}
}