Compare commits
10
Commits
f75c8b9d3c
...
dad5eef9aa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad5eef9aa | ||
|
|
425919e9a3 | ||
|
|
31a8f85e54 | ||
|
|
5b3094bd4f | ||
|
|
eaca56c07f | ||
|
|
c4cfd59613 | ||
|
|
63a7fbac9b | ||
|
|
358ed9ad5b | ||
|
|
cf536e18d6 | ||
|
|
64d0d29a7c |
@@ -0,0 +1 @@
|
|||||||
|
TODO
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
# Basic Aura Highlighting
|
# Basic Aura Highlighting
|
||||||
|
|
||||||
A small, FoundryVTT Pathfinder 2e/Starfinder 2e module that places a highlight around tokens in another token's aura.
|
A module that places a highlight around tokens in another token's aura.
|
||||||
|
|||||||
@@ -11,6 +11,10 @@
|
|||||||
"name": "Refresh During Movement",
|
"name": "Refresh During Movement",
|
||||||
"hint": "If disabled, aura highlights will only refresh when a token stops moving or its settings are updated. This can improve performance."
|
"hint": "If disabled, aura highlights will only refresh when a token stops moving or its settings are updated. This can improve performance."
|
||||||
},
|
},
|
||||||
|
"thickness": {
|
||||||
|
"name": "Highlight Thickness",
|
||||||
|
"hint": "The thickness of the aura highlights in pixels (1-100)."
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"name": "Aura Name"
|
"name": "Aura Name"
|
||||||
},
|
},
|
||||||
@@ -26,6 +30,13 @@
|
|||||||
"to": "To",
|
"to": "To",
|
||||||
"edge": "Edge",
|
"edge": "Edge",
|
||||||
"center": "Center"
|
"center": "Center"
|
||||||
|
},
|
||||||
|
"walls": {
|
||||||
|
"name": "Restricted by Walls",
|
||||||
|
"hint": "If enabled, the aura will be blocked by walls (Line of Sight). GM Only."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"name": "Enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-13
@@ -1,28 +1,19 @@
|
|||||||
{
|
{
|
||||||
"id": "basic_aura_highlighting",
|
"id": "basic_aura_highlighting",
|
||||||
"title": "Basic Aura Highlighting",
|
"title": "Basic Aura Highlighting",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimum": "13",
|
"minimum": "13",
|
||||||
"verified": "13",
|
"verified": "13",
|
||||||
"maximum": "14"
|
"maximum": "14"
|
||||||
},
|
},
|
||||||
"description": "<p>A module specifically for Pathfinder 2e that places a highlight around tokens in another token's aura. This aura can be measured from the center of the tokens or edge to edge. See the token or prototype token menus for configuration.</p>",
|
"description": "<p>A module that places a highlight around tokens in another token's aura. This aura can be measured from the center of the tokens or edge to edge. See the token or prototype token menus for configuration.</p>",
|
||||||
"relationships": {
|
|
||||||
"systems": [
|
|
||||||
{
|
|
||||||
"id": "pf2e",
|
|
||||||
"manifest": "https://github.com/foundryvtt/pf2e/releases/latest/download/system.json",
|
|
||||||
"compatibility": {
|
|
||||||
"minimum": "6.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"flags": {
|
"flags": {
|
||||||
"canUpload": true
|
"canUpload": true
|
||||||
},
|
},
|
||||||
"url": "https://codeberg.org/Grimsace/basic_aura_highlighting",
|
"url": "https://codeberg.org/Grimsace/basic_aura_highlighting",
|
||||||
|
"manifest": "https://codeberg.org/Grimsace/basic_aura_highlighting/raw/branch/main/module.json",
|
||||||
|
"download": "https://codeberg.org/Grimsace/basic_aura_highlighting/archive/main.zip",
|
||||||
"esmodules": ["scripts/module.js"],
|
"esmodules": ["scripts/module.js"],
|
||||||
"styles": ["styles/module.css"],
|
"styles": ["styles/module.css"],
|
||||||
"languages": [
|
"languages": [
|
||||||
|
|||||||
+243
-82
@@ -1,12 +1,17 @@
|
|||||||
|
// Basic Aura Highlighting
|
||||||
|
// A module for Foundry VTT that highlights tokens within specified aura ranges.
|
||||||
|
|
||||||
const MODULE_ID = "basic_aura_highlighting";
|
const MODULE_ID = "basic_aura_highlighting";
|
||||||
const AURAS_TAB = "basic-aura-highlighting-auras";
|
const AURAS_TAB = "basic-aura-highlighting-auras";
|
||||||
const AURAS_FLAG_PATH = `flags.${MODULE_ID}.auras`;
|
const AURAS_FLAG_PATH = `flags.${MODULE_ID}.auras`;
|
||||||
const HIGHLIGHT_CONTAINER_NAME = `${MODULE_ID}.highlights`;
|
const HIGHLIGHT_CONTAINER_NAME = `${MODULE_ID}.highlights`;
|
||||||
const HIGHLIGHT_LINE_WIDTH = 4;
|
|
||||||
const HIGHLIGHT_RING_SPACING = 5;
|
const HIGHLIGHT_RING_SPACING = 5;
|
||||||
const HIGHLIGHT_ALPHA_THRESHOLD = 0.2;
|
const HIGHLIGHT_ALPHA_THRESHOLD = 0.2;
|
||||||
const ALPHA_SAMPLE_SIZE = 48;
|
const ALPHA_SAMPLE_SIZE = 48;
|
||||||
const HIGHLIGHT_FILTER_MARKER = Symbol(MODULE_ID);
|
const HIGHLIGHT_FILTER_MARKER = Symbol(MODULE_ID);
|
||||||
|
const THICKNESS_SETTING = "thickness";
|
||||||
|
|
||||||
|
// Shader for drawing an outline around the token's non-transparent pixels
|
||||||
const ALPHA_OUTLINE_FRAGMENT_SHADER = `
|
const ALPHA_OUTLINE_FRAGMENT_SHADER = `
|
||||||
varying vec2 vTextureCoord;
|
varying vec2 vTextureCoord;
|
||||||
uniform sampler2D uSampler;
|
uniform sampler2D uSampler;
|
||||||
@@ -48,6 +53,7 @@ const alphaShapeCache = new Map();
|
|||||||
|
|
||||||
const REFRESH_DURING_MOVE_SETTING = "refreshDuringMove";
|
const REFRESH_DURING_MOVE_SETTING = "refreshDuringMove";
|
||||||
|
|
||||||
|
// Initialization: Register settings and hooks
|
||||||
Hooks.once("init", () => {
|
Hooks.once("init", () => {
|
||||||
game.settings.register(MODULE_ID, REFRESH_DURING_MOVE_SETTING, {
|
game.settings.register(MODULE_ID, REFRESH_DURING_MOVE_SETTING, {
|
||||||
name: localize("settings.refreshDuringMove.name"),
|
name: localize("settings.refreshDuringMove.name"),
|
||||||
@@ -58,16 +64,35 @@ Hooks.once("init", () => {
|
|||||||
default: true,
|
default: true,
|
||||||
onChange: scheduleAuraHighlightRefresh,
|
onChange: scheduleAuraHighlightRefresh,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
game.settings.register(MODULE_ID, THICKNESS_SETTING, {
|
||||||
|
name: localize("settings.thickness.name"),
|
||||||
|
hint: localize("settings.thickness.hint"),
|
||||||
|
scope: "client",
|
||||||
|
config: true,
|
||||||
|
type: Number,
|
||||||
|
range: {
|
||||||
|
min: 1,
|
||||||
|
max: 100,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
|
default: 4,
|
||||||
|
onChange: scheduleAuraHighlightRefresh,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Hooks.on("renderTokenConfig", injectAurasTab);
|
Hooks.on("renderTokenConfig", injectAurasTab);
|
||||||
Hooks.on("renderPrototypeTokenConfig", injectAurasTab);
|
Hooks.on("renderPrototypeTokenConfig", injectAurasTab);
|
||||||
|
|
||||||
|
// Data parsing hooks
|
||||||
Hooks.on("preUpdateToken", (_token, changes) =>
|
Hooks.on("preUpdateToken", (_token, changes) =>
|
||||||
parseAurasInChanges(changes, AURAS_FLAG_PATH),
|
parseAurasInChanges(changes, AURAS_FLAG_PATH),
|
||||||
);
|
);
|
||||||
Hooks.on("preUpdateActor", (_actor, changes) =>
|
Hooks.on("preUpdateActor", (_actor, changes) =>
|
||||||
parseAurasInChanges(changes, `prototypeToken.${AURAS_FLAG_PATH}`),
|
parseAurasInChanges(changes, `prototypeToken.${AURAS_FLAG_PATH}`),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Refresh hooks
|
||||||
Hooks.on("canvasReady", scheduleAuraHighlightRefresh);
|
Hooks.on("canvasReady", scheduleAuraHighlightRefresh);
|
||||||
Hooks.on("canvasTearDown", clearAuraHighlights);
|
Hooks.on("canvasTearDown", clearAuraHighlights);
|
||||||
Hooks.on("controlToken", scheduleAuraHighlightRefresh);
|
Hooks.on("controlToken", scheduleAuraHighlightRefresh);
|
||||||
@@ -89,6 +114,7 @@ Hooks.on("refreshToken", (token) => {
|
|||||||
Hooks.on("createToken", scheduleAuraHighlightRefresh);
|
Hooks.on("createToken", scheduleAuraHighlightRefresh);
|
||||||
Hooks.on("deleteToken", scheduleAuraHighlightRefresh);
|
Hooks.on("deleteToken", scheduleAuraHighlightRefresh);
|
||||||
|
|
||||||
|
// UI Injection: Add the "Auras" tab to Token Configuration
|
||||||
function injectAurasTab(app, element) {
|
function injectAurasTab(app, element) {
|
||||||
const root = getRootElement(element);
|
const root = getRootElement(element);
|
||||||
if (!root || root.querySelector(`[data-tab="${AURAS_TAB}"]`)) return;
|
if (!root || root.querySelector(`[data-tab="${AURAS_TAB}"]`)) return;
|
||||||
@@ -167,6 +193,7 @@ function createAurasTabPanel(group, auras) {
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Event handling for the Auras configuration tab
|
||||||
function activateAurasControls(root, group, panel) {
|
function activateAurasControls(root, group, panel) {
|
||||||
const form = root.querySelector("form");
|
const form = root.querySelector("form");
|
||||||
const aurasTabButton = root.querySelector(
|
const aurasTabButton = root.querySelector(
|
||||||
@@ -224,14 +251,23 @@ function renderAuras(panel, auras) {
|
|||||||
function renderAura(aura, index) {
|
function renderAura(aura, index) {
|
||||||
const id = escapeHtml(aura.id);
|
const id = escapeHtml(aura.id);
|
||||||
const nameLabel = localize("settings.name.name");
|
const nameLabel = localize("settings.name.name");
|
||||||
const rangeLabel = localize("settings.radius.name");
|
const rangeLabel = `${localize("settings.radius.name")} (Scene Units)`;
|
||||||
const colorLabel = localize("settings.color.name");
|
const colorLabel = localize("settings.color.name");
|
||||||
const fromLabel = localize("settings.measurement.from");
|
const fromLabel = localize("settings.measurement.from");
|
||||||
const toLabel = localize("settings.measurement.to");
|
const toLabel = localize("settings.measurement.to");
|
||||||
|
const wallsLabel = localize("settings.walls.name");
|
||||||
|
const enabledLabel = localize("settings.enabled.name");
|
||||||
|
const isGM = game.user.isGM;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<fieldset class="basic-aura-highlighting__aura" data-aura-id="${id}">
|
<fieldset class="basic-aura-highlighting__aura" data-aura-id="${id}">
|
||||||
<legend>${escapeHtml(aura.name || defaultAuraName(index))}</legend>
|
<legend>${escapeHtml(aura.name || defaultAuraName(index))}</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${id}-enabled">${enabledLabel}</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input id="${id}-enabled" type="checkbox" ${aura.enabled ? "checked" : ""} data-aura-field="enabled">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${id}-name">${nameLabel}</label>
|
<label for="${id}-name">${nameLabel}</label>
|
||||||
<div class="form-fields">
|
<div class="form-fields">
|
||||||
@@ -243,7 +279,6 @@ function renderAura(aura, index) {
|
|||||||
<div class="form-fields">
|
<div class="form-fields">
|
||||||
<input id="${id}-range" type="number" min="0" step="0.5" value="${aura.range}" data-aura-field="range">
|
<input id="${id}-range" type="number" min="0" step="0.5" value="${aura.range}" data-aura-field="range">
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">${localize("settings.radius.hint")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="${id}-color">${colorLabel}</label>
|
<label for="${id}-color">${colorLabel}</label>
|
||||||
@@ -269,6 +304,12 @@ function renderAura(aura, index) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${id}-walls">${wallsLabel}</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input id="${id}-walls" type="checkbox" ${aura.walls ? "checked" : ""} data-aura-field="walls" ${isGM ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="button" class="basic-aura-highlighting__delete" data-action="delete-aura">
|
<button type="button" class="basic-aura-highlighting__delete" data-action="delete-aura">
|
||||||
<i class="fa-solid fa-trash"></i> ${localize("tabs.delete")}
|
<i class="fa-solid fa-trash"></i> ${localize("tabs.delete")}
|
||||||
@@ -278,6 +319,7 @@ function renderAura(aura, index) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data Management: Parsing and normalizing aura flags
|
||||||
function readAuras(panel) {
|
function readAuras(panel) {
|
||||||
return Array.from(panel.querySelectorAll("[data-aura-id]")).map((item) => ({
|
return Array.from(panel.querySelectorAll("[data-aura-id]")).map((item) => ({
|
||||||
id: item.dataset.auraId,
|
id: item.dataset.auraId,
|
||||||
@@ -290,6 +332,8 @@ function readAuras(panel) {
|
|||||||
to: normalizeMeasurementMode(
|
to: normalizeMeasurementMode(
|
||||||
item.querySelector("[data-aura-field='to']")?.value,
|
item.querySelector("[data-aura-field='to']")?.value,
|
||||||
),
|
),
|
||||||
|
walls: !!item.querySelector("[data-aura-field='walls']")?.checked,
|
||||||
|
enabled: !!item.querySelector("[data-aura-field='enabled']")?.checked,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +351,8 @@ function normalizeAura(aura = {}) {
|
|||||||
color: typeof aura.color === "string" ? aura.color : "#ff0000",
|
color: typeof aura.color === "string" ? aura.color : "#ff0000",
|
||||||
from: normalizeMeasurementMode(aura.from ?? legacyMeasurement),
|
from: normalizeMeasurementMode(aura.from ?? legacyMeasurement),
|
||||||
to: normalizeMeasurementMode(aura.to ?? legacyMeasurement),
|
to: normalizeMeasurementMode(aura.to ?? legacyMeasurement),
|
||||||
|
walls: aura.walls ?? true,
|
||||||
|
enabled: aura.enabled ?? true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,6 +397,9 @@ function parseAuras(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rendering Logic: Highlights and calculations
|
||||||
|
|
||||||
|
// Debounce highlight refreshes to avoid excessive calculations
|
||||||
function scheduleAuraHighlightRefresh() {
|
function scheduleAuraHighlightRefresh() {
|
||||||
if (pendingHighlightRefresh !== null) return;
|
if (pendingHighlightRefresh !== null) return;
|
||||||
|
|
||||||
@@ -422,131 +471,228 @@ function clearHighlightContainer(container) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Distance Calculation Logic
|
||||||
|
|
||||||
|
// Find which tokens are within range of which auras
|
||||||
function collectAuraHighlights(sourceTokens) {
|
function collectAuraHighlights(sourceTokens) {
|
||||||
const highlights = new Map();
|
const highlights = new Map();
|
||||||
|
const tokens = globalThis.canvas.tokens.placeables;
|
||||||
|
const pixelsPerUnit = getPixelsPerSceneUnit();
|
||||||
|
|
||||||
|
// Cache token-specific data for the duration of this refresh
|
||||||
|
const tokenCache = new Map();
|
||||||
|
|
||||||
for (const source of sourceTokens) {
|
for (const source of sourceTokens) {
|
||||||
const auras = getTokenAuras(source);
|
const auras = getTokenAuras(source);
|
||||||
for (const target of globalThis.canvas.tokens.placeables) {
|
if (auras.length === 0) continue;
|
||||||
|
|
||||||
|
const sourceData = getCachedTokenData(source, tokenCache, pixelsPerUnit);
|
||||||
|
|
||||||
|
for (const target of tokens) {
|
||||||
if (target === source || target.document.hidden) continue;
|
if (target === source || target.document.hidden) continue;
|
||||||
|
|
||||||
for (const aura of auras) {
|
const targetData = getCachedTokenData(target, tokenCache, pixelsPerUnit);
|
||||||
if (!isTokenInAura(source, target, aura)) continue;
|
const dz = Math.abs(sourceData.elevation - targetData.elevation);
|
||||||
|
const dz2 = dz * dz;
|
||||||
|
|
||||||
|
for (const aura of auras) {
|
||||||
|
if (!aura.enabled) continue;
|
||||||
|
|
||||||
|
const rangePixels = aura.range * pixelsPerUnit;
|
||||||
|
const rangePixels2 = rangePixels * rangePixels;
|
||||||
|
|
||||||
|
// Broad phase: check if elevation difference alone exceeds range
|
||||||
|
if (dz2 > rangePixels2) continue;
|
||||||
|
|
||||||
|
if (
|
||||||
|
isTokenInAuraCached(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
aura,
|
||||||
|
sourceData,
|
||||||
|
targetData,
|
||||||
|
dz2,
|
||||||
|
rangePixels2,
|
||||||
|
)
|
||||||
|
) {
|
||||||
const targetHighlights = highlights.get(target.id) ?? [];
|
const targetHighlights = highlights.get(target.id) ?? [];
|
||||||
targetHighlights.push(aura);
|
targetHighlights.push(aura);
|
||||||
highlights.set(target.id, targetHighlights);
|
highlights.set(target.id, targetHighlights);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return highlights;
|
return highlights;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCachedTokenData(token, cache, pixelsPerUnit) {
|
||||||
|
let data = cache.get(token.id);
|
||||||
|
if (data) return data;
|
||||||
|
|
||||||
|
data = {
|
||||||
|
center: token.center,
|
||||||
|
bounds: token.bounds,
|
||||||
|
elevation: (token.document?.elevation ?? 0) * pixelsPerUnit,
|
||||||
|
alphaPoints: null, // Lazy loaded
|
||||||
|
};
|
||||||
|
cache.set(token.id, data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
function getTokenAuras(token) {
|
function getTokenAuras(token) {
|
||||||
return normalizeAuras(
|
return normalizeAuras(
|
||||||
parseAuras(token.document?.getFlag?.(MODULE_ID, "auras")),
|
parseAuras(token.document?.getFlag?.(MODULE_ID, "auras")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTokenInAura(source, target, aura) {
|
function isTokenInAuraCached(
|
||||||
const rangePixels = aura.range * getPixelsPerSceneUnit();
|
source,
|
||||||
return measureAuraDistance(source, target, aura) <= rangePixels;
|
target,
|
||||||
}
|
aura,
|
||||||
|
sourceData,
|
||||||
|
targetData,
|
||||||
|
dz2,
|
||||||
|
rangePixels2,
|
||||||
|
) {
|
||||||
|
// Broad phase: Bounding box check
|
||||||
|
const sourceBounds = sourceData.bounds;
|
||||||
|
const targetBounds = targetData.bounds;
|
||||||
|
|
||||||
function measureCenterDistance(source, target) {
|
const dxBox = Math.max(
|
||||||
const dx = source.center.x - target.center.x;
|
|
||||||
const dy = source.center.y - target.center.y;
|
|
||||||
const dz = getElevationOffset(source, target);
|
|
||||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function measureAuraDistance(source, target, aura) {
|
|
||||||
const sourcePoints = getMeasurementPoints(source, aura.from);
|
|
||||||
const targetPoints = getMeasurementPoints(target, aura.to);
|
|
||||||
if (sourcePoints.length === 0 || targetPoints.length === 0)
|
|
||||||
return measureBoundsDistance(source, target, aura);
|
|
||||||
|
|
||||||
return measurePointSetDistance(sourcePoints, targetPoints, source, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
function measureBoundsDistance(source, target, aura) {
|
|
||||||
const dz = getElevationOffset(source, target);
|
|
||||||
let d2d = 0;
|
|
||||||
if (aura.from === "center" && aura.to === "center")
|
|
||||||
return measureCenterDistance(source, target);
|
|
||||||
if (aura.from === "center")
|
|
||||||
d2d = measurePointToBoundsDistance(source.center, target.bounds);
|
|
||||||
else if (aura.to === "center")
|
|
||||||
d2d = measurePointToBoundsDistance(target.center, source.bounds);
|
|
||||||
else d2d = measureBoundsEdgeDistance(source, target);
|
|
||||||
|
|
||||||
return Math.sqrt(d2d * d2d + dz * dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function measureBoundsEdgeDistance(source, target) {
|
|
||||||
const alphaDistance = measureAlphaEdgeDistance(source, target);
|
|
||||||
if (Number.isFinite(alphaDistance)) return alphaDistance;
|
|
||||||
|
|
||||||
const sourceBounds = source.bounds;
|
|
||||||
const targetBounds = target.bounds;
|
|
||||||
const dx = Math.max(
|
|
||||||
sourceBounds.x - targetBounds.right,
|
sourceBounds.x - targetBounds.right,
|
||||||
targetBounds.x - sourceBounds.right,
|
targetBounds.x - sourceBounds.right,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const dy = Math.max(
|
const dyBox = Math.max(
|
||||||
sourceBounds.y - targetBounds.bottom,
|
sourceBounds.y - targetBounds.bottom,
|
||||||
targetBounds.y - sourceBounds.bottom,
|
targetBounds.y - sourceBounds.bottom,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
return Math.hypot(dx, dy);
|
|
||||||
|
if (dxBox * dxBox + dyBox * dyBox + dz2 > rangePixels2) return false;
|
||||||
|
|
||||||
|
// Narrow phase: Distance calculation
|
||||||
|
const d2d2 = measureAuraDistanceSquared(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
aura,
|
||||||
|
sourceData,
|
||||||
|
targetData,
|
||||||
|
);
|
||||||
|
if (d2d2 + dz2 > rangePixels2) return false;
|
||||||
|
|
||||||
|
// Wall restriction check
|
||||||
|
if (aura.walls) {
|
||||||
|
const isBlocked = CONFIG.Canvas.polygonBackends.sight.testCollision(
|
||||||
|
sourceData.center,
|
||||||
|
targetData.center,
|
||||||
|
{ mode: "any", type: "sight" },
|
||||||
|
);
|
||||||
|
if (isBlocked) return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function measurePointToBoundsDistance(point, bounds) {
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance from center to center (squared)
|
||||||
|
function measureCenterDistance2(sourceData, targetData) {
|
||||||
|
const dx = sourceData.center.x - targetData.center.x;
|
||||||
|
const dy = sourceData.center.y - targetData.center.y;
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main distance entry point (squared)
|
||||||
|
function measureAuraDistanceSquared(
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
aura,
|
||||||
|
sourceData,
|
||||||
|
targetData,
|
||||||
|
) {
|
||||||
|
if (aura.from === "center" && aura.to === "center") {
|
||||||
|
return measureCenterDistance2(sourceData, targetData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourcePoints = getMeasurementPointsCached(
|
||||||
|
source,
|
||||||
|
aura.from,
|
||||||
|
sourceData,
|
||||||
|
);
|
||||||
|
const targetPoints = getMeasurementPointsCached(target, aura.to, targetData);
|
||||||
|
|
||||||
|
if (sourcePoints.length === 0 || targetPoints.length === 0) {
|
||||||
|
return measureBoundsDistance2(sourceData, targetData, aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
return measurePointSetDistance2(sourcePoints, targetPoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback distance calculation using token bounds (squared)
|
||||||
|
function measureBoundsDistance2(sourceData, targetData, aura) {
|
||||||
|
if (aura.from === "center")
|
||||||
|
return measurePointToBoundsDistance2(sourceData.center, targetData.bounds);
|
||||||
|
if (aura.to === "center")
|
||||||
|
return measurePointToBoundsDistance2(targetData.center, sourceData.bounds);
|
||||||
|
|
||||||
|
const dx = Math.max(
|
||||||
|
sourceData.bounds.x - targetData.bounds.right,
|
||||||
|
targetData.bounds.x - sourceData.bounds.right,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const dy = Math.max(
|
||||||
|
sourceData.bounds.y - targetData.bounds.bottom,
|
||||||
|
targetData.bounds.y - sourceData.bounds.bottom,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function measurePointToBoundsDistance2(point, bounds) {
|
||||||
const dx = Math.max(bounds.x - point.x, point.x - bounds.right, 0);
|
const dx = Math.max(bounds.x - point.x, point.x - bounds.right, 0);
|
||||||
const dy = Math.max(bounds.y - point.y, point.y - bounds.bottom, 0);
|
const dy = Math.max(bounds.y - point.y, point.y - bounds.bottom, 0);
|
||||||
return Math.hypot(dx, dy);
|
return dx * dx + dy * dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
function measureAlphaEdgeDistance(source, target) {
|
function getMeasurementPointsCached(token, mode, tokenData) {
|
||||||
const sourcePoints = getTokenAlphaEdgePoints(source);
|
if (mode === "center") return [tokenData.center];
|
||||||
const targetPoints = getTokenAlphaEdgePoints(target);
|
|
||||||
if (sourcePoints.length === 0 || targetPoints.length === 0) return Number.NaN;
|
|
||||||
|
|
||||||
return measurePointSetDistance(sourcePoints, targetPoints, source, target);
|
if (tokenData.alphaPoints === null) {
|
||||||
|
tokenData.alphaPoints = getTokenAlphaEdgePoints(token);
|
||||||
|
}
|
||||||
|
return tokenData.alphaPoints;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMeasurementPoints(token, mode) {
|
// Find the minimum distance squared between two sets of points
|
||||||
if (mode === "center") return [token.center];
|
function measurePointSetDistance2(sourcePoints, targetPoints) {
|
||||||
|
|
||||||
const alphaPoints = getTokenAlphaEdgePoints(token);
|
|
||||||
if (alphaPoints.length > 0) return alphaPoints;
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function measurePointSetDistance(sourcePoints, targetPoints, source, target) {
|
|
||||||
let minDistanceSquared = Infinity;
|
let minDistanceSquared = Infinity;
|
||||||
for (const sourcePoint of sourcePoints) {
|
for (let i = 0, lenS = sourcePoints.length; i < lenS; i++) {
|
||||||
for (const targetPoint of targetPoints) {
|
const sP = sourcePoints[i];
|
||||||
const dx = sourcePoint.x - targetPoint.x;
|
for (let j = 0, lenT = targetPoints.length; j < lenT; j++) {
|
||||||
const dy = sourcePoint.y - targetPoint.y;
|
const tP = targetPoints[j];
|
||||||
const distanceSquared = dx * dx + dy * dy;
|
const dx = sP.x - tP.x;
|
||||||
if (distanceSquared < minDistanceSquared)
|
const dy = sP.y - tP.y;
|
||||||
minDistanceSquared = distanceSquared;
|
const d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 < minDistanceSquared) {
|
||||||
|
minDistanceSquared = d2;
|
||||||
|
// Optimization: Distance can't be less than 0
|
||||||
|
if (minDistanceSquared === 0) return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return minDistanceSquared;
|
||||||
|
}
|
||||||
|
|
||||||
const dz = getElevationOffset(source, target);
|
// Convert elevation difference to pixel distance
|
||||||
return Math.sqrt(minDistanceSquared + dz * dz);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getElevationOffset(source, target) {
|
function getElevationOffset(source, target) {
|
||||||
const e1 = source.document?.elevation ?? 0;
|
const e1 = source.document?.elevation ?? 0;
|
||||||
const e2 = target.document?.elevation ?? 0;
|
const e2 = target.document?.elevation ?? 0;
|
||||||
return Math.abs(e1 - e2) * getPixelsPerSceneUnit();
|
return Math.abs(e1 - e2) * getPixelsPerSceneUnit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alpha Sampling: Pixel-perfect edge detection
|
||||||
|
|
||||||
|
// Get points along the visual edge of the token's texture
|
||||||
function getTokenAlphaEdgePoints(token) {
|
function getTokenAlphaEdgePoints(token) {
|
||||||
const displayObject = getTokenFilterTarget(token);
|
const displayObject = getTokenFilterTarget(token);
|
||||||
const normalizedPoints = getTextureAlphaEdgePoints(displayObject);
|
const normalizedPoints = getTextureAlphaEdgePoints(displayObject);
|
||||||
@@ -556,6 +702,7 @@ function getTokenAlphaEdgePoints(token) {
|
|||||||
return normalizedPoints.map(mapper);
|
return normalizedPoints.map(mapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normalize texture-space points to canvas-space coordinates
|
||||||
function getAlphaPointMapper(displayObject, token) {
|
function getAlphaPointMapper(displayObject, token) {
|
||||||
const localBounds = displayObject?.getLocalBounds?.();
|
const localBounds = displayObject?.getLocalBounds?.();
|
||||||
const transform = displayObject?.worldTransform;
|
const transform = displayObject?.worldTransform;
|
||||||
@@ -567,6 +714,7 @@ function getAlphaPointMapper(displayObject, token) {
|
|||||||
x: localBounds.x + point.x * localBounds.width,
|
x: localBounds.x + point.x * localBounds.width,
|
||||||
y: localBounds.y + point.y * localBounds.height,
|
y: localBounds.y + point.y * localBounds.height,
|
||||||
};
|
};
|
||||||
|
// Map from local mesh space to world (screen) space, then back to canvas space
|
||||||
const worldPoint = transform.apply(localPoint);
|
const worldPoint = transform.apply(localPoint);
|
||||||
return stageTransform.applyInverse(worldPoint);
|
return stageTransform.applyInverse(worldPoint);
|
||||||
};
|
};
|
||||||
@@ -579,6 +727,7 @@ function getAlphaPointMapper(displayObject, token) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract and cache points representing the non-transparent outline of a texture
|
||||||
function getTextureAlphaEdgePoints(displayObject) {
|
function getTextureAlphaEdgePoints(displayObject) {
|
||||||
const texture = displayObject?.texture;
|
const texture = displayObject?.texture;
|
||||||
const source = getTextureSource(texture);
|
const source = getTextureSource(texture);
|
||||||
@@ -649,6 +798,7 @@ function getAlphaShapeCacheKey(texture, frame) {
|
|||||||
return `${textureId}:${frame.x}:${frame.y}:${frame.width}:${frame.height}:${ALPHA_SAMPLE_SIZE}:${HIGHLIGHT_ALPHA_THRESHOLD}`;
|
return `${textureId}:${frame.x}:${frame.y}:${frame.width}:${frame.height}:${ALPHA_SAMPLE_SIZE}:${HIGHLIGHT_ALPHA_THRESHOLD}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw the texture to a small canvas to scan its alpha channel
|
||||||
function sampleTextureAlphaEdges(source, frame) {
|
function sampleTextureAlphaEdges(source, frame) {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = ALPHA_SAMPLE_SIZE;
|
canvas.width = ALPHA_SAMPLE_SIZE;
|
||||||
@@ -682,6 +832,7 @@ function sampleTextureAlphaEdges(source, frame) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scan image data for pixels that transition from opaque to transparent
|
||||||
function getAlphaEdgeSamplePoints(imageData) {
|
function getAlphaEdgeSamplePoints(imageData) {
|
||||||
const alphaThreshold = HIGHLIGHT_ALPHA_THRESHOLD * 255;
|
const alphaThreshold = HIGHLIGHT_ALPHA_THRESHOLD * 255;
|
||||||
const { data, width, height } = imageData;
|
const { data, width, height } = imageData;
|
||||||
@@ -735,6 +886,8 @@ function isAlphaEdgePixel(opaquePixels, width, height, x, y) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter Management: Drawing the highlights
|
||||||
|
|
||||||
function highlightToken(token, highlights) {
|
function highlightToken(token, highlights) {
|
||||||
const target = getTokenFilterTarget(token);
|
const target = getTokenFilterTarget(token);
|
||||||
const filters = highlights
|
const filters = highlights
|
||||||
@@ -777,11 +930,12 @@ function removeHighlightFilters(target) {
|
|||||||
target.filters = remainingFilters.length > 0 ? remainingFilters : null;
|
target.filters = remainingFilters.length > 0 ? remainingFilters : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine which outline filter to use based on available system/module filters
|
||||||
function createOutlineFilter(color) {
|
function createOutlineFilter(color) {
|
||||||
const colorNumber = colorToNumber(color);
|
const colorNumber = colorToNumber(color);
|
||||||
const pixiOutlineFilter = globalThis.PIXI?.filters?.OutlineFilter;
|
const pixiOutlineFilter = globalThis.PIXI?.filters?.OutlineFilter;
|
||||||
if (pixiOutlineFilter)
|
if (pixiOutlineFilter)
|
||||||
return new pixiOutlineFilter(HIGHLIGHT_LINE_WIDTH, colorNumber, 0.2);
|
return new pixiOutlineFilter(getHighlightThickness(), colorNumber, 0.2);
|
||||||
|
|
||||||
const outlineOverlayFilter =
|
const outlineOverlayFilter =
|
||||||
globalThis.OutlineOverlayFilter ??
|
globalThis.OutlineOverlayFilter ??
|
||||||
@@ -789,7 +943,7 @@ function createOutlineFilter(color) {
|
|||||||
if (outlineOverlayFilter?.create) {
|
if (outlineOverlayFilter?.create) {
|
||||||
const filter = outlineOverlayFilter.create({
|
const filter = outlineOverlayFilter.create({
|
||||||
outlineColor: colorToRgbArray(colorNumber),
|
outlineColor: colorToRgbArray(colorNumber),
|
||||||
thickness: [HIGHLIGHT_LINE_WIDTH, HIGHLIGHT_LINE_WIDTH],
|
thickness: [getHighlightThickness(), getHighlightThickness()],
|
||||||
alphaThreshold: HIGHLIGHT_ALPHA_THRESHOLD,
|
alphaThreshold: HIGHLIGHT_ALPHA_THRESHOLD,
|
||||||
knockout: false,
|
knockout: false,
|
||||||
wave: false,
|
wave: false,
|
||||||
@@ -808,7 +962,7 @@ function createOutlineFilter(color) {
|
|||||||
if (glowOverlayFilter?.create) {
|
if (glowOverlayFilter?.create) {
|
||||||
const filter = glowOverlayFilter.create({
|
const filter = glowOverlayFilter.create({
|
||||||
glowColor: colorToRgbArray(colorNumber),
|
glowColor: colorToRgbArray(colorNumber),
|
||||||
distance: HIGHLIGHT_LINE_WIDTH,
|
distance: getHighlightThickness(),
|
||||||
outerStrength: 3,
|
outerStrength: 3,
|
||||||
innerStrength: 0,
|
innerStrength: 0,
|
||||||
alpha: 1,
|
alpha: 1,
|
||||||
@@ -824,7 +978,7 @@ function createOutlineFilter(color) {
|
|||||||
function createAlphaOutlineFilter(color) {
|
function createAlphaOutlineFilter(color) {
|
||||||
const uniforms = {
|
const uniforms = {
|
||||||
outlineColor: colorToRgbArray(color),
|
outlineColor: colorToRgbArray(color),
|
||||||
thickness: HIGHLIGHT_LINE_WIDTH,
|
thickness: getHighlightThickness(),
|
||||||
alphaThreshold: HIGHLIGHT_ALPHA_THRESHOLD,
|
alphaThreshold: HIGHLIGHT_ALPHA_THRESHOLD,
|
||||||
};
|
};
|
||||||
const baseFilter =
|
const baseFilter =
|
||||||
@@ -838,19 +992,20 @@ function createAlphaOutlineFilter(color) {
|
|||||||
ALPHA_OUTLINE_FRAGMENT_SHADER,
|
ALPHA_OUTLINE_FRAGMENT_SHADER,
|
||||||
uniforms,
|
uniforms,
|
||||||
);
|
);
|
||||||
filter.padding = HIGHLIGHT_LINE_WIDTH;
|
filter.padding = getHighlightThickness();
|
||||||
return filter;
|
return filter;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback graphics when filters are not supported
|
||||||
function drawFallbackTokenHighlights(container, token, highlights) {
|
function drawFallbackTokenHighlights(container, token, highlights) {
|
||||||
highlights.forEach((aura, index) => {
|
highlights.forEach((aura, index) => {
|
||||||
const bounds = token.bounds.clone().pad(index * HIGHLIGHT_RING_SPACING);
|
const bounds = token.bounds.clone().pad(index * HIGHLIGHT_RING_SPACING);
|
||||||
const graphic = new PIXI.Graphics();
|
const graphic = new PIXI.Graphics();
|
||||||
|
|
||||||
graphic.lineStyle(HIGHLIGHT_LINE_WIDTH, colorToNumber(aura.color), 0.9);
|
graphic.lineStyle(getHighlightThickness(), colorToNumber(aura.color), 0.9);
|
||||||
graphic.drawRoundedRect(
|
graphic.drawRoundedRect(
|
||||||
bounds.x,
|
bounds.x,
|
||||||
bounds.y,
|
bounds.y,
|
||||||
@@ -862,6 +1017,8 @@ function drawFallbackTokenHighlights(container, token, highlights) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
|
||||||
function getPixelsPerSceneUnit() {
|
function getPixelsPerSceneUnit() {
|
||||||
const activeCanvas = globalThis.canvas;
|
const activeCanvas = globalThis.canvas;
|
||||||
const gridSize =
|
const gridSize =
|
||||||
@@ -904,6 +1061,10 @@ function activateAurasTab(root, group) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHighlightThickness() {
|
||||||
|
return game.settings.get(MODULE_ID, THICKNESS_SETTING) ?? 4;
|
||||||
|
}
|
||||||
|
|
||||||
function localize(path) {
|
function localize(path) {
|
||||||
return game.i18n.localize(`BASIC_AURA_HIGHLIGHTING.${path}`);
|
return game.i18n.localize(`BASIC_AURA_HIGHLIGHTING.${path}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user