diff --git a/src/client/render/gl/Renderer.ts b/src/client/render/gl/Renderer.ts index 16b90a3a4..e82ae1126 100644 --- a/src/client/render/gl/Renderer.ts +++ b/src/client/render/gl/Renderer.ts @@ -350,8 +350,8 @@ export class GPURenderer { // just the affected tiles instead of rebuilding the whole map. A tile // changing owner can also flip its defense-coverage flag (same-owner test), // so mark the coverage stale too — one coalesced re-stamp happens per frame. - this.territoryPass.setBorderPatchConsumer((x, y) => { - this.borderPass.patchTile(x, y); + this.territoryPass.setBorderPatchConsumer((x, y, prevOwner, newOwner) => { + this.borderPass.patchTile(x, y, prevOwner, newOwner); this.defenseCoveragePass.markTileDirty(x, y); }); // Territory fill darkens on interior tiles defended by a same-owner post; diff --git a/src/client/render/gl/passes/BorderComputePass.ts b/src/client/render/gl/passes/BorderComputePass.ts index 414eb7ddf..c9ba76f37 100644 --- a/src/client/render/gl/passes/BorderComputePass.ts +++ b/src/client/render/gl/passes/BorderComputePass.ts @@ -187,13 +187,16 @@ export class BorderComputePass { } /** - * Notify that one tile changed owner. Schedules incremental border recompute - * for that tile + its 4 cardinal neighbors. Cheap: ~5 points per call. - * Caller is responsible for ensuring tileTex contains the new state before - * the next draw — TerritoryPass.flushTileTexture takes care of that. + * Notify that one tile changed owner (from `prevOwner` to `newOwner`). + * Schedules incremental border recompute for that tile + its 4 cardinal + * neighbors — or, when the change touches the highlighted owner, a wider box + * so the highlight band's inner edge keeps up. Cheap: ~5 points per call in + * the common case. Caller is responsible for ensuring tileTex contains the + * new state before the next draw — TerritoryPass.flushTileTexture takes care + * of that. */ - patchTile(x: number, y: number): void { - this.scatter.pushWithNeighbors(x, y); + patchTile(x: number, y: number, prevOwner: number, newOwner: number): void { + this.scatter.pushWithNeighbors(x, y, prevOwner, newOwner); } /** The border buffer texture (RG8, tile resolution). */ diff --git a/src/client/render/gl/passes/BorderScatterPass.ts b/src/client/render/gl/passes/BorderScatterPass.ts index 65851e125..1f5638c9c 100644 --- a/src/client/render/gl/passes/BorderScatterPass.ts +++ b/src/client/render/gl/passes/BorderScatterPass.ts @@ -13,10 +13,11 @@ * border shader makes the neighbors' results depend on this tile's ownership. * Use `pushWithNeighbors` to do that expansion automatically. * - * Highlight-thicken rings within `uHighlightThicken` of a changed tile are - * NOT incrementally repainted — they'll lag visually until the next full - * recompute (which fires on highlight / relation / defense changes). That - * artifact is small and short-lived; for live combat it's a fair trade. + * When a tile is gained or lost by the highlighted owner, it also affects the + * highlight thickening of nearby highlight-owner tiles (an N-tile Chebyshev + * expansion), so `pushWithNeighbors` widens the repaint to that radius for + * those tiles only — otherwise the inner edge of the highlight band would lag + * until the next full recompute. */ import type { RenderSettings } from "../RenderSettings"; @@ -122,13 +123,52 @@ export class BorderScatterPass { this.patchCount++; } - /** Queue the tile + its 4 cardinal neighbors (clipped to map bounds). */ - pushWithNeighbors(x: number, y: number): void { - this.push(x, y); - if (x > 0) this.push(x - 1, y); - if (x < this.mapW - 1) this.push(x + 1, y); - if (y > 0) this.push(x, y - 1); - if (y < this.mapH - 1) this.push(x, y + 1); + /** + * Queue the tile + the neighborhood whose border value depends on it + * (clipped to map bounds). `prevOwner`/`newOwner` are the tile's owner before + * and after the change. + * + * Normal borders only need the 4 cardinal neighbors (the shader's border + * test is cardinal-only). But the highlight thickening is an N-tile Chebyshev + * expansion: a tile being gained or lost by the highlighted owner affects the + * thickening of every highlight-owner tile within `highlightThicken` of it. + * In that case — and only that case — repaint the whole box so the inner edge + * of the highlight band tracks the change instead of lagging until the next + * full recompute. Changes elsewhere on the map don't touch the band, so they + * keep the cheap cardinal cross. + */ + pushWithNeighbors( + x: number, + y: number, + prevOwner: number, + newOwner: number, + ): void { + const touchesHighlight = + this.highlightOwner !== 0 && + (prevOwner === this.highlightOwner || newOwner === this.highlightOwner); + + if (!touchesHighlight) { + this.push(x, y); + if (x > 0) this.push(x - 1, y); + if (x < this.mapW - 1) this.push(x + 1, y); + if (y > 0) this.push(x, y - 1); + if (y < this.mapH - 1) this.push(x, y + 1); + return; + } + + const r = Math.max( + 1, + Math.floor(this.settings.mapOverlay.highlightThicken), + ); + const x0 = Math.max(0, x - r); + const x1 = Math.min(this.mapW - 1, x + r); + const y0 = Math.max(0, y - r); + const y1 = Math.min(this.mapH - 1, y + r); + for (let yy = y0; yy <= y1; yy++) { + for (let xx = x0; xx <= x1; xx++) { + this.push(xx, yy); + } + } } get count(): number { diff --git a/src/client/render/gl/passes/TerritoryPass.ts b/src/client/render/gl/passes/TerritoryPass.ts index 8ad77a794..5b3547264 100644 --- a/src/client/render/gl/passes/TerritoryPass.ts +++ b/src/client/render/gl/passes/TerritoryPass.ts @@ -16,7 +16,7 @@ import type { TilePair } from "../../types"; import type { RenderSettings } from "../RenderSettings"; import { getPaletteSize } from "../utils/ColorUtils"; import { createMapQuad, createProgram, shaderSrc } from "../utils/GlUtils"; -import { FALLOUT_BIT, TILE_DEFINES } from "../utils/TileCodec"; +import { FALLOUT_BIT, OWNER_MASK, TILE_DEFINES } from "../utils/TileCodec"; import overlayVertSrc from "../shaders/map-overlay/overlay.vert.glsl?raw"; import territoryFragSrc from "../shaders/map-overlay/territory.frag.glsl?raw"; @@ -89,7 +89,9 @@ export class TerritoryPass { * incrementally repaint affected tiles instead of rebuilding the whole map. * Wired by the renderer to `borderPass.patchTile`. */ - private borderPatchConsumer: ((x: number, y: number) => void) | null = null; + private borderPatchConsumer: + | ((x: number, y: number, prevOwner: number, newOwner: number) => void) + | null = null; /** * Drip buckets — round-robin staggering of tile updates across render frames. @@ -216,7 +218,9 @@ export class TerritoryPass { * hooks this to `borderPass.patchTile` so border recompute scales with the * number of changed tiles instead of full map area. */ - setBorderPatchConsumer(fn: (x: number, y: number) => void): void { + setBorderPatchConsumer( + fn: (x: number, y: number, prevOwner: number, newOwner: number) => void, + ): void { this.borderPatchConsumer = fn; } @@ -246,7 +250,8 @@ export class TerritoryPass { for (let i = 0; i < bucket.length; i += 2) { const ref = bucket[i]; const state = bucket[i + 1]; - if (((ts[ref] ^ state) & FALLOUT_BIT) !== 0) { + const prev = ts[ref]; + if (((prev ^ state) & FALLOUT_BIT) !== 0) { this.falloutTouched = true; } ts[ref] = state; @@ -254,7 +259,9 @@ export class TerritoryPass { const x = ref % w; const y = (ref - x) / w; this.scatter.push(x, y, state); - if (borderFn) borderFn(x, y); + if (borderFn) { + borderFn(x, y, prev & OWNER_MASK, state & OWNER_MASK); + } } } bucket.length = 0; @@ -280,7 +287,8 @@ export class TerritoryPass { for (let i = 0; i < bucket.length; i += 2) { const ref = bucket[i]; const state = bucket[i + 1]; - if (((ts[ref] ^ state) & FALLOUT_BIT) !== 0) { + const prev = ts[ref]; + if (((prev ^ state) & FALLOUT_BIT) !== 0) { this.falloutTouched = true; } ts[ref] = state; @@ -288,7 +296,9 @@ export class TerritoryPass { const x = ref % w; const y = (ref - x) / w; this.scatter.push(x, y, state); - if (borderFn) borderFn(x, y); + if (borderFn) { + borderFn(x, y, prev & OWNER_MASK, state & OWNER_MASK); + } } } bucket.length = 0;