Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Add support for blindsight and truesight, flip default of module sett…
Browse files Browse the repository at this point in the history
…ing (#7)
  • Loading branch information
stwlam authored Oct 13, 2022
1 parent b9641f5 commit f371fee
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 64 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ jobs:
draft: false
prerelease: false
token: ${{ secrets.GITHUB_TOKEN }}
artifacts: './module.json,./adequate-vision.zip'
artifacts: './module.json,./*.md,./adequate-vision.zip'
tag: ${{ env.moduleVersion }}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Version 0.0.4
* Add support for blindsight and truesight
* Fiddle with darkvision contrast and brightness defaults
* Change default value of Link Actor Senses setting to true

## Version 0.0.3
* Handle Devil's Sight with a new vision mode

Expand Down
18 changes: 4 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,13 @@

Serviceable vision management for dnd5e

## The Module
## How It Works

This module does three things:
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.

1. Replaces the core darkvision mode with one that has fewer unwelcome visual surprises and is more in tune with dnd5e rules.
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).

2. Adds a rudimentary "devil's sight" vision mode.

3. Includes a setting to link each PC token's vision and detection modes with its corresponding actor's senses. This is currently in testing, but it makes no server updates. For greater convenience, consider using this setting with a global illumination threshold at a point where you deem "darkness" to end.

Try it, and if you don't like it, disable the setting to undo all the changes made. Send feedback if you have suggestions on how it should work.

## Usage Instructions

1. Enable the module.

2. Set a token to have darkvision/devil's sight. For a hands-free experience, enable the "Link Actor Senses" setting.
No server updates are made with the "Link Actor Senses" setting. Try it, and if you don't like it, disable the setting to undo all the changes made. The vision and detection modes will still be available with the setting disabled.

---

Expand Down
62 changes: 31 additions & 31 deletions module.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
{
"id": "adequate-vision",
"title": "Adequate Vision",
"description": "Serviceable vision management for dnd5e",
"authors": [
{
"name": "Shark that walks like a man"
"id": "adequate-vision",
"title": "Adequate Vision",
"description": "Serviceable vision management for dnd5e",
"authors": [
{
"name": "Shark that walks like a man",
"flags": {}
}
],
"scripts": [
"script.js"
],
"version": "0.0.4",
"compatibility": {
"minimum": "10",
"verified": "10.288"
},
"relationships": {
"systems": [
{
"id": "dnd5e",
"type": "system",
"compatibility": {
"minimum": "2.0.3",
"verified": "2.0.3"
}
],
"scripts": [
"script.js"
],
"version": "0.0.3",
"compatibility": {
"minimum": "10.286",
"verified": "10.288",
"maximum": "10"
},
"relationships": {
"systems": [
{
"id": "dnd5e",
"type": "system",
"compatibility": {
"minimum": "2.0.3",
"verified": "2.0.3"
}
}
]
},
"url": "https://github.com/stwlam/adequate-vision",
"manifest": "https://github.com/stwlam/adequate-vision/releases/latest/download/module.json",
"download": "https://github.com/stwlam/adequate-vision/releases/latest/download/adequate-vision.zip"
}
]
},
"url": "https://github.com/stwlam/adequate-vision",
"manifest": "https://github.com/stwlam/adequate-vision/releases/latest/download/module.json",
"download": "https://github.com/stwlam/adequate-vision/releases/latest/download/adequate-vision.zip"
}
95 changes: 77 additions & 18 deletions script.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Hooks.once("init", () => {
},
vision: {
darkness: { adaptive: true },
defaults: { contrast: 0.05, saturation: -1.0, brightness: 0.75 },
defaults: { contrast: 0, saturation: -1.0, brightness: 0.65 },
},
});

Expand All @@ -28,31 +28,34 @@ Hooks.once("init", () => {
},
vision: {
darkness: { adaptive: true },
defaults: { contrast: 0, saturation: 0, brightness: 0.75, range: 120 },
defaults: { contrast: 0, saturation: 0, brightness: 0.65, range: 120 },
},
});

CONFIG.Canvas.detectionModes.blindsight = new BlindDetectionMode();
});

// Register setting
Hooks.once("setup", () => {
game.settings.register("adequate-vision", "linkActorSenses", {
name: "Link Actor Senses (In Testing!)",
hint: "Automatically add and remove vision/detection modes according to the senses possessed by each token's corresponding actor. Currently only supported for PCs.",
hint: "Automatically manage vision/detection modes according to the senses possessed by each token's corresponding actor. Currently only supported for PCs.",
scope: "world",
config: true,
default: false,
default: true,
requiresReload: true,
type: Boolean,
});
});

// Update token sources every time a scene is viewed, including on initial load
// Update token sources when the game is ready
Hooks.once("ready", () => {
onReady();
});

