// SPDX-FileCopyrightText: 2021 Johannes Loher // // SPDX-License-Identifier: MIT '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(); } });