diff --git a/.eslintrc.js b/.eslintrc.js index f6fae36..91e4aa5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { globals: { foundry: false, + PrototypeTokenDocument: false, }, rules: { diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..da487ff --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5ef3cba --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + }, + "eslint.nodePath": ".yarn/sdks", + "prettier.prettierPath": ".yarn/sdks/prettier/index.js" +} diff --git a/.yarn/sdks/eslint/bin/eslint.js b/.yarn/sdks/eslint/bin/eslint.js new file mode 100755 index 0000000..4e7554d --- /dev/null +++ b/.yarn/sdks/eslint/bin/eslint.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, createRequireFromPath} = require(`module`); +const {resolve} = require(`path`); + +const relPnpApiPath = "../../../../.pnp.js"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/bin/eslint.js + require(absPnpApiPath).setup(); + } +} + +// Defer to the real eslint/bin/eslint.js your application uses +module.exports = absRequire(`eslint/bin/eslint.js`); diff --git a/.yarn/sdks/eslint/lib/api.js b/.yarn/sdks/eslint/lib/api.js new file mode 100644 index 0000000..ac3c9fc --- /dev/null +++ b/.yarn/sdks/eslint/lib/api.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, createRequireFromPath} = require(`module`); +const {resolve} = require(`path`); + +const relPnpApiPath = "../../../../.pnp.js"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require eslint/lib/api.js + require(absPnpApiPath).setup(); + } +} + +// Defer to the real eslint/lib/api.js your application uses +module.exports = absRequire(`eslint/lib/api.js`); diff --git a/.yarn/sdks/eslint/package.json b/.yarn/sdks/eslint/package.json new file mode 100644 index 0000000..53788a0 --- /dev/null +++ b/.yarn/sdks/eslint/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint", + "version": "7.29.0-pnpify", + "main": "./lib/api.js", + "type": "commonjs" +} diff --git a/.yarn/sdks/integrations.yml b/.yarn/sdks/integrations.yml new file mode 100644 index 0000000..76ed42b --- /dev/null +++ b/.yarn/sdks/integrations.yml @@ -0,0 +1,5 @@ +# This file is automatically generated by PnPify. +# Manual changes will be lost! + +integrations: + - vscode diff --git a/.yarn/sdks/prettier/index.js b/.yarn/sdks/prettier/index.js new file mode 100755 index 0000000..db0cd17 --- /dev/null +++ b/.yarn/sdks/prettier/index.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const {existsSync} = require(`fs`); +const {createRequire, createRequireFromPath} = require(`module`); +const {resolve} = require(`path`); + +const relPnpApiPath = "../../../.pnp.js"; + +const absPnpApiPath = resolve(__dirname, relPnpApiPath); +const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath); + +if (existsSync(absPnpApiPath)) { + if (!process.versions.pnp) { + // Setup the environment to be able to require prettier/index.js + require(absPnpApiPath).setup(); + } +} + +// Defer to the real prettier/index.js your application uses +module.exports = absRequire(`prettier/index.js`); diff --git a/.yarn/sdks/prettier/package.json b/.yarn/sdks/prettier/package.json new file mode 100644 index 0000000..8ca3fc6 --- /dev/null +++ b/.yarn/sdks/prettier/package.json @@ -0,0 +1,6 @@ +{ + "name": "prettier", + "version": "2.3.2-pnpify", + "main": "./index.js", + "type": "commonjs" +} diff --git a/src/lang/en.json b/src/lang/en.json index 41fd85b..494a614 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1,3 +1,10 @@ { - "DDV.ErrorFailedToOverride": "Failed to override {target}, some things might not work correctly." + "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", + "DDV.DimVisionDarknessRange": "Dim Vision Darkness Range", + "DDV.DimVisionDarknessRangeHint": "You may specify a range of darkness levels during which this token has dim vision.", + "DDV.BrightVisionDarknessRange": "Bright Vision Darkness Range", + "DDV.BrightVisionDarknessRangeHint": "You may specify a range of darkness levels during which this token has bright vision." } diff --git a/src/module/darkness-dependent-vision-config.js b/src/module/darkness-dependent-vision-config.js new file mode 100644 index 0000000..3c69588 --- /dev/null +++ b/src/module/darkness-dependent-vision-config.js @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2021 Johannes Loher +// +// SPDX-License-Identifier: MIT + +'use strict'; + +import notifications from './notifications'; + +export class DarknessDependentVisionConfig extends FormApplication { + constructor(object, options) { + super(object, options); + this.token = this.object; + if (this.isPrototype) this.token = new PrototypeTokenDocument(this.object.data.token, { actor: this.object }); + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ['darkness-dependent-vision-config'], + template: `modules/darkness-dependent-vision/templates/darkness-dependent-vision-config.hbs`, + width: 520, + height: 'auto', + }); + } + + get id() { + return `darkness-dependent-vision-config-${this.object.id}`; + } + + /** + * A convenience accessor to test whether we are configuring the prototype Token for an Actor. + * @type {boolean} + */ + get isPrototype() { + return this.object instanceof Actor; + } + + /** + * Convenience access to the Actor document that this Token represents + * @type {Actor} + */ + get actor() { + return this.isPrototype ? this.object : this.token.actor; + } + + get title() { + const name = this.isPrototype + ? `[${game.i18n.localize('TOKEN.TitlePrototype')}] ${this.actor.name}` + : this.token.name; + return `${name}: ${game.i18n.localize('DDV.Title')}`; + } + + 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, + }; + } + + 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')); + return this; + } + return super.render(force, options); + } + + async _updateObject(event, formData) { + // Configure the Prototype Token data of an Actor + if (this.isPrototype) return this.actor.update({ token: formData }); + // Update an embedded Token document + else return this.token.update(formData); + } +} diff --git a/src/module/darkness-dependent-vision.js b/src/module/darkness-dependent-vision.js index d031692..0ecd71e 100644 --- a/src/module/darkness-dependent-vision.js +++ b/src/module/darkness-dependent-vision.js @@ -7,6 +7,7 @@ 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'; @@ -14,22 +15,30 @@ Hooks.once('init', async () => { logger.info(`Initializing ${packageName}`); try { - libWrapper.register('darkness-dependent-vision', 'Token.prototype.dimRadius', getDimRadius, 'OVERRIDE'); + libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.dimRadius', getDimRadius, 'OVERRIDE'); } catch (e) { - notifications.warn(game.i18n.format('DDV.ErrorFailedToOverride', { target: 'Token.prototype.dimRadius' }), { - log: true, - }); + notifications.warn( + game.i18n.format('DDV.ErrorFailedToOverride', { target: 'CONFIG.Token.documentClass.prototype.dimRadius' }), + { + log: true, + }, + ); } try { - libWrapper.register('darkness-dependent-vision', 'Token.prototype.brightRadius', getBrightRadius, 'OVERRIDE'); + libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.brightRadius', getBrightRadius, 'OVERRIDE'); } catch (e) { - notifications.warn(game.i18n.format('DDV.ErrorFailedToOverride', { target: 'Token.prototype.brightRadius' }), { - log: true, - }); + notifications.warn( + game.i18n.format('DDV.ErrorFailedToOverride', { target: 'CONFIG.Token.documentClass.prototype.brightRadius' }), + { + log: true, + }, + ); } - libWrapper.register('darkness-dependent-vision', 'Token.prototype.updateSource', updateSource, 'WRAPPER'); + libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.updateSource', updateSource, 'WRAPPER'); + + libWrapper.register(packageName, 'CONFIG.Token.sheetClass.prototype._getHeaderButtons', getHeaderButtons, 'WRAPPER'); }); /** @@ -37,12 +46,10 @@ Hooks.once('init', async () => { * its containing {@link Scene}? */ function hasDimVision() { - const dimVisionDarknessLowerBound = this.document.getFlag(packageName, 'dimVisionDarknessLowerBound') ?? -1; - const dimVisionDarknessUpperBound = this.document.getFlag(packageName, 'dimVisionDarknessUpperBound') ?? 2; - return ( - this.document.parent.data.darkness >= dimVisionDarknessLowerBound && - this.document.parent.data.darkness < dimVisionDarknessUpperBound - ); + 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; } /** @@ -50,12 +57,10 @@ function hasDimVision() { * its containing {@link Scene}? */ function hasBrightVision() { - const brightVisionDarknessLowerBound = this.document.getFlag(packageName, 'brightVisionDarknessLowerBound') ?? -1; - const brightVisionDarknessUpperBound = this.document.getFlag(packageName, 'brightVisionDarknessUpperBound') ?? 2; - return ( - this.document.parent.data.darkness >= brightVisionDarknessLowerBound && - this.document.parent.data.darkness < brightVisionDarknessUpperBound - ); + 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; } /** @@ -100,12 +105,9 @@ function getBrightRadius() { return this.getLightRadius(r); } -/** - * @typedef {({defer, deleted, noUpdateFog}?: {defer?: boolean, deleted?: boolean, noUpdateFog?: boolean}) => void} UpdateSourceFunction - */ - /** * 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. @@ -145,6 +147,29 @@ function updateSource(wrapped, { defer = false, deleted = false, noUpdateFog = f } } +/** + * 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()); @@ -153,10 +178,10 @@ Hooks.on('updateScene', (scene, change) => { Hooks.on('updateToken', (token, change) => { const shouldUpdateSource = [ - 'dimVisionDarknessLowerBound', - 'dimVisionDarknessUpperBound', - 'brightVisionDarknessLowerBound', - 'brightVisionDarknessUpperBound', + '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/templates/darkness-dependent-vision-config.hbs b/src/templates/darkness-dependent-vision-config.hbs new file mode 100644 index 0000000..baee810 --- /dev/null +++ b/src/templates/darkness-dependent-vision-config.hbs @@ -0,0 +1,40 @@ +{{!-- +SPDX-FileCopyrightText: 2021 Johannes Loher +// +SPDX-License-Identifier: MIT +--}} + +