mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface RenderSettings {
|
||||
passEnabled: {
|
||||
terrain: boolean;
|
||||
mapOverlay: boolean;
|
||||
territoryPatterns: boolean;
|
||||
structure: boolean;
|
||||
unit: boolean;
|
||||
name: boolean;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user