// Update token sources every time a scene is viewed
Hooks.on("canvasReady", () => {
const tokens = canvas.scene?.tokens.contents ?? [];
const actors = new Set(tokens.flatMap((t) => t.actor ?? []));
for (const actor of actors) {
updateTokens(actor);
}
if (game.ready) onReady();
});

// Update token sources when an actor's senses are updated
Expand All @@ -77,6 +80,15 @@ Hooks.on("deleteActiveEffect", (effect) => {
}
});

// Update when a new token is added to a scene
Hooks.on("createToken", (token, context, userId) => {
if (token.actor) {
Promise.resolve().then(() => {
updateTokens(token.actor);
});
}
});

// Update token sources when a token is updated
Hooks.on("updateToken", (token, changes, context, userId) => {
if (!token.actor) return;
Expand All @@ -87,15 +99,23 @@ Hooks.on("updateToken", (token, changes, context, userId) => {
}
});

function updateTokens(actor) {
function onReady() {
const tokens = canvas.scene?.tokens.contents ?? [];
const actors = new Set(tokens.flatMap((t) => t.actor ?? []));
for (const actor of actors) {
updateTokens(actor, { force: true });
}
}

function updateTokens(actor, { force = false } = {}) {
// Only make updates if the following are true
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"];
if (!checks.every((c) => c)) return;

const handledSenses = ["darkvision", "blindsight", "tremorsense"];
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 }), {});
Expand All @@ -108,6 +128,7 @@ function updateTokens(actor) {
for (const token of tokens) {
const updates = {};
const { sight, detectionModes } = token;
const canSeeInDark = ["darkvision", "devilsSight", "truesight"].some((m) => !!modes[m]);

// Devil's sight and darkvision
if (modes.devilsSight && (sight.visionMode !== "devilsSight" || sight.range !== mode.devilsSight)) {
Expand All @@ -116,18 +137,31 @@ function updateTokens(actor) {
} else if (modes.darkvision && (sight.visionMode !== "darkvision" || sight.range !== modes.darkvision)) {
const defaults = CONFIG.Canvas.visionModes.darkvision.vision.defaults;
updates.sight = { visionMode: "darkvision", ...defaults, range: modes.darkvision };
} else {
} else if (!canSeeInDark && token.sight.visionMode !== "basic" && token.sight.range !== null) {
updates.sight = { visionMode: "basic", contrast: 0, brightness: 0, saturation: 0, range: null };
}

// Blindsight
if (modes.blindsight) {
updates.detectionModes ??= [];
updates.detectionModes.push({ id: "blindsight", enabled: true, range: modes.blindsight });
}

// Truesight
if (modes.truesight && sight.visionMode !== "devilsSight") {
const defaults = CONFIG.Canvas.visionModes.devilsSight.vision.defaults;
const range = Math.max(modes.truesight, modes.devilsSight ?? 0);
updates.sight = { visionMode: "devilsSight", ...defaults, range };
updates.detectionModes ??= [];
updates.detectionModes.push({ id: "seeInvisibility", enabled: true, range: modes.truesight });
}

// Tremorsense
if (modes.tremorsense) {
const hasFeelTremor = detectionModes.some((m) => m.id === "feelTremor" && m.range === mode.tremorsense);
if (!hasFeelTremor) {
updates.detectionModes = [
{ id: "feelTremor", enabled: true, range: modes.tremorsense },
...token._source.detectionModes.filter((m) => m.id !== "feelTremor"),
];
updates.detectionModes ??= [];
updates.detectionModes.push({ id: "feelTremor", enabled: true, range: modes.tremorsense });
}
} else if (detectionModes.some((m) => m.id === "feelTremor")) {
updates.detectionModes = token._source.detectionModes.filter((m) => m.id !== "feelTremor");
Expand All @@ -141,7 +175,32 @@ function updateTokens(actor) {
}

// Reinitialize vision and refresh lighting
if (madeUpdates && (game.user.character || canvas.tokens.controlled.length > 0)) {
if (madeUpdates || force) {
canvas.perception.update({ initializeVision: true, refreshLighting: true }, true);
}
}

class BlindDetectionMode extends DetectionMode {
constructor() {
super({
id: "blindsight",
label: "Blindsight",
type: DetectionMode.DETECTION_TYPES.SIGHT,
});
}

/** @override */
static getDetectionFilter() {
const filter = (this._detectionFilter ??= OutlineOverlayFilter.create({
wave: true,
knockout: false,
}));
filter.thickness = 1;
return filter;
}

/** @override */
_canDetect(visionSource, target) {
return target instanceof Token || target instanceof DoorControl;
}
}

0 comments on commit f371fee

Please sign in to comment.