From 849e8a7ff3e076576f1d044cfc2b7d717e462526 Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Tue, 13 Jan 2026 06:03:03 +0100 Subject: [PATCH] Add contested drawing controls to TerritoryLayer and TerritoryWebGLRenderer - Introduced UI elements for enabling contested drawing and selecting contest patterns (blueNoise, checkerboard, bayer4x4) in TerritoryLayer. - Updated TerritoryWebGLRenderer to handle contest pattern modes and integrate them into the rendering logic. - Enhanced shader logic to support new contest pattern modes for improved visual representation of contested territories. - Added functionality to synchronize contest state and pattern mode between the UI and renderer. --- src/client/graphics/layers/TerritoryLayer.ts | 78 +++++++++++++++++++ .../graphics/layers/TerritoryWebGLRenderer.ts | 63 ++++++++++++++- 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index ba0a14f03..6450c225d 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -89,6 +89,8 @@ export class TerritoryLayer implements Layer { private tickIntervalEmaMs = 0; private readonly TICK_INTERVAL_EMA_ALPHA = 0.2; private smoothingDebugUi: HTMLDivElement | null = null; + private contestedPatternMode: "blueNoise" | "checkerboard" | "bayer4x4" = + "blueNoise"; constructor( private game: GameView, @@ -570,6 +572,80 @@ export class TerritoryLayer implements Layer { modeRow.appendChild(modeSelect); root.appendChild(modeRow); + // Contested drawing controls + const contestedRow = document.createElement("label"); + contestedRow.style.display = "flex"; + contestedRow.style.alignItems = "center"; + contestedRow.style.gap = "6px"; + contestedRow.style.marginBottom = "6px"; + + const contestedCheckbox = document.createElement("input"); + contestedCheckbox.type = "checkbox"; + contestedCheckbox.checked = this.contestEnabled; + contestedCheckbox.addEventListener("change", () => { + const enabled = contestedCheckbox.checked; + this.contestEnabled = enabled; + this.contestTileCount = 0; + this.contestActive = false; + if (enabled) { + this.ensureContestScratch(); + this.syncContestStateToRenderer(); + } else { + this.contestComponents.clear(); + } + this.territoryRenderer?.setContestEnabled(enabled); + this.territoryRenderer?.markAllDirty(); + }); + + const contestedText = document.createElement("span"); + contestedText.textContent = "contested draw"; + contestedRow.appendChild(contestedCheckbox); + contestedRow.appendChild(contestedText); + root.appendChild(contestedRow); + + const contestedModeRow = document.createElement("label"); + contestedModeRow.style.display = "flex"; + contestedModeRow.style.alignItems = "center"; + contestedModeRow.style.gap = "6px"; + contestedModeRow.style.marginBottom = "0px"; + + const contestedModeText = document.createElement("span"); + contestedModeText.textContent = "contested pattern:"; + + const contestedModeSelect = document.createElement("select"); + contestedModeSelect.style.font = "12px monospace"; + contestedModeSelect.style.background = "rgba(0,0,0,0.35)"; + contestedModeSelect.style.color = "rgba(255,255,255,0.92)"; + contestedModeSelect.style.border = "1px solid rgba(255,255,255,0.2)"; + contestedModeSelect.style.borderRadius = "4px"; + contestedModeSelect.style.padding = "2px 4px"; + + const contestedModes: Array<"blueNoise" | "checkerboard" | "bayer4x4"> = [ + "blueNoise", + "checkerboard", + "bayer4x4", + ]; + for (const m of contestedModes) { + const opt = document.createElement("option"); + opt.value = m; + opt.textContent = m; + contestedModeSelect.appendChild(opt); + } + contestedModeSelect.value = this.contestedPatternMode; + contestedModeSelect.addEventListener("change", () => { + const v = contestedModeSelect.value as + | "blueNoise" + | "checkerboard" + | "bayer4x4"; + this.contestedPatternMode = v; + this.territoryRenderer?.setContestPatternMode(v); + this.territoryRenderer?.markAllDirty(); + }); + + contestedModeRow.appendChild(contestedModeText); + contestedModeRow.appendChild(contestedModeSelect); + root.appendChild(contestedModeRow); + document.body.appendChild(root); this.smoothingDebugUi = root; } @@ -646,6 +722,7 @@ export class TerritoryLayer implements Layer { this.territoryRenderer = renderer; this.territoryRenderer.setContestEnabled(this.contestEnabled); + this.territoryRenderer.setContestPatternMode(this.contestedPatternMode); this.territoryRenderer.setAlternativeView(this.alternativeView); this.territoryRenderer.markAllDirty(); this.territoryRenderer.refreshPalette(); @@ -1372,6 +1449,7 @@ export class TerritoryLayer implements Layer { `smoothPrereq: prevCopy ${stats.prevStateCopySupported ? "yes" : "no"}`, `jfa: ${jfaStatus} dirty ${stats.jfaDirty ? "yes" : "no"}`, `contests: ${this.contestEnabled ? "on" : "off"} comps ${this.contestComponents.size}`, + `contestPattern: ${this.contestedPatternMode}`, `contestTiles: ${this.contestTileCount}`, `contestTicks: ${this.contestDurationTicks}`, `hovered: ${stats.hoveredPlayerId}`, diff --git a/src/client/graphics/layers/TerritoryWebGLRenderer.ts b/src/client/graphics/layers/TerritoryWebGLRenderer.ts index 307301f6e..efaa948f6 100644 --- a/src/client/graphics/layers/TerritoryWebGLRenderer.ts +++ b/src/client/graphics/layers/TerritoryWebGLRenderer.ts @@ -28,6 +28,7 @@ export class TerritoryWebGLRenderer { public readonly canvas: HTMLCanvasElement; private contestEnabled = false; + private contestPatternMode: 0 | 1 | 2 = 0; // 0=blueNoise(strength), 1=checkerboard(50/50), 2=bayer4x4(strength) private readonly gl: WebGL2RenderingContext | null; private readonly program: WebGLProgram | null; @@ -94,6 +95,7 @@ export class TerritoryWebGLRenderer { relations: WebGLUniformLocation | null; patterns: WebGLUniformLocation | null; contestEnabled: WebGLUniformLocation | null; + contestPatternMode: WebGLUniformLocation | null; contestOwners: WebGLUniformLocation | null; contestIds: WebGLUniformLocation | null; contestTimes: WebGLUniformLocation | null; @@ -258,6 +260,7 @@ export class TerritoryWebGLRenderer { relations: null, patterns: null, contestEnabled: null, + contestPatternMode: null, contestOwners: null, contestIds: null, contestTimes: null, @@ -351,6 +354,7 @@ export class TerritoryWebGLRenderer { relations: null, patterns: null, contestEnabled: null, + contestPatternMode: null, contestOwners: null, contestIds: null, contestTimes: null, @@ -445,6 +449,10 @@ export class TerritoryWebGLRenderer { relations: gl.getUniformLocation(this.program, "u_relations"), patterns: gl.getUniformLocation(this.program, "u_patterns"), contestEnabled: gl.getUniformLocation(this.program, "u_contestEnabled"), + contestPatternMode: gl.getUniformLocation( + this.program, + "u_contestPatternMode", + ), contestOwners: gl.getUniformLocation(this.program, "u_contestOwners"), contestIds: gl.getUniformLocation(this.program, "u_contestIds"), contestTimes: gl.getUniformLocation(this.program, "u_contestTimes"), @@ -1284,6 +1292,12 @@ export class TerritoryWebGLRenderer { } } + setContestPatternMode(mode: "blueNoise" | "checkerboard" | "bayer4x4") { + if (mode === "checkerboard") this.contestPatternMode = 1; + else if (mode === "bayer4x4") this.contestPatternMode = 2; + else this.contestPatternMode = 0; + } + markTile(tile: TileRef) { if (this.needsFullUpload) { return; @@ -1757,6 +1771,9 @@ export class TerritoryWebGLRenderer { if (this.uniforms.contestEnabled) { gl.uniform1i(this.uniforms.contestEnabled, this.contestEnabled ? 1 : 0); } + if (this.uniforms.contestPatternMode) { + gl.uniform1i(this.uniforms.contestPatternMode, this.contestPatternMode); + } if (this.uniforms.contestNow) { gl.uniform1i(this.uniforms.contestNow, this.contestNow); } @@ -2713,6 +2730,7 @@ export class TerritoryWebGLRenderer { uniform usampler2D u_relations; uniform usampler2D u_patterns; uniform bool u_contestEnabled; + uniform int u_contestPatternMode; // 0=blueNoise(strength), 1=checkerboard(50/50), 2=bayer4x4(strength) uniform usampler2D u_contestOwners; uniform usampler2D u_contestIds; uniform usampler2D u_contestTimes; @@ -2934,6 +2952,47 @@ export class TerritoryWebGLRenderer { return fract(52.9829189 * x); } + float bayer4x4(ivec2 texCoord) { + // Classic 4x4 Bayer matrix values 0..15 mapped to (0.5/16 .. 15.5/16) + int x = texCoord.x & 3; + int y = texCoord.y & 3; + int idx = (y << 2) | x; + int v = 0; + // Row-major: + // 0 8 2 10 + // 12 4 14 6 + // 3 11 1 9 + // 15 7 13 5 + if (idx == 0) v = 0; + else if (idx == 1) v = 8; + else if (idx == 2) v = 2; + else if (idx == 3) v = 10; + else if (idx == 4) v = 12; + else if (idx == 5) v = 4; + else if (idx == 6) v = 14; + else if (idx == 7) v = 6; + else if (idx == 8) v = 3; + else if (idx == 9) v = 11; + else if (idx == 10) v = 1; + else if (idx == 11) v = 9; + else if (idx == 12) v = 15; + else if (idx == 13) v = 7; + else if (idx == 14) v = 13; + else v = 5; + return (float(v) + 0.5) / 16.0; + } + + bool contestPickAttacker(ivec2 texCoord, float strength) { + if (u_contestPatternMode == 1) { + // Checkerboard is always 50/50 (ignores strength) + return ((texCoord.x + texCoord.y) & 1) == 0; + } + if (u_contestPatternMode == 2) { + return bayer4x4(texCoord) < strength; + } + return blueNoise(texCoord) < strength; + } + uint relationCode(uint owner, uint other) { if (owner == 0u || other == 0u) { return 0u; @@ -3193,8 +3252,8 @@ export class TerritoryWebGLRenderer { defenderBase = defenderColor.rgb; } float strength = contestStrength(contestId); - float noise = blueNoise(texCoord); - vec3 contestColor = noise < strength ? latestOwnerBase : defenderBase; + bool pickAttacker = contestPickAttacker(texCoord, strength); + vec3 contestColor = pickAttacker ? latestOwnerBase : defenderBase; // Blend contested fill on top of terrain color = mix(baseTerrainColor, contestColor, u_alpha); }