Display territory skins again (#3966)

## Description:

Display territory skins (patterns) again.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

tryout33
This commit is contained in:
VariableVince
2026-05-19 00:48:05 +02:00
committed by GitHub
parent 7863529b2c
commit ed928db081
8 changed files with 167 additions and 6 deletions
+6
View File
@@ -451,6 +451,12 @@ async function createClientGame(
// setAltView called to switch passes into alt mode.
eventBus.on(AlternateViewEvent, (e) => view.setAltView(e.alternateView));
view.setShowPatterns(userSettings.territoryPatterns());
globalThis.addEventListener(
`${USER_SETTINGS_CHANGED_EVENT}:settings.territoryPatterns`,
(e) => view.setShowPatterns((e as CustomEvent<string>).detail === "true"),
);
const gameRenderer = createRenderer(
inputOverlay,
gameView,
+32 -1
View File
@@ -1,4 +1,6 @@
import { Colord } from "colord";
import { base64url } from "jose";
import { decodePatternData } from "../core/PatternDecoder";
import { PlayerType } from "../core/game/Game";
import { GameView } from "../core/game/GameView";
import { uploadFrameData } from "./render/frame/Upload";
@@ -23,6 +25,9 @@ const PALETTE_SIZE = 4096;
*/
export class WebGLFrameBuilder {
private readonly palette: Float32Array;
private readonly patternMeta: Float32Array;
private readonly patternData: Uint8Array;
private readonly knownSmallIDs = new Set<number>();
// The renderer needs to know which player is "me" so affiliation tint,
// unit colors, and SAM-radius perspective work. Push it once the local
@@ -33,6 +38,8 @@ export class WebGLFrameBuilder {
constructor(private readonly view: WebGLGameView) {
this.palette = new Float32Array(PALETTE_SIZE * 2 * 4);
this.patternMeta = new Float32Array(PALETTE_SIZE * 4);
this.patternData = new Uint8Array(PALETTE_SIZE * 1024);
}
update(gameView: GameView): void {
@@ -116,6 +123,25 @@ export class WebGLFrameBuilder {
this.writePaletteEntry(smallID, p.territoryColor(), p.borderColor());
const pattern = p.cosmetics.pattern;
if (pattern && pattern.patternData) {
try {
const decoded = decodePatternData(
pattern.patternData,
base64url.decode,
);
const metaOff = smallID * 4;
this.patternMeta[metaOff] = 1.0; // hasPattern = true
this.patternMeta[metaOff + 1] = decoded.width;
this.patternMeta[metaOff + 2] = decoded.height;
this.patternMeta[metaOff + 3] = decoded.scale;
this.patternData.set(decoded.bytes.slice(3), smallID * 1024);
} catch (e) {
console.warn("Failed to decode territory pattern", e);
}
}
newPlayers.push({
...p.static,
flag: p.cosmetics.flag,
@@ -123,7 +149,12 @@ export class WebGLFrameBuilder {
});
}
if (newPlayers.length > 0) {
this.view.addPlayers(newPlayers, this.palette);
this.view.addPlayers(
newPlayers,
this.palette,
this.patternMeta,
this.patternData,
);
}
}
+10 -2
View File
@@ -217,8 +217,13 @@ export class GameView {
updatePalette(paletteData: Float32Array): void {
this.renderer.updatePalette(paletteData);
}
addPlayers(players: PlayerStatic[], paletteData: Float32Array): void {
this.renderer.addPlayers(players, paletteData);
addPlayers(
players: PlayerStatic[],
paletteData: Float32Array,
patternMeta: Float32Array,
patternData: Uint8Array,
): void {
this.renderer.addPlayers(players, paletteData, patternMeta, patternData);
}
uploadRailroadState(data: Uint8Array): void {
this.renderer.uploadRailroadState(data);
@@ -328,6 +333,9 @@ export class GameView {
setAltView(active: boolean): void {
this.renderer.setAltView(active);
}
setShowPatterns(active: boolean): void {
this.renderer.setShowPatterns(active);
}
setHighlightOwner(ownerID: number): void {
this.renderer.setHighlightOwner(ownerID);
}
+1
View File
@@ -4,6 +4,7 @@ export interface RenderSettings {
passEnabled: {
terrain: boolean;
mapOverlay: boolean;
territoryPatterns: boolean;
structure: boolean;
unit: boolean;
name: boolean;
+67 -2
View File
@@ -127,6 +127,8 @@ export class GPURenderer {
private paletteTex: WebGLTexture;
private paletteData: Float32Array;
private patternMetaTex: WebGLTexture;
private patternDataTex: WebGLTexture;
private canvas: HTMLCanvasElement;
private settings: RenderSettings;
private sceneTarget: RenderTarget;
@@ -219,6 +221,26 @@ export class GPURenderer {
filter: gl.NEAREST,
});
this.patternMetaTex = createTexture2D(gl, {
width: palW,
height: 1,
internalFormat: gl.RGBA32F,
format: gl.RGBA,
type: gl.FLOAT,
data: new Float32Array(palW * 4),
filter: gl.NEAREST,
});
this.patternDataTex = createTexture2D(gl, {
width: 1024,
height: palW,
internalFormat: gl.R8UI,
format: gl.RED_INTEGER,
type: gl.UNSIGNED_BYTE,
data: new Uint8Array(palW * 1024),
filter: gl.NEAREST,
});
// --- Border compute (creates its own borderTex) ---
// Need a temporary tileTex reference for border compute — we'll create
// GPUResources first, then wire everything.
@@ -259,7 +281,7 @@ export class GPURenderer {
this.settings,
);
// --- Territory (needs tileTex, trailTex, paletteTex) ---
// --- Territory (needs tileTex, trailTex, paletteTex, patternTexs) ---
this.territoryPass = new TerritoryPass(
gl,
mapW,
@@ -267,6 +289,8 @@ export class GPURenderer {
this.res.tileTex,
this.res.trailTex,
this.paletteTex,
this.patternMetaTex,
this.patternDataTex,
this.settings,
);
@@ -582,8 +606,43 @@ export class GPURenderer {
}
/** Register late-arriving players (updates palette + NamePass lookup maps). */
addPlayers(players: PlayerStatic[], paletteData: Float32Array): void {
addPlayers(
players: PlayerStatic[],
paletteData: Float32Array,
patternMeta: Float32Array,
patternData: Uint8Array,
): void {
this.updatePalette(paletteData);
const gl = this.gl;
const palW = getPaletteSize();
gl.bindTexture(gl.TEXTURE_2D, this.patternMetaTex);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0,
0,
palW,
1,
gl.RGBA,
gl.FLOAT,
patternMeta,
);
gl.bindTexture(gl.TEXTURE_2D, this.patternDataTex);
gl.texSubImage2D(
gl.TEXTURE_2D,
0,
0,
0,
1024,
palW,
gl.RED_INTEGER,
gl.UNSIGNED_BYTE,
patternData,
);
this.namePass.addPlayers(players, this.paletteData);
for (const p of players) {
if (p.team !== null) this.playerTeams.set(p.smallID, p.team);
@@ -838,6 +897,10 @@ export class GPURenderer {
this.trailPass.setAltView(active);
}
setShowPatterns(active: boolean): void {
this.territoryPass.setShowPatterns(active);
}
setGridView(active: boolean): void {
this.gridView = active;
}
@@ -1147,6 +1210,8 @@ export class GPURenderer {
this.barPass.dispose();
disposeGPUResources(this.gl, this.res);
this.gl.deleteTexture(this.paletteTex);
this.gl.deleteTexture(this.patternMetaTex);
this.gl.deleteTexture(this.patternDataTex);
this.gl.deleteFramebuffer(this.sceneTarget.fbo);
this.gl.deleteTexture(this.sceneTarget.tex);
this.lastUnits = new Map();
+24 -1
View File
@@ -36,14 +36,18 @@ export class TerritoryPass {
private uCharcoalAlpha: WebGLUniformLocation;
private uHighlightOwner: WebGLUniformLocation;
private uHighlightBrighten: WebGLUniformLocation;
private uShowPatterns: WebGLUniformLocation;
private highlightOwner = 0;
private vao: WebGLVertexArrayObject;
private tileTex: WebGLTexture;
private trailTex: WebGLTexture;
private paletteTex: WebGLTexture;
private patternMetaTex: WebGLTexture;
private patternDataTex: WebGLTexture;
private altView = false;
private showPatterns = true;
/** CPU-side tile state (deltas written here, flushed to GPU before draw). */
private cpuTileState: Uint16Array;
@@ -72,6 +76,8 @@ export class TerritoryPass {
tileTex: WebGLTexture,
trailTex: WebGLTexture,
paletteTex: WebGLTexture,
patternMetaTex: WebGLTexture,
patternDataTex: WebGLTexture,
settings: RenderSettings,
) {
this.gl = gl;
@@ -81,6 +87,8 @@ export class TerritoryPass {
this.tileTex = tileTex;
this.trailTex = trailTex;
this.paletteTex = paletteTex;
this.patternMetaTex = patternMetaTex;
this.patternDataTex = patternDataTex;
this.cpuTileState = new Uint16Array(mapW * mapH);
this.cpuTrailState = new Uint8Array(mapW * mapH);
@@ -112,10 +120,13 @@ export class TerritoryPass {
this.program,
"uHighlightBrighten",
)!;
this.uShowPatterns = gl.getUniformLocation(this.program, "uShowPatterns")!;
gl.useProgram(this.program);
gl.uniform1i(gl.getUniformLocation(this.program, "uTileTex"), 0);
gl.uniform1i(gl.getUniformLocation(this.program, "uPalette"), 1);
gl.uniform1i(gl.getUniformLocation(this.program, "uPatternMeta"), 2);
gl.uniform1i(gl.getUniformLocation(this.program, "uPatternData"), 3);
this.vao = createMapQuad(gl, mapW, mapH);
}
@@ -330,6 +341,10 @@ export class TerritoryPass {
this.altView = active;
}
setShowPatterns(show: boolean): void {
this.showPatterns = show;
}
/** Set the hovered player's smallID for territory-fill brightening (0 = off). */
setHighlightOwner(ownerID: number): void {
this.highlightOwner = ownerID;
@@ -352,11 +367,19 @@ export class TerritoryPass {
gl.uniform1f(this.uCharcoalAlpha, mo.charcoalAlpha);
gl.uniform1ui(this.uHighlightOwner, this.highlightOwner);
gl.uniform1f(this.uHighlightBrighten, mo.highlightFillBrighten);
gl.uniform1i(
this.uShowPatterns,
this.settings.passEnabled.territoryPatterns && this.showPatterns ? 1 : 0,
);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, this.tileTex);
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, this.paletteTex);
gl.activeTexture(gl.TEXTURE2);
gl.bindTexture(gl.TEXTURE_2D, this.patternMetaTex);
gl.activeTexture(gl.TEXTURE3);
gl.bindTexture(gl.TEXTURE_2D, this.patternDataTex);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLES, 0, 6);
@@ -366,6 +389,6 @@ export class TerritoryPass {
const gl = this.gl;
gl.deleteProgram(this.program);
gl.deleteVertexArray(this.vao);
// tileTex, trailTex, paletteTex owned by GPUResources / renderer
// tileTex, trailTex, paletteTex, patternMetaTex, patternDataTex owned by GPUResources / renderer
}
}
@@ -2,6 +2,7 @@
"passEnabled": {
"terrain": true,
"mapOverlay": true,
"territoryPatterns": true,
"structure": true,
"unit": true,
"name": true,
@@ -4,6 +4,9 @@ precision highp usampler2D;
uniform usampler2D uTileTex; // R16UI — tile state per cell
uniform sampler2D uPalette; // RGBA32F — player colors
uniform sampler2D uPatternMeta; // RGBA32F — 1D buffer, 1 px per owner. R=hasPattern, G=width, B=height, A=scale
uniform usampler2D uPatternData; // R8UI — 2D buffer, row per owner, bytes for bitmask
uniform int uShowPatterns;
uniform vec2 uMapSize;
uniform int uAltView;
@@ -42,6 +45,29 @@ void main() {
float u = (float(owner) + 0.5) / float(PALETTE_SIZE);
vec4 color = texture(uPalette, vec2(u, 0.25));
if (uShowPatterns == 1) {
vec4 meta = texelFetch(uPatternMeta, ivec2(int(owner), 0), 0);
if (meta.r > 0.0) {
int pWidth = int(meta.g);
int pHeight = int(meta.b);
int pScale = int(meta.a);
int px = tc.x >> pScale;
int py = tc.y >> pScale;
int mx = ((px % pWidth) + pWidth) % pWidth;
int my = ((py % pHeight) + pHeight) % pHeight;
int bitIndex = my * pWidth + mx;
int byteIndex = bitIndex >> 3;
uint patternByte = texelFetch(uPatternData, ivec2(byteIndex, int(owner)), 0).r;
bool isPrimary = (patternByte & (1u << uint(bitIndex & 7))) == 0u;
if (!isPrimary) {
color = texture(uPalette, vec2(u, 0.75));
}
}
}
// Hover highlight: brighten every tile owned by the hovered player.
if (uHighlightOwner != 0u && owner == uHighlightOwner) {
color.rgb = mix(color.rgb, vec3(1.0), uHighlightBrighten);