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.
This commit is contained in:
scamiv
2026-01-13 06:03:03 +01:00
parent ae96118edc
commit 849e8a7ff3
2 changed files with 139 additions and 2 deletions
@@ -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}`,
@@ -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);
}