From 9f39c0856bb30380ae4504d570c207ac23bcadf9 Mon Sep 17 00:00:00 2001 From: Johannes Loher Date: Tue, 29 Jun 2021 20:09:34 +0200 Subject: [PATCH] Do some cleanup --- src/lang/en.json | 1 - src/module/config.js | 7 + .../darkness-dependent-vision-config.js | 12 +- src/module/darkness-dependent-vision.js | 185 +----------------- src/module/hooks.js | 57 ++++++ src/module/logger.js | 14 +- src/module/notifications.js | 46 ----- src/module/wrappers/token-config.js | 30 +++ src/module/wrappers/token.js | 113 +++++++++++ 9 files changed, 223 insertions(+), 242 deletions(-) create mode 100644 src/module/config.js create mode 100644 src/module/hooks.js delete mode 100644 src/module/notifications.js create mode 100644 src/module/wrappers/token-config.js create mode 100644 src/module/wrappers/token.js diff --git a/src/lang/en.json b/src/lang/en.json index 494a614..ca2e74d 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1,5 +1,4 @@ { - "DDV.ErrorFailedToOverride": "Failed to override {target}, some things might not work correctly.", "DDV.WarningLackingPermissionToConfigure": "You do not have permission to configure this Token!", "DDV.Title": "Darkness Dependent Vision Configuration", "DDV.TokenConfigHeaderButtonLabel": "DDV", diff --git a/src/module/config.js b/src/module/config.js new file mode 100644 index 0000000..a98a434 --- /dev/null +++ b/src/module/config.js @@ -0,0 +1,7 @@ +// SPDX-FileCopyrightText: 2021 Johannes Loher +// +// SPDX-License-Identifier: MIT + +'use strict'; + +export const packageName = 'darkness-dependent-vision'; diff --git a/src/module/darkness-dependent-vision-config.js b/src/module/darkness-dependent-vision-config.js index 3c69588..5770690 100644 --- a/src/module/darkness-dependent-vision-config.js +++ b/src/module/darkness-dependent-vision-config.js @@ -4,8 +4,13 @@ 'use strict'; -import notifications from './notifications'; - +/** + * An application that provides functionality to configure darkness dependent + * vision for a {@link TokenDocument}. + * @extends {FormApplication} + * @remarks Most of the code of this class is heavily inspired by the + * implementation of {@link TokenConfig}. + */ export class DarknessDependentVisionConfig extends FormApplication { constructor(object, options) { super(object, options); @@ -52,7 +57,6 @@ export class DarknessDependentVisionConfig extends FormApplication { async getData() { const data = this.isPrototype ? this.actor.data.token : this.token.data; return { - cssClasses: [this.isPrototype ? 'prototype' : null].filter((c) => !!c).join(' '), object: data, }; } @@ -60,7 +64,7 @@ export class DarknessDependentVisionConfig extends FormApplication { async render(force, options) { const canConfigure = game.user.isGM || this.actor?.isOwner; if (!game.user.can('TOKEN_CONFIGURE') || !canConfigure) { - notifications.warn(game.i18n.localize('DDV.WarningLackingPermissionToConfigure')); + ui.notifications?.warn(game.i18n.localize('DDV.WarningLackingPermissionToConfigure')); return this; } return super.render(force, options); diff --git a/src/module/darkness-dependent-vision.js b/src/module/darkness-dependent-vision.js index 0ecd71e..9b1e503 100644 --- a/src/module/darkness-dependent-vision.js +++ b/src/module/darkness-dependent-vision.js @@ -4,186 +4,5 @@ 'use strict'; -import logger from './logger'; -import { libWrapper } from './shims/libWrapperShim'; -import notifications from './notifications'; -import { DarknessDependentVisionConfig } from './darkness-dependent-vision-config'; - -const packageName = 'darkness-dependent-vision'; - -Hooks.once('init', async () => { - logger.info(`Initializing ${packageName}`); - - try { - libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.dimRadius', getDimRadius, 'OVERRIDE'); - } catch (e) { - notifications.warn( - game.i18n.format('DDV.ErrorFailedToOverride', { target: 'CONFIG.Token.documentClass.prototype.dimRadius' }), - { - log: true, - }, - ); - } - - try { - libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.brightRadius', getBrightRadius, 'OVERRIDE'); - } catch (e) { - notifications.warn( - game.i18n.format('DDV.ErrorFailedToOverride', { target: 'CONFIG.Token.documentClass.prototype.brightRadius' }), - { - log: true, - }, - ); - } - - libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.updateSource', updateSource, 'WRAPPER'); - - libWrapper.register(packageName, 'CONFIG.Token.sheetClass.prototype._getHeaderButtons', getHeaderButtons, 'WRAPPER'); -}); - -/** - * Does this {@link Token} have dim vision, considering the darkness level of - * its containing {@link Scene}? - */ -function hasDimVision() { - const dimVisionDarknessMin = this.document.getFlag(packageName, 'dimVisionDarknessMin') ?? 0; - const dimVisionDarknessMax = this.document.getFlag(packageName, 'dimVisionDarknessMax') ?? 1; - const darkness = this.document.parent.data.darkness; - return dimVisionDarknessMin <= darkness && darkness <= dimVisionDarknessMax; -} - -/** - * Does this {@link Token} have bright vision, considering the darkness level of - * its containing {@link Scene}? - */ -function hasBrightVision() { - const brightVisionDarknessMin = this.document.getFlag(packageName, 'brightVisionDarknessMin') ?? 0; - const brightVisionDarknessMax = this.document.getFlag(packageName, 'brightVisionDarknessMax') ?? 1; - const darkness = this.document.parent.data.darkness; - return brightVisionDarknessMin <= darkness && darkness <= brightVisionDarknessMax; -} - -/** - * Get this {@link Token}'s dim vision distance of in grid units, considering - * the darkness level of its containing {@link Scene}. - * - * @returns {number} The the number of grid units that this {@link Token} has - * dim vision - */ -function getDimVision() { - return hasDimVision.call(this) ? this.data.dimSight : 0; -} - -/** - * Get this {@link Token}'s bright vision distance in grid units, considering - * the darkness level of its containing {@link Scene}. - * - * @returns {number} The the number of grid units that this {@link Token} has - * bright vision - */ -function getBrightVision() { - return hasBrightVision.call(this) ? this.data.brightSight : 0; -} - -/** - * Translate the token's sight distance in units into a radius in pixels. - * @return {number} The sight radius in pixels - */ -function getDimRadius() { - const dimSight = getDimVision.call(this); - let r = Math.abs(this.data.dimLight) > Math.abs(dimSight) ? this.data.dimLight : dimSight; - return this.getLightRadius(r); -} - -/** - * Translate the token's bright light distance in units into a radius in pixels. - * @return {number} The bright radius in pixels - */ -function getBrightRadius() { - const brightSight = getBrightVision.call(this); - let r = Math.abs(this.data.brightLight) > Math.abs(brightSight) ? this.data.brightLight : brightSight; - return this.getLightRadius(r); -} - -/** - * Update the light and vision source objects associated with this Token - * @typedef {({defer, deleted, noUpdateFog}?: {defer?: boolean, deleted?: boolean, noUpdateFog?: boolean}) => void} UpdateSourceFunction - * @param {UpdateSourceFunction} wrapped The function that is wrapped by this function and needs to be called next in the chain - * @param {boolean} [defer] Defer refreshing the SightLayer to manually call that refresh later. - * @param {boolean} [deleted] Indicate that this light source has been deleted. - * @param {boolean} [noUpdateFog] Never update the Fog exploration progress for this update. - */ -function updateSource(wrapped, { defer = false, deleted = false, noUpdateFog = false } = {}) { - wrapped({ defer, deleted, noUpdateFog }); - - // Prepare some common data - const origin = this.getSightOrigin(); - const sourceId = this.sourceId; - const d = canvas.dimensions; - - // Update vision source - const isVisionSource = this._isVisionSource(); - if (isVisionSource && !deleted) { - const dimSight = getDimVision.call(this); - const brightSight = getBrightVision.call(this); - let dim = Math.min(this.getLightRadius(dimSight), d.maxR); - const bright = Math.min(this.getLightRadius(brightSight), d.maxR); - this.vision.initialize({ - x: origin.x, - y: origin.y, - dim: dim, - bright: bright, - angle: this.data.sightAngle, - rotation: this.data.rotation, - }); - canvas.sight.sources.set(sourceId, this.vision); - if (!defer) { - this.vision.drawLight(); - canvas.sight.refresh({ noUpdateFog }); - } - } else { - canvas.sight.sources.delete(sourceId); - if (isVisionSource && !defer) canvas.sight.refresh(); - } -} - -/** - * Specify the set of config buttons which should appear in the Application header. - * Buttons should be returned as an Array of objects. - * The header buttons which are added to the application can be modified by the getApplicationHeaderButtons hook. - * @typedef {{label: string, class: string, icon: string, onclick: Function|null}} ApplicationHeaderButton - * @param {() => ApplicationHeaderButton[]} wrapped The function that is wrapped by this function and needs to be called next in the chain - * @fires Application#hook:getApplicationHeaderButtons - * @returns {ApplicationHeaderButton[]} - */ -function getHeaderButtons(wrapped) { - const buttons = wrapped(); - const button = { - label: 'DDV.TokenConfigHeaderButtonLabel', - class: 'configure-darkness-dependent-vision', - icon: 'fas fa-eye', - onclick: async () => { - return new DarknessDependentVisionConfig(this.object).render(true); - }, - }; - - return [button, ...buttons]; -} - -Hooks.on('updateScene', (scene, change) => { - if (change.darkness != null) { - scene.getEmbeddedCollection('Token').forEach((tokenDocument) => tokenDocument.object.updateSource()); - } -}); - -Hooks.on('updateToken', (token, change) => { - const shouldUpdateSource = [ - 'dimVisionDarknessMin', - 'dimVisionDarknessMax', - 'brightVisionDarknessMin', - 'brightVisionDarknessMax', - ].some((flagKey) => `flags.darkness-dependent-vision.${flagKey}` in foundry.utils.flattenObject(change)); - if (shouldUpdateSource) { - token.object.updateSource(); - } -}); +import registerForHooks from './hooks'; +registerForHooks(); diff --git a/src/module/hooks.js b/src/module/hooks.js new file mode 100644 index 0000000..296d166 --- /dev/null +++ b/src/module/hooks.js @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2021 Johannes Loher +// +// SPDX-License-Identifier: MIT + +'use strict'; + +import { packageName } from './config'; +import logger from './logger'; +import { getBrightRadius, getDimRadius, updateSource } from './wrappers/token'; +import { getHeaderButtons } from './wrappers/token-config'; +import { libWrapper } from './shims/libWrapperShim'; + +export default function registerForHooks() { + Hooks.once('init', onInit); + Hooks.on('updateScene', onUpdateScene); + Hooks.on('updateToken', onUpdateToken); +} + +function onInit() { + logger.info(`Initializing ${packageName}`); + + const dimRadiusTarget = 'CONFIG.Token.objectClass.prototype.dimRadius'; + try { + libWrapper.register(packageName, dimRadiusTarget, getDimRadius, 'OVERRIDE'); + } catch (e) { + logger.warn(`Failed to override ${dimRadiusTarget}, some things might not work correctly:`, e); + } + + const brightRadiusTarget = 'CONFIG.Token.objectClass.prototype.dimRadius'; + try { + libWrapper.register(packageName, brightRadiusTarget, getBrightRadius, 'OVERRIDE'); + } catch (e) { + logger.warn(`Failed to override ${brightRadiusTarget}, some things might not work correctly:`, e); + } + + libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.updateSource', updateSource, 'WRAPPER'); + + libWrapper.register(packageName, 'CONFIG.Token.sheetClass.prototype._getHeaderButtons', getHeaderButtons, 'WRAPPER'); +} + +function onUpdateScene(scene, change) { + if (change.darkness != null) { + scene.getEmbeddedCollection('Token').forEach((tokenDocument) => tokenDocument.object.updateSource()); + } +} + +function onUpdateToken(token, change) { + const shouldUpdateSource = [ + 'dimVisionDarknessMin', + 'dimVisionDarknessMax', + 'brightVisionDarknessMin', + 'brightVisionDarknessMax', + ].some((flagKey) => `flags.darkness-dependent-vision.${flagKey}` in foundry.utils.flattenObject(change)); + if (shouldUpdateSource) { + token.object.updateSource(); + } +} diff --git a/src/module/logger.js b/src/module/logger.js index aa95e63..71da521 100644 --- a/src/module/logger.js +++ b/src/module/logger.js @@ -8,22 +8,20 @@ const loggingContext = 'DDV'; const loggingSeparator = '|'; /** - * @typedef {'debug' | 'info' | 'warning' | 'error'} LogLevel - */ - -/** + * Get a logging function for the requested log level. + * @typedef {'debug' | 'info' | 'warn' | 'error'} LogLevel * @typedef {(...args: unknown[]) => void} LoggingFunction - */ - -/** * @param {LogLevel} [type = 'info'] - The log level of the requested logger * @returns {LoggingFunction} */ function getLoggingFunction(type = 'info') { - const log = { debug: console.debug, info: console.info, warning: console.warn, error: console.error }[type]; + const log = console[type]; return (...data) => log(loggingContext, loggingSeparator, ...data); } +/** + * A singleton logger object. + */ const logger = Object.freeze({ debug: getLoggingFunction('debug'), info: getLoggingFunction('info'), diff --git a/src/module/notifications.js b/src/module/notifications.js deleted file mode 100644 index 03ab71a..0000000 --- a/src/module/notifications.js +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Johannes Loher -// -// SPDX-License-Identifier: MIT - -'use strict'; - -import logger from './logger'; - -const notificationPrefix = 'Darkness Dependent Vision:'; - -/** - * @typedef {"info" | "warn" | "error"} NotificationType - */ - -/** - * @typedef {(message: string, {permanent, log}?: {permanent?: boolean, log?: boolean}) => void} NotificationFunction - */ - -/** - * @param {NotificationType} type The type of the notification function to get - * @returns {NotificationFunction} A function that can be called to send a notification to the user - */ -function getNotificationFunction(type) { - return (message, { permanent = false, log = false } = {}) => { - ui.notifications[type](`${notificationPrefix} ${message}`, { permanent }); - if (log) { - logger[type](message); - } - }; -} - -const notifications = { - info: getNotificationFunction('info'), - warn: getNotificationFunction('warn'), - error: getNotificationFunction('error'), - notify: (message, type, { permanent = false, log = false } = {}) => { - ui.notifications.notify(`${notificationPrefix} ${message}`, type, { permanent }); - if (log) { - logger.getLoggingFunction(type)(message); - } - }, -}; - -Object.freeze(notifications); - -export default notifications; diff --git a/src/module/wrappers/token-config.js b/src/module/wrappers/token-config.js new file mode 100644 index 0000000..661d2e5 --- /dev/null +++ b/src/module/wrappers/token-config.js @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2021 Johannes Loher +// +// SPDX-License-Identifier: MIT + +'use strict'; + +import { DarknessDependentVisionConfig } from '../darkness-dependent-vision-config'; + +/** + * Specify the set of config buttons which should appear in the Application header. + * Buttons should be returned as an Array of objects. + * The header buttons which are added to the application can be modified by the getApplicationHeaderButtons hook. + * @typedef {{label: string, class: string, icon: string, onclick: Function|null}} ApplicationHeaderButton + * @param {() => ApplicationHeaderButton[]} wrapped The function that is wrapped by this function and needs to be called next in the chain + * @fires Application#hook:getApplicationHeaderButtons + * @returns {ApplicationHeaderButton[]} + */ +export function getHeaderButtons(wrapped) { + const buttons = wrapped(); + const button = { + label: 'DDV.TokenConfigHeaderButtonLabel', + class: 'configure-darkness-dependent-vision', + icon: 'fas fa-eye', + onclick: async () => { + return new DarknessDependentVisionConfig(this.object).render(true); + }, + }; + + return [button, ...buttons]; +} diff --git a/src/module/wrappers/token.js b/src/module/wrappers/token.js new file mode 100644 index 0000000..cbbe9f7 --- /dev/null +++ b/src/module/wrappers/token.js @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2021 Johannes Loher +// +// SPDX-License-Identifier: MIT + +'use strict'; + +import { packageName } from '../config'; + +/** + * Translate the token's sight distance in units into a radius in pixels. + * @return {number} The sight radius in pixels + */ +export function getDimRadius() { + const dimSight = getDimVision.call(this); + let r = Math.abs(this.data.dimLight) > Math.abs(dimSight) ? this.data.dimLight : dimSight; + return this.getLightRadius(r); +} + +/** + * Translate the token's bright light distance in units into a radius in pixels. + * @return {number} The bright radius in pixels + */ +export function getBrightRadius() { + const brightSight = getBrightVision.call(this); + let r = Math.abs(this.data.brightLight) > Math.abs(brightSight) ? this.data.brightLight : brightSight; + return this.getLightRadius(r); +} + +/** + * Update the light and vision source objects associated with this Token + * @typedef {({defer, deleted, noUpdateFog}?: {defer?: boolean, deleted?: boolean, noUpdateFog?: boolean}) => void} UpdateSourceFunction + * @param {UpdateSourceFunction} wrapped The function that is wrapped by this function and needs to be called next in the chain + * @param {boolean} [defer] Defer refreshing the SightLayer to manually call that refresh later. + * @param {boolean} [deleted] Indicate that this light source has been deleted. + * @param {boolean} [noUpdateFog] Never update the Fog exploration progress for this update. + */ +export function updateSource(wrapped, { defer = false, deleted = false, noUpdateFog = false } = {}) { + wrapped({ defer, deleted, noUpdateFog }); + + // Prepare some common data + const origin = this.getSightOrigin(); + const sourceId = this.sourceId; + const d = canvas.dimensions; + + // Update vision source + const isVisionSource = this._isVisionSource(); + if (isVisionSource && !deleted) { + const dimSight = getDimVision.call(this); + const brightSight = getBrightVision.call(this); + let dim = Math.min(this.getLightRadius(dimSight), d.maxR); + const bright = Math.min(this.getLightRadius(brightSight), d.maxR); + this.vision.initialize({ + x: origin.x, + y: origin.y, + dim: dim, + bright: bright, + angle: this.data.sightAngle, + rotation: this.data.rotation, + }); + canvas.sight.sources.set(sourceId, this.vision); + if (!defer) { + this.vision.drawLight(); + canvas.sight.refresh({ noUpdateFog }); + } + } else { + canvas.sight.sources.delete(sourceId); + if (isVisionSource && !defer) canvas.sight.refresh(); + } +} + +/** + * Does this {@link Token} have dim vision, considering the darkness level of + * its containing {@link Scene}? + */ +function hasDimVision() { + const dimVisionDarknessMin = this.document.getFlag(packageName, 'dimVisionDarknessMin') ?? 0; + const dimVisionDarknessMax = this.document.getFlag(packageName, 'dimVisionDarknessMax') ?? 1; + const darkness = this.document.parent.data.darkness; + return dimVisionDarknessMin <= darkness && darkness <= dimVisionDarknessMax; +} + +/** + * Does this {@link Token} have bright vision, considering the darkness level of + * its containing {@link Scene}? + */ +function hasBrightVision() { + const brightVisionDarknessMin = this.document.getFlag(packageName, 'brightVisionDarknessMin') ?? 0; + const brightVisionDarknessMax = this.document.getFlag(packageName, 'brightVisionDarknessMax') ?? 1; + const darkness = this.document.parent.data.darkness; + return brightVisionDarknessMin <= darkness && darkness <= brightVisionDarknessMax; +} + +/** + * Get this {@link Token}'s dim vision distance of in grid units, considering + * the darkness level of its containing {@link Scene}. + * + * @returns {number} The the number of grid units that this {@link Token} has + * dim vision + */ +function getDimVision() { + return hasDimVision.call(this) ? this.data.dimSight : 0; +} + +/** + * Get this {@link Token}'s bright vision distance in grid units, considering + * the darkness level of its containing {@link Scene}. + * + * @returns {number} The the number of grid units that this {@link Token} has + * bright vision + */ +function getBrightVision() { + return hasBrightVision.call(this) ? this.data.brightSight : 0; +}