Add form application to configure darkness dependent vision per token
This commit is contained in:
parent
32ae12c6bd
commit
47b5793087
13 changed files with 266 additions and 30 deletions
|
@ -19,6 +19,7 @@ module.exports = {
|
||||||
|
|
||||||
globals: {
|
globals: {
|
||||||
foundry: false,
|
foundry: false,
|
||||||
|
PrototypeTokenDocument: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
|
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["arcanis.vscode-zipfs", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||||
|
}
|
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.yarn": true,
|
||||||
|
"**/.pnp.*": true
|
||||||
|
},
|
||||||
|
"eslint.nodePath": ".yarn/sdks",
|
||||||
|
"prettier.prettierPath": ".yarn/sdks/prettier/index.js"
|
||||||
|
}
|
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
20
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
|
@ -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`);
|
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
20
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
|
@ -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`);
|
6
.yarn/sdks/eslint/package.json
vendored
Normal file
6
.yarn/sdks/eslint/package.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "eslint",
|
||||||
|
"version": "7.29.0-pnpify",
|
||||||
|
"main": "./lib/api.js",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
5
.yarn/sdks/integrations.yml
vendored
Normal file
5
.yarn/sdks/integrations.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
# This file is automatically generated by PnPify.
|
||||||
|
# Manual changes will be lost!
|
||||||
|
|
||||||
|
integrations:
|
||||||
|
- vscode
|
20
.yarn/sdks/prettier/index.js
vendored
Executable file
20
.yarn/sdks/prettier/index.js
vendored
Executable file
|
@ -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`);
|
6
.yarn/sdks/prettier/package.json
vendored
Normal file
6
.yarn/sdks/prettier/package.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "prettier",
|
||||||
|
"version": "2.3.2-pnpify",
|
||||||
|
"main": "./index.js",
|
||||||
|
"type": "commonjs"
|
||||||
|
}
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
75
src/module/darkness-dependent-vision-config.js
Normal file
75
src/module/darkness-dependent-vision-config.js
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import { libWrapper } from './shims/libWrapperShim';
|
import { libWrapper } from './shims/libWrapperShim';
|
||||||
import notifications from './notifications';
|
import notifications from './notifications';
|
||||||
|
import { DarknessDependentVisionConfig } from './darkness-dependent-vision-config';
|
||||||
|
|
||||||
const packageName = 'darkness-dependent-vision';
|
const packageName = 'darkness-dependent-vision';
|
||||||
|
|
||||||
|
@ -14,22 +15,30 @@ Hooks.once('init', async () => {
|
||||||
logger.info(`Initializing ${packageName}`);
|
logger.info(`Initializing ${packageName}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
libWrapper.register('darkness-dependent-vision', 'Token.prototype.dimRadius', getDimRadius, 'OVERRIDE');
|
libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.dimRadius', getDimRadius, 'OVERRIDE');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notifications.warn(game.i18n.format('DDV.ErrorFailedToOverride', { target: 'Token.prototype.dimRadius' }), {
|
notifications.warn(
|
||||||
log: true,
|
game.i18n.format('DDV.ErrorFailedToOverride', { target: 'CONFIG.Token.documentClass.prototype.dimRadius' }),
|
||||||
});
|
{
|
||||||
|
log: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
libWrapper.register('darkness-dependent-vision', 'Token.prototype.brightRadius', getBrightRadius, 'OVERRIDE');
|
libWrapper.register(packageName, 'CONFIG.Token.objectClass.prototype.brightRadius', getBrightRadius, 'OVERRIDE');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notifications.warn(game.i18n.format('DDV.ErrorFailedToOverride', { target: 'Token.prototype.brightRadius' }), {
|
notifications.warn(
|
||||||
log: true,
|
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}?
|
* its containing {@link Scene}?
|
||||||
*/
|
*/
|
||||||
function hasDimVision() {
|
function hasDimVision() {
|
||||||
const dimVisionDarknessLowerBound = this.document.getFlag(packageName, 'dimVisionDarknessLowerBound') ?? -1;
|
const dimVisionDarknessMin = this.document.getFlag(packageName, 'dimVisionDarknessMin') ?? 0;
|
||||||
const dimVisionDarknessUpperBound = this.document.getFlag(packageName, 'dimVisionDarknessUpperBound') ?? 2;
|
const dimVisionDarknessMax = this.document.getFlag(packageName, 'dimVisionDarknessMax') ?? 1;
|
||||||
return (
|
const darkness = this.document.parent.data.darkness;
|
||||||
this.document.parent.data.darkness >= dimVisionDarknessLowerBound &&
|
return dimVisionDarknessMin <= darkness && darkness <= dimVisionDarknessMax;
|
||||||
this.document.parent.data.darkness < dimVisionDarknessUpperBound
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -50,12 +57,10 @@ function hasDimVision() {
|
||||||
* its containing {@link Scene}?
|
* its containing {@link Scene}?
|
||||||
*/
|
*/
|
||||||
function hasBrightVision() {
|
function hasBrightVision() {
|
||||||
const brightVisionDarknessLowerBound = this.document.getFlag(packageName, 'brightVisionDarknessLowerBound') ?? -1;
|
const brightVisionDarknessMin = this.document.getFlag(packageName, 'brightVisionDarknessMin') ?? 0;
|
||||||
const brightVisionDarknessUpperBound = this.document.getFlag(packageName, 'brightVisionDarknessUpperBound') ?? 2;
|
const brightVisionDarknessMax = this.document.getFlag(packageName, 'brightVisionDarknessMax') ?? 1;
|
||||||
return (
|
const darkness = this.document.parent.data.darkness;
|
||||||
this.document.parent.data.darkness >= brightVisionDarknessLowerBound &&
|
return brightVisionDarknessMin <= darkness && darkness <= brightVisionDarknessMax;
|
||||||
this.document.parent.data.darkness < brightVisionDarknessUpperBound
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -100,12 +105,9 @@ function getBrightRadius() {
|
||||||
return this.getLightRadius(r);
|
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
|
* 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 {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} [defer] Defer refreshing the SightLayer to manually call that refresh later.
|
||||||
* @param {boolean} [deleted] Indicate that this light source has been deleted.
|
* @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) => {
|
Hooks.on('updateScene', (scene, change) => {
|
||||||
if (change.darkness != null) {
|
if (change.darkness != null) {
|
||||||
scene.getEmbeddedCollection('Token').forEach((tokenDocument) => tokenDocument.object.updateSource());
|
scene.getEmbeddedCollection('Token').forEach((tokenDocument) => tokenDocument.object.updateSource());
|
||||||
|
@ -153,10 +178,10 @@ Hooks.on('updateScene', (scene, change) => {
|
||||||
|
|
||||||
Hooks.on('updateToken', (token, change) => {
|
Hooks.on('updateToken', (token, change) => {
|
||||||
const shouldUpdateSource = [
|
const shouldUpdateSource = [
|
||||||
'dimVisionDarknessLowerBound',
|
'dimVisionDarknessMin',
|
||||||
'dimVisionDarknessUpperBound',
|
'dimVisionDarknessMax',
|
||||||
'brightVisionDarknessLowerBound',
|
'brightVisionDarknessMin',
|
||||||
'brightVisionDarknessUpperBound',
|
'brightVisionDarknessMax',
|
||||||
].some((flagKey) => `flags.darkness-dependent-vision.${flagKey}` in foundry.utils.flattenObject(change));
|
].some((flagKey) => `flags.darkness-dependent-vision.${flagKey}` in foundry.utils.flattenObject(change));
|
||||||
if (shouldUpdateSource) {
|
if (shouldUpdateSource) {
|
||||||
token.object.updateSource();
|
token.object.updateSource();
|
||||||
|
|
40
src/templates/darkness-dependent-vision-config.hbs
Normal file
40
src/templates/darkness-dependent-vision-config.hbs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{{!--
|
||||||
|
SPDX-FileCopyrightText: 2021 Johannes Loher
|
||||||
|
//
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
--}}
|
||||||
|
|
||||||
|
<form class="{{cssClasses}}" autocomplete="off">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ localize 'DDV.DimVisionDarknessRange' }}</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<label for="flags.darkness-dependent-vision.dimVisionDarknessMin">{{ localize "Between" }}</label>
|
||||||
|
<input type="number" name="flags.darkness-dependent-vision.dimVisionDarknessMin"
|
||||||
|
value="{{object.flags.darkness-dependent-vision.dimVisionDarknessMin}}" min="0" max="1" step="any"
|
||||||
|
placeholder="0" />
|
||||||
|
<label for="flags.darkness-dependent-vision.dimVisionDarknessMax">{{ localize "and" }}</label>
|
||||||
|
<input type="number" name="flags.darkness-dependent-vision.dimVisionDarknessMax"
|
||||||
|
value="{{object.flags.darkness-dependent-vision.dimVisionDarknessMax}}" min="0" max="1" step="any"
|
||||||
|
placeholder="1" />
|
||||||
|
</div>
|
||||||
|
<p class="hint">{{ localize "DDV.DimVisionDarknessRangeHint" }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>{{ localize 'DDV.BrightVisionDarknessRange' }}</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<label for="flags.darkness-dependent-vision.brightVisionDarknessMin">{{ localize "Between" }}</label>
|
||||||
|
<input type="number" name="flags.darkness-dependent-vision.brightVisionDarknessMin"
|
||||||
|
value="{{object.flags.darkness-dependent-vision.brightVisionDarknessMin}}" min="0" max="1" step="any"
|
||||||
|
placeholder="0" />
|
||||||
|
<label for="flags.darkness-dependent-vision.brightVisionDarknessMax">{{ localize "and" }}</label>
|
||||||
|
<input type="number" name="flags.darkness-dependent-vision.brightVisionDarknessMax"
|
||||||
|
value="{{object.flags.darkness-dependent-vision.brightVisionDarknessMax}}" min="0" max="1" step="any"
|
||||||
|
placeholder="1" />
|
||||||
|
</div>
|
||||||
|
<p class="hint">{{ localize "DDV.BrightVisionDarknessRangeHint" }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" name="submit" value="1"><i class="far fa-save"></i> {{localize "TOKEN.Update"}}</button>
|
||||||
|
</form>
|
Loading…
Reference in a new issue