From 28f901de2cfd140e2887cf6385170db83fbb1efc Mon Sep 17 00:00:00 2001 From: In3luki <41452412+In3luki@users.noreply.github.com> Date: Tue, 12 Nov 2024 12:32:42 +0100 Subject: [PATCH] Improve aura rendering for tokens with multiple auras --- src/module/canvas/token/aura/map.ts | 39 +++++++++++++++- src/module/canvas/token/aura/renderer.ts | 12 +++++ src/module/canvas/token/aura/util.ts | 58 ++++++++++++++---------- src/module/canvas/token/object.ts | 2 +- 4 files changed, 84 insertions(+), 27 deletions(-) diff --git a/src/module/canvas/token/aura/map.ts b/src/module/canvas/token/aura/map.ts index c3162dad32e..f56b14d1a6c 100644 --- a/src/module/canvas/token/aura/map.ts +++ b/src/module/canvas/token/aura/map.ts @@ -1,5 +1,7 @@ +import * as R from "remeda"; import { TokenPF2e } from "../object.ts"; import { AuraRenderer } from "./renderer.ts"; +import { createTestPolygons } from "./util.ts"; export class AuraRenderers extends Map { readonly token: TokenPF2e; @@ -77,17 +79,48 @@ export class AuraRenderers extends Map { } if (showBordersHighlights && (this.token.hover || this.token.layer.highlightObjects)) { - const { highlightId } = this; + const highlightId = this.highlightId; const highlight = canvas.interface.grid.highlightLayers[highlightId] ?? canvas.interface.grid.addHighlightLayer(highlightId); highlight.clear(); - for (const aura of this.values()) { + // Reuse the largest aura's polygons for multiple auras + if (this.size > 1) { + const testPolygons = this.#createGroupedPolygons(); + for (const aura of this.values()) { + const polygons = testPolygons[aura.restrictionType]; + if (polygons.length > 0) { + aura.polygons = polygons; + } + } + } + // Always draw highlight from largest to smallest radius + for (const aura of [...this.values()].sort((a, b) => a.radius - b.radius)) { aura.highlight(); } } } + /** Create test polygons for the largest aura grouped by wall restriction type */ + #createGroupedPolygons(): TestPolygons { + const polygons: TestPolygons = { + move: [], + sight: [], + sound: [], + }; + const grouped = R.groupBy( + [...this.values()].sort((a, b) => a.radius - b.radius), + (r) => r.restrictionType, + ); + for (const [type, renderers] of R.entries(grouped)) { + if (polygons[type].length > 0) continue; + const renderer = renderers.at(0); + if (!renderer) continue; + polygons[type] = createTestPolygons(renderer); + } + return polygons; + } + /** Deallocate the aura's GPU memory before removing from map */ override delete(key: string): boolean { const aura = this.get(key); @@ -118,3 +151,5 @@ export class AuraRenderers extends Map { canvas.interface.grid.destroyHighlightLayer(this.highlightId); } } + +type TestPolygons = Record; diff --git a/src/module/canvas/token/aura/renderer.ts b/src/module/canvas/token/aura/renderer.ts index 0c997ad6c5f..db1823f1d6d 100644 --- a/src/module/canvas/token/aura/renderer.ts +++ b/src/module/canvas/token/aura/renderer.ts @@ -32,6 +32,12 @@ class AuraRenderer extends PIXI.Graphics implements TokenAuraData { textureContainer: PIXI.Graphics | null = null; + /** An optional set of predefined polygons used to test square inclusion */ + polygons: ClockwiseSweepPolygon[] | null = null; + + /** The wall restriction type used for this aura */ + restrictionType: Exclude; + constructor(params: AuraRendererParams) { super(); @@ -42,6 +48,12 @@ class AuraRenderer extends PIXI.Graphics implements TokenAuraData { this.radiusPixels = 0.5 * this.token.mechanicalBounds.width + (this.radius / canvas.dimensions.distance) * canvas.grid.size; this.traits = params.traits; + this.restrictionType = + this.traits.includes("visual") && !this.traits.includes("auditory") + ? "sight" + : this.traits.includes("auditory") && !this.traits.includes("visual") + ? "sound" + : "move"; this.addChild(this.border); } diff --git a/src/module/canvas/token/aura/util.ts b/src/module/canvas/token/aura/util.ts index fb442a92dc8..b59ff2c2d61 100644 --- a/src/module/canvas/token/aura/util.ts +++ b/src/module/canvas/token/aura/util.ts @@ -3,8 +3,9 @@ import { measureDistanceCuboid } from "@module/canvas/helpers.ts"; import { TokenDocumentPF2e } from "@scene"; import type BaseEffectSource from "types/foundry/client-esm/canvas/sources/base-effect-source.d.ts"; import type { TokenPF2e } from "../index.ts"; +import type { AuraRenderer } from "./renderer.ts"; -export function getAreaSquares(data: GetAreaSquaresParams): EffectAreaSquare[] { +function getAreaSquares(data: GetAreaSquaresParams): EffectAreaSquare[] { if (!canvas.ready) return []; const squareWidth = canvas.dimensions.size; const rowCount = Math.ceil(data.bounds.width / squareWidth); @@ -28,13 +29,37 @@ export function getAreaSquares(data: GetAreaSquaresParams): EffectAreaSquare[] { }; const topLeftSquare = new EffectAreaSquare(data.bounds.x, data.bounds.y, squareWidth, squareWidth); + const tokenBounds = data.token.mechanicalBounds; + const testPolygons = data.polygons ?? createTestPolygons(data); + + return emptyVector + .reduce( + (squares: EffectAreaSquare[][]) => { + const lastSquare = squares.at(-1)?.at(-1) ?? { x: NaN, y: NaN }; + const column = genColumn( + new EffectAreaSquare(lastSquare.x + squareWidth, topLeftSquare.y, squareWidth, squareWidth), + ); + squares.push(column); + return squares; + }, + [genColumn(topLeftSquare)], + ) + .flat() + .filter((s) => measureDistanceCuboid(tokenBounds, s) <= data.radius) + .map((square) => { + square.active = testPolygons.some((c) => c.contains(square.center.x, square.center.y)); + return square; + }); +} + +function createTestPolygons(data: GetAreaSquaresParams): ClockwiseSweepPolygon[] { const collisionType = - data.traits?.includes("visual") && !data.traits.includes("auditory") + data.restrictionType ?? + (data.traits?.includes("visual") && !data.traits.includes("auditory") ? "sight" : data.traits?.includes("auditory") && !data.traits.includes("visual") ? "sound" - : "move"; - + : "move"); const tokenBounds = data.token.mechanicalBounds; const tokenCenter = data.token.center; const tokenCenters = [ @@ -61,37 +86,22 @@ export function getAreaSquares(data: GetAreaSquaresParams): EffectAreaSquare[] { return new PointSource({ object: tokenObject }); })(); - const tokenCenterPolygons = tokenCenters.map((c) => + return tokenCenters.map((c) => CONFIG.Canvas.polygonBackends[collisionType].create(c, { type: collisionType, source: pointSource, boundaryShape: [data.bounds], }), ); - - return emptyVector - .reduce( - (squares: EffectAreaSquare[][]) => { - const lastSquare = squares.at(-1)?.at(-1) ?? { x: NaN, y: NaN }; - const column = genColumn( - new EffectAreaSquare(lastSquare.x + squareWidth, topLeftSquare.y, squareWidth, squareWidth), - ); - squares.push(column); - return squares; - }, - [genColumn(topLeftSquare)], - ) - .flat() - .filter((s) => measureDistanceCuboid(tokenBounds, s) <= data.radius) - .map((square) => { - square.active = tokenCenterPolygons.some((c) => c.contains(square.center.x, square.center.y)); - return square; - }); } interface GetAreaSquaresParams { bounds: PIXI.Rectangle; + polygons?: ClockwiseSweepPolygon[] | null; + restrictionType?: AuraRenderer["restrictionType"]; radius: number; token: TokenPF2e | TokenDocumentPF2e; traits?: string[]; } + +export { createTestPolygons, getAreaSquares }; diff --git a/src/module/canvas/token/object.ts b/src/module/canvas/token/object.ts index 5de22e25056..423093d3ba8 100644 --- a/src/module/canvas/token/object.ts +++ b/src/module/canvas/token/object.ts @@ -637,7 +637,7 @@ class TokenPF2e extends ): void { super._onUpdate(changed, operation, userId); - if (changed.width) { + if (changed.width || changed.flags?.pf2e?.linkToActorSize !== undefined) { if (this.animation) { this.animation.then(() => { this.auras.reset();