diff --git a/CHANGELOG.md b/CHANGELOG.md index 29039f6..f04009a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.1.0 +* Extend actor-sense link support to NPCs +* Handle Active Effect updates (in addition to creates/deletes) + ## Version 0.0.5 * Limit See Invisibility detection mode to areas of map that are actually seen (contributed by dev7355608) diff --git a/README.md b/README.md index 9d25442..3db149a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,10 @@ Serviceable vision management for dnd5e ## How It Works -This module replaces the core darkvision mode with one that has fewer unwelcome visual surprises and is more in tune with dnd5e rules. It also adds a "devil's sight" vision mode and blindsight detection mode. +This module replaces some core vision and detection modes with ones that have fewer unwelcome visual surprises and is more in tune with dnd5e rules: +- Dim lighting outside an a token's darkvision range won't be rendered with higher brightness. +- If an invisible token stands in darkness, darkvision or a similar sense will be necessary to see it. +- Vision/detection modes have been added to support blindsight, truesight, and Devil's Sight. Additionally, it includes a setting ("Link Actor Senses") to link each PC token's vision and detection modes with its corresponding actor's senses. For greater convenience, consider using this setting with a global illumination threshold at a point where you deem "darkness" to end (e.g., 0.75 darkness). diff --git a/module.json b/module.json index 35aedeb..32f9fbf 100644 --- a/module.json +++ b/module.json @@ -11,7 +11,7 @@ "scripts": [ "script.js" ], - "version": "0.0.5", + "version": "0.0.6", "compatibility": { "minimum": "10", "verified": "10.288" diff --git a/script.js b/script.js index 4426ade..3cf7378 100644 --- a/script.js +++ b/script.js @@ -67,21 +67,24 @@ Hooks.on("updateActor", (actor, changes, context, userId) => { } }); -// Handle addition and removal of Devil's Sight +// Handle updates of actor senses via AEs Hooks.on("createActiveEffect", (effect) => { - // Could use a better check than a localization-unfriendly label if (effect.parent instanceof Actor) { updateTokens(effect.parent); } }); - +Hooks.on("updateActiveEffect", (effect) => { + if (effect.parent instanceof Actor) { + updateTokens(effect.parent); + } +}); Hooks.on("deleteActiveEffect", (effect) => { if (effect.parent instanceof Actor) { updateTokens(effect.parent); } }); -// Update when a new token is added to a scene +// Process when a new token is added or updated Hooks.on("createToken", (token, context, userId) => { if (token.actor) { Promise.resolve().then(() => { @@ -89,8 +92,6 @@ Hooks.on("createToken", (token, context, userId) => { }); } }); - -// Update token sources when a token is updated Hooks.on("updateToken", (token, changes, context, userId) => { if (!token.actor) return; @@ -113,13 +114,15 @@ function updateTokens(actor, { force = false } = {}) { const linkActorSenses = game.settings.get("adequate-vision", "linkActorSenses"); const tokenVisionEnabled = !!canvas.scene?.tokenVision; const userIsObserver = actor.getUserLevel(game.user) >= CONST.DOCUMENT_OWNERSHIP_LEVELS.OBSERVER; - const checks = [linkActorSenses, tokenVisionEnabled, userIsObserver, actor.type === "character"]; + const checks = [linkActorSenses, tokenVisionEnabled, userIsObserver, ["character", "npc"].includes(actor.type)]; if (!checks.every((c) => c)) return; const handledSenses = ["darkvision", "blindsight", "tremorsense", "truesight"]; const modes = Object.entries(actor.system.attributes.senses) .filter(([sense, range]) => handledSenses.includes(sense) && typeof range === "number" && range > 0) .reduce((entries, [sense, range]) => ({ ...entries, [sense]: range }), {}); + + // Could use a better check than a localization-unfriendly label if (actor.effects.some((e) => e.label === "Devil's Sight" && !e.disabled && !e.isSuppressed)) { modes.devilsSight = 120; } @@ -217,9 +220,7 @@ class InvisibilityDetectionMode extends DetectionMode { /** @override */ static getDetectionFilter() { - return this._detectionFilter ??= GlowOverlayFilter.create({ - glowColor: [0, 0.60, 0.33, 1] - }); + return (this._detectionFilter ??= GlowOverlayFilter.create({ glowColor: [0, 0.6, 0.33, 1] })); } /** @override */ @@ -227,10 +228,11 @@ class InvisibilityDetectionMode extends DetectionMode { const source = visionSource.object; // Only invisible tokens can be detected; the vision source must not be blinded - return !(source instanceof Token - && source.document.hasStatusEffect(CONFIG.specialStatusEffects.BLIND)) - && target instanceof Token - && target.document.hasStatusEffect(CONFIG.specialStatusEffects.INVISIBLE); + return ( + !(source instanceof Token && source.document.hasStatusEffect(CONFIG.specialStatusEffects.BLIND)) && + target instanceof Token && + target.document.hasStatusEffect(CONFIG.specialStatusEffects.INVISIBLE) + ); } /** @override */ @@ -240,13 +242,15 @@ class InvisibilityDetectionMode extends DetectionMode { // Temporarily remove the invisible status effect from the target (see TokenDocument#hasStatusEffect) if (!target.actor) { - const icon = CONFIG.statusEffects.find(e => e.id === statusId)?.icon; + const icon = CONFIG.statusEffects.find((e) => e.id === statusId)?.icon; effects = this.effects; - this.effects = this.effects.filter(e => e !== icon); + this.effects = this.effects.filter((e) => e !== icon); } else { - effects = target.actor.effects.filter(e => !e.disabled && e.getFlag("core", "statusId") === statusId); - effects.forEach(e => e.disabled = true); + effects = target.actor.effects.filter((e) => !e.disabled && e.getFlag("core", "statusId") === statusId); + for (const effect of effects) { + effect.disabled = true; + } } // Test visibility without the invisible status effect @@ -256,7 +260,9 @@ class InvisibilityDetectionMode extends DetectionMode { if (!target.actor) { this.effects = effects; } else { - effects.forEach(e => e.disabled = false); + for (const effect of effects) { + effect.disabled = false; + } } return isVisible;