mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-27 03:04:39 +00:00
accept delay, use triplebuffer,
Refactor contest management in TerritoryLayer and TerritoryWebGLRenderer - Updated contest duration handling to use ticks instead of milliseconds for improved precision. - Introduced new tick-based state management for contest updates and rendering. - Enhanced interpolation logic for smoother transitions between contest states. - Removed obsolete smooth state handling and related properties to streamline code. - Added support for older contest states in the WebGL renderer for better visual fidelity.
This commit is contained in:
@@ -20,7 +20,7 @@ import { TerritoryWebGLRenderer } from "./TerritoryWebGLRenderer";
|
||||
const CONTEST_ID_MASK = 0x7fff;
|
||||
const CONTEST_ATTACKER_EVER_BIT = 0x8000;
|
||||
const CONTEST_TIME_WRAP = 32768;
|
||||
const DEFAULT_CONTEST_DURATION_MS = 200;
|
||||
const DEFAULT_CONTEST_DURATION_TICKS = 2;
|
||||
const CONTEST_SPEED_TPS_MAX = 20;
|
||||
const CONTEST_SPEED_EMA_ALPHA = 0.8;
|
||||
const CONTEST_SPEED_DECAY_HALFLIFE_MS = 100;
|
||||
@@ -65,7 +65,7 @@ export class TerritoryLayer implements Layer {
|
||||
private lastFocusedPlayer: PlayerView | null = null;
|
||||
private lastMyPlayerSmallId: number | null = null;
|
||||
private lastPaletteSignature: string | null = null;
|
||||
private contestDurationMs = DEFAULT_CONTEST_DURATION_MS;
|
||||
private contestDurationTicks = DEFAULT_CONTEST_DURATION_TICKS;
|
||||
private contestActive = false;
|
||||
private contestNextId = 1;
|
||||
private contestFreeIds: number[] = [];
|
||||
@@ -74,10 +74,15 @@ export class TerritoryLayer implements Layer {
|
||||
private contestAttackers: Uint16Array | null = null;
|
||||
private contestTileIndices: Int32Array | null = null;
|
||||
private contestComponents = new Map<number, ContestComponent>();
|
||||
private smoothDurationMs = 100;
|
||||
private smoothActive = false;
|
||||
private smoothStartMs = 0;
|
||||
private smoothSnapshotPending = false;
|
||||
private tickSnapshotPending = false;
|
||||
private tickTimeMsCurrent = 0;
|
||||
private tickTimeMsPrev = 0;
|
||||
private tickTimeMsOlder = 0;
|
||||
private tickNumberCurrent: number | null = null;
|
||||
private tickNumberPrev: number | null = null;
|
||||
private tickNumberOlder: number | null = null;
|
||||
private interpolationDelayMs = 100;
|
||||
private lastInterpolationPair: "prevCurrent" | "olderPrev" = "prevCurrent";
|
||||
private contestSpeedDeltas = new Map<number, number>();
|
||||
private contestSpeedLastUpdateMs = 0;
|
||||
|
||||
@@ -114,17 +119,27 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
this.refreshPaletteIfNeeded();
|
||||
|
||||
const tickNumber = this.game.ticks();
|
||||
if (this.tickNumberCurrent !== tickNumber) {
|
||||
this.tickNumberOlder = this.tickNumberPrev;
|
||||
this.tickNumberPrev = this.tickNumberCurrent;
|
||||
this.tickNumberCurrent = tickNumber;
|
||||
|
||||
this.tickTimeMsOlder = this.tickTimeMsPrev;
|
||||
this.tickTimeMsPrev = this.tickTimeMsCurrent;
|
||||
this.tickTimeMsCurrent = now;
|
||||
|
||||
if (this.territoryRenderer) {
|
||||
this.tickSnapshotPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.game.recentlyUpdatedTiles().forEach((t) => this.markTile(t));
|
||||
const ownerUpdates = this.game.recentlyUpdatedOwnerTiles();
|
||||
if (ownerUpdates.length > 0) {
|
||||
if (this.territoryRenderer) {
|
||||
this.smoothSnapshotPending = true;
|
||||
}
|
||||
this.smoothStartMs = now;
|
||||
this.smoothActive = true;
|
||||
}
|
||||
this.contestSpeedDeltas.clear();
|
||||
this.applyContestChanges(ownerUpdates, now);
|
||||
const nowTickPacked = this.packContestTick(this.game.ticks());
|
||||
this.applyContestChanges(ownerUpdates, nowTickPacked);
|
||||
this.updateContestState(nowTickPacked);
|
||||
this.updateContestSpeeds(now);
|
||||
this.updateContestStrengths();
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
@@ -389,7 +404,6 @@ export class TerritoryLayer implements Layer {
|
||||
this.configureRenderers();
|
||||
this.ensureContestScratch();
|
||||
this.syncContestStateToRenderer();
|
||||
this.syncSmoothStateToRenderer();
|
||||
|
||||
// Add a second canvas for highlights
|
||||
this.highlightCanvas = document.createElement("canvas");
|
||||
@@ -450,12 +464,11 @@ export class TerritoryLayer implements Layer {
|
||||
return;
|
||||
}
|
||||
const now = this.nowMs();
|
||||
if (this.smoothSnapshotPending) {
|
||||
if (this.tickSnapshotPending) {
|
||||
this.territoryRenderer.snapshotStateForSmoothing();
|
||||
this.smoothSnapshotPending = false;
|
||||
this.tickSnapshotPending = false;
|
||||
}
|
||||
this.updateSmoothState(now);
|
||||
this.updateContestState(now);
|
||||
this.updateInterpolationState(now);
|
||||
|
||||
const renderTerritoryStart = FrameProfiler.start();
|
||||
this.territoryRenderer.setViewSize(
|
||||
@@ -582,20 +595,38 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private updateSmoothState(now: number) {
|
||||
private updateInterpolationState(now: number) {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
}
|
||||
let progress = 1;
|
||||
if (this.smoothActive) {
|
||||
const elapsed = now - this.smoothStartMs;
|
||||
progress = Math.max(0, Math.min(1, elapsed / this.smoothDurationMs));
|
||||
if (progress >= 1) {
|
||||
this.smoothActive = false;
|
||||
}
|
||||
|
||||
if (this.tickTimeMsPrev <= 0 || this.tickTimeMsCurrent <= 0) {
|
||||
this.lastInterpolationPair = "prevCurrent";
|
||||
this.territoryRenderer.setInterpolationPair("prevCurrent");
|
||||
this.territoryRenderer.setSmoothProgress(1);
|
||||
this.territoryRenderer.setSmoothEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const renderTime = now - this.interpolationDelayMs;
|
||||
|
||||
let pair: "prevCurrent" | "olderPrev" = "prevCurrent";
|
||||
let fromTime = this.tickTimeMsPrev;
|
||||
let toTime = this.tickTimeMsCurrent;
|
||||
|
||||
if (this.tickTimeMsOlder > 0 && renderTime < this.tickTimeMsPrev) {
|
||||
pair = "olderPrev";
|
||||
fromTime = this.tickTimeMsOlder;
|
||||
toTime = this.tickTimeMsPrev;
|
||||
}
|
||||
|
||||
const denom = Math.max(1, Math.min(250, toTime - fromTime));
|
||||
const progress = Math.max(0, Math.min(1, (renderTime - fromTime) / denom));
|
||||
|
||||
this.lastInterpolationPair = pair;
|
||||
this.territoryRenderer.setInterpolationPair(pair);
|
||||
this.territoryRenderer.setSmoothProgress(progress);
|
||||
this.territoryRenderer.setSmoothEnabled(this.smoothActive);
|
||||
this.territoryRenderer.setSmoothEnabled(true);
|
||||
}
|
||||
|
||||
private recordContestSpeed(componentId: number) {
|
||||
@@ -605,13 +636,12 @@ export class TerritoryLayer implements Layer {
|
||||
|
||||
private applyContestChanges(
|
||||
changes: Array<{ tile: TileRef; previousOwner: number; newOwner: number }>,
|
||||
now: number,
|
||||
nowTickPacked: number,
|
||||
) {
|
||||
if (!this.territoryRenderer || changes.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.ensureContestScratch();
|
||||
const nowPacked = this.packContestTime(now);
|
||||
|
||||
for (const change of changes) {
|
||||
if (change.newOwner === change.previousOwner) {
|
||||
@@ -624,7 +654,7 @@ export class TerritoryLayer implements Layer {
|
||||
tile,
|
||||
change.previousOwner,
|
||||
change.newOwner,
|
||||
nowPacked,
|
||||
nowTickPacked,
|
||||
);
|
||||
if (component) {
|
||||
this.recordContestSpeed(component.id);
|
||||
@@ -639,7 +669,7 @@ export class TerritoryLayer implements Layer {
|
||||
tile,
|
||||
change.previousOwner,
|
||||
change.newOwner,
|
||||
nowPacked,
|
||||
nowTickPacked,
|
||||
);
|
||||
if (newComponent) {
|
||||
this.recordContestSpeed(newComponent.id);
|
||||
@@ -660,8 +690,8 @@ export class TerritoryLayer implements Layer {
|
||||
component.id,
|
||||
attackerEver,
|
||||
);
|
||||
component.lastActivityPacked = nowPacked;
|
||||
this.territoryRenderer.setContestTime(component.id, nowPacked);
|
||||
component.lastActivityPacked = nowTickPacked;
|
||||
this.territoryRenderer.setContestTime(component.id, nowTickPacked);
|
||||
this.recordContestSpeed(component.id);
|
||||
} else {
|
||||
this.removeTileFromComponent(tile, component);
|
||||
@@ -669,7 +699,7 @@ export class TerritoryLayer implements Layer {
|
||||
tile,
|
||||
change.previousOwner,
|
||||
change.newOwner,
|
||||
nowPacked,
|
||||
nowTickPacked,
|
||||
);
|
||||
if (newComponent) {
|
||||
this.recordContestSpeed(newComponent.id);
|
||||
@@ -777,13 +807,15 @@ export class TerritoryLayer implements Layer {
|
||||
return total;
|
||||
}
|
||||
|
||||
private updateContestState(now: number) {
|
||||
private updateContestState(nowTickPacked: number) {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
}
|
||||
this.ensureContestScratch();
|
||||
const nowPacked = this.packContestTime(now);
|
||||
this.territoryRenderer.setContestNow(nowPacked, this.contestDurationMs);
|
||||
this.territoryRenderer.setContestNow(
|
||||
nowTickPacked,
|
||||
this.contestDurationTicks,
|
||||
);
|
||||
|
||||
if (!this.contestActive) {
|
||||
return;
|
||||
@@ -792,10 +824,10 @@ export class TerritoryLayer implements Layer {
|
||||
const expired: ContestComponent[] = [];
|
||||
for (const component of this.contestComponents.values()) {
|
||||
const elapsed = this.contestElapsed(
|
||||
nowPacked,
|
||||
nowTickPacked,
|
||||
component.lastActivityPacked,
|
||||
);
|
||||
if (elapsed >= this.contestDurationMs) {
|
||||
if (elapsed >= this.contestDurationTicks) {
|
||||
expired.push(component);
|
||||
}
|
||||
}
|
||||
@@ -809,7 +841,7 @@ export class TerritoryLayer implements Layer {
|
||||
tile: TileRef,
|
||||
defender: number,
|
||||
attacker: number,
|
||||
nowPacked: number,
|
||||
nowTickPacked: number,
|
||||
): ContestComponent | null {
|
||||
if (attacker === defender || attacker === 0 || defender === 0) {
|
||||
return null;
|
||||
@@ -817,7 +849,7 @@ export class TerritoryLayer implements Layer {
|
||||
const neighbors = this.collectNeighborComponents(tile, attacker, defender);
|
||||
let component: ContestComponent;
|
||||
if (neighbors.length === 0) {
|
||||
component = this.createContestComponent(attacker, defender, nowPacked);
|
||||
component = this.createContestComponent(attacker, defender, nowTickPacked);
|
||||
} else {
|
||||
component = neighbors[0];
|
||||
for (let i = 1; i < neighbors.length; i++) {
|
||||
@@ -826,8 +858,8 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
|
||||
this.addTileToComponent(tile, component, true);
|
||||
component.lastActivityPacked = nowPacked;
|
||||
this.territoryRenderer?.setContestTime(component.id, nowPacked);
|
||||
component.lastActivityPacked = nowTickPacked;
|
||||
this.territoryRenderer?.setContestTime(component.id, nowTickPacked);
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -858,14 +890,14 @@ export class TerritoryLayer implements Layer {
|
||||
private createContestComponent(
|
||||
attacker: number,
|
||||
defender: number,
|
||||
nowPacked: number,
|
||||
nowTickPacked: number,
|
||||
): ContestComponent {
|
||||
const id = this.allocateContestComponentId();
|
||||
const component: ContestComponent = {
|
||||
id,
|
||||
attacker,
|
||||
defender,
|
||||
lastActivityPacked: nowPacked,
|
||||
lastActivityPacked: nowTickPacked,
|
||||
tiles: [],
|
||||
speed: 0,
|
||||
strength: 0.5,
|
||||
@@ -1026,8 +1058,8 @@ export class TerritoryLayer implements Layer {
|
||||
return (this.contestComponentIds![tile] & CONTEST_ATTACKER_EVER_BIT) !== 0;
|
||||
}
|
||||
|
||||
private packContestTime(now: number): number {
|
||||
return Math.floor(now) % CONTEST_TIME_WRAP;
|
||||
private packContestTick(tick: number): number {
|
||||
return Math.floor(tick) % CONTEST_TIME_WRAP;
|
||||
}
|
||||
|
||||
private contestElapsed(nowPacked: number, startPacked: number): number {
|
||||
@@ -1078,25 +1110,6 @@ export class TerritoryLayer implements Layer {
|
||||
}
|
||||
}
|
||||
|
||||
private syncSmoothStateToRenderer() {
|
||||
if (!this.territoryRenderer) {
|
||||
return;
|
||||
}
|
||||
if (this.smoothActive) {
|
||||
const now = this.nowMs();
|
||||
const elapsed = now - this.smoothStartMs;
|
||||
const progress = Math.max(
|
||||
0,
|
||||
Math.min(1, elapsed / this.smoothDurationMs),
|
||||
);
|
||||
this.territoryRenderer.setSmoothProgress(progress);
|
||||
this.territoryRenderer.setSmoothEnabled(true);
|
||||
} else {
|
||||
this.territoryRenderer.setSmoothEnabled(false);
|
||||
this.territoryRenderer.setSmoothProgress(1);
|
||||
}
|
||||
}
|
||||
|
||||
private computePaletteSignature(): string {
|
||||
let maxSmallId = 0;
|
||||
for (const player of this.game.playerViews()) {
|
||||
@@ -1134,12 +1147,13 @@ export class TerritoryLayer implements Layer {
|
||||
`view: ${stats.viewWidth}x${stats.viewHeight}`,
|
||||
`scale: ${stats.viewScale.toFixed(2)}`,
|
||||
`offset: ${stats.viewOffsetX.toFixed(1)}, ${stats.viewOffsetY.toFixed(1)}`,
|
||||
`smooth: ${stats.smoothEnabled ? "on" : "off"} ${stats.smoothProgress.toFixed(2)} active ${this.smoothActive ? "yes" : "no"}`,
|
||||
`smooth: ${stats.smoothEnabled ? "on" : "off"} ${stats.smoothProgress.toFixed(2)} pair ${this.lastInterpolationPair}`,
|
||||
`tick: ${this.tickNumberCurrent ?? "-"} prev ${this.tickNumberPrev ?? "-"}`,
|
||||
`delayMs: ${this.interpolationDelayMs.toFixed(0)}`,
|
||||
`smoothPrereq: prevCopy ${stats.prevStateCopySupported ? "yes" : "no"}`,
|
||||
`jfa: ${jfaStatus} dirty ${stats.jfaDirty ? "yes" : "no"}`,
|
||||
`contest: ${this.contestActive ? "on" : "off"} comps ${this.contestComponents.size}`,
|
||||
`contestMs: ${this.contestDurationMs}`,
|
||||
`smoothMs: ${this.smoothDurationMs}`,
|
||||
`contestTicks: ${this.contestDurationTicks}`,
|
||||
`hovered: ${stats.hoveredPlayerId}`,
|
||||
];
|
||||
const padding = 6;
|
||||
|
||||
@@ -43,14 +43,18 @@ export class TerritoryWebGLRenderer {
|
||||
private readonly contestSpeedsTexture: WebGLTexture | null;
|
||||
private readonly contestStrengthsTexture: WebGLTexture | null;
|
||||
private readonly prevOwnerTexture: WebGLTexture | null;
|
||||
private readonly olderOwnerTexture: WebGLTexture | null;
|
||||
private readonly stateFramebuffer: WebGLFramebuffer | null;
|
||||
private readonly prevStateFramebuffer: WebGLFramebuffer | null;
|
||||
private readonly olderStateFramebuffer: WebGLFramebuffer | null;
|
||||
private readonly jfaTextureA: WebGLTexture | null;
|
||||
private readonly jfaTextureB: WebGLTexture | null;
|
||||
private readonly jfaFramebufferA: WebGLFramebuffer | null;
|
||||
private readonly jfaFramebufferB: WebGLFramebuffer | null;
|
||||
private readonly jfaResultOlderTexture: WebGLTexture | null;
|
||||
private readonly jfaResultOldTexture: WebGLTexture | null;
|
||||
private readonly jfaResultNewTexture: WebGLTexture | null;
|
||||
private readonly jfaResultOlderFramebuffer: WebGLFramebuffer | null;
|
||||
private readonly jfaResultOldFramebuffer: WebGLFramebuffer | null;
|
||||
private readonly jfaResultNewFramebuffer: WebGLFramebuffer | null;
|
||||
private readonly jfaSeedProgram: WebGLProgram | null;
|
||||
@@ -70,6 +74,7 @@ export class TerritoryWebGLRenderer {
|
||||
viewScale: WebGLUniformLocation | null;
|
||||
viewOffset: WebGLUniformLocation | null;
|
||||
state: WebGLUniformLocation | null;
|
||||
latestState: WebGLUniformLocation | null;
|
||||
palette: WebGLUniformLocation | null;
|
||||
relations: WebGLUniformLocation | null;
|
||||
patterns: WebGLUniformLocation | null;
|
||||
@@ -138,14 +143,16 @@ export class TerritoryWebGLRenderer {
|
||||
private hoveredPlayerId = -1;
|
||||
private animationStartTime = Date.now();
|
||||
private contestNow = 0;
|
||||
private contestDurationMs = 0;
|
||||
private contestDurationTicks = 0;
|
||||
private smoothProgress = 1;
|
||||
private smoothEnabled = true;
|
||||
private jfaSupported = false;
|
||||
private jfaDisabledReason: string | null = null;
|
||||
private jfaDirty = false;
|
||||
private jfaHistoryInitialized = false;
|
||||
private prevStateCopySupported = false;
|
||||
private jfaSteps: number[] = [];
|
||||
private interpolationPair: "prevCurrent" | "olderPrev" = "prevCurrent";
|
||||
private readonly userSettings = new UserSettings();
|
||||
private readonly patternBytesCache = new Map<string, Uint8Array>();
|
||||
|
||||
@@ -191,14 +198,18 @@ export class TerritoryWebGLRenderer {
|
||||
this.contestSpeedsTexture = null;
|
||||
this.contestStrengthsTexture = null;
|
||||
this.prevOwnerTexture = null;
|
||||
this.olderOwnerTexture = null;
|
||||
this.stateFramebuffer = null;
|
||||
this.prevStateFramebuffer = null;
|
||||
this.olderStateFramebuffer = null;
|
||||
this.jfaTextureA = null;
|
||||
this.jfaTextureB = null;
|
||||
this.jfaFramebufferA = null;
|
||||
this.jfaFramebufferB = null;
|
||||
this.jfaResultOlderTexture = null;
|
||||
this.jfaResultOldTexture = null;
|
||||
this.jfaResultNewTexture = null;
|
||||
this.jfaResultOlderFramebuffer = null;
|
||||
this.jfaResultOldFramebuffer = null;
|
||||
this.jfaResultNewFramebuffer = null;
|
||||
this.jfaSeedProgram = null;
|
||||
@@ -211,6 +222,7 @@ export class TerritoryWebGLRenderer {
|
||||
viewScale: null,
|
||||
viewOffset: null,
|
||||
state: null,
|
||||
latestState: null,
|
||||
palette: null,
|
||||
relations: null,
|
||||
patterns: null,
|
||||
@@ -264,14 +276,18 @@ export class TerritoryWebGLRenderer {
|
||||
this.contestSpeedsTexture = null;
|
||||
this.contestStrengthsTexture = null;
|
||||
this.prevOwnerTexture = null;
|
||||
this.olderOwnerTexture = null;
|
||||
this.stateFramebuffer = null;
|
||||
this.prevStateFramebuffer = null;
|
||||
this.olderStateFramebuffer = null;
|
||||
this.jfaTextureA = null;
|
||||
this.jfaTextureB = null;
|
||||
this.jfaFramebufferA = null;
|
||||
this.jfaFramebufferB = null;
|
||||
this.jfaResultOlderTexture = null;
|
||||
this.jfaResultOldTexture = null;
|
||||
this.jfaResultNewTexture = null;
|
||||
this.jfaResultOlderFramebuffer = null;
|
||||
this.jfaResultOldFramebuffer = null;
|
||||
this.jfaResultNewFramebuffer = null;
|
||||
this.jfaSeedProgram = null;
|
||||
@@ -284,6 +300,7 @@ export class TerritoryWebGLRenderer {
|
||||
viewScale: null,
|
||||
viewOffset: null,
|
||||
state: null,
|
||||
latestState: null,
|
||||
palette: null,
|
||||
relations: null,
|
||||
patterns: null,
|
||||
@@ -357,6 +374,7 @@ export class TerritoryWebGLRenderer {
|
||||
viewScale: gl.getUniformLocation(this.program, "u_viewScale"),
|
||||
viewOffset: gl.getUniformLocation(this.program, "u_viewOffset"),
|
||||
state: gl.getUniformLocation(this.program, "u_state"),
|
||||
latestState: gl.getUniformLocation(this.program, "u_latestState"),
|
||||
palette: gl.getUniformLocation(this.program, "u_palette"),
|
||||
relations: gl.getUniformLocation(this.program, "u_relations"),
|
||||
patterns: gl.getUniformLocation(this.program, "u_patterns"),
|
||||
@@ -372,7 +390,7 @@ export class TerritoryWebGLRenderer {
|
||||
contestNow: gl.getUniformLocation(this.program, "u_contestNow"),
|
||||
contestDuration: gl.getUniformLocation(
|
||||
this.program,
|
||||
"u_contestDurationMs",
|
||||
"u_contestDurationTicks",
|
||||
),
|
||||
prevOwner: gl.getUniformLocation(this.program, "u_prevOwner"),
|
||||
jfaSeedsOld: gl.getUniformLocation(this.program, "u_jfaSeedsOld"),
|
||||
@@ -466,14 +484,20 @@ export class TerritoryWebGLRenderer {
|
||||
this.contestSpeedsTexture = gl.createTexture();
|
||||
this.contestStrengthsTexture = gl.createTexture();
|
||||
this.prevOwnerTexture = gl.createTexture();
|
||||
this.olderOwnerTexture = gl.createTexture();
|
||||
this.stateFramebuffer = gl.createFramebuffer();
|
||||
this.prevStateFramebuffer = gl.createFramebuffer();
|
||||
this.olderStateFramebuffer = gl.createFramebuffer();
|
||||
this.jfaTextureA = this.jfaSupported ? gl.createTexture() : null;
|
||||
this.jfaTextureB = this.jfaSupported ? gl.createTexture() : null;
|
||||
this.jfaFramebufferA = this.jfaSupported ? gl.createFramebuffer() : null;
|
||||
this.jfaFramebufferB = this.jfaSupported ? gl.createFramebuffer() : null;
|
||||
this.jfaResultOlderTexture = this.jfaSupported ? gl.createTexture() : null;
|
||||
this.jfaResultOldTexture = this.jfaSupported ? gl.createTexture() : null;
|
||||
this.jfaResultNewTexture = this.jfaSupported ? gl.createTexture() : null;
|
||||
this.jfaResultOlderFramebuffer = this.jfaSupported
|
||||
? gl.createFramebuffer()
|
||||
: null;
|
||||
this.jfaResultOldFramebuffer = this.jfaSupported
|
||||
? gl.createFramebuffer()
|
||||
: null;
|
||||
@@ -616,11 +640,32 @@ export class TerritoryWebGLRenderer {
|
||||
this.state,
|
||||
);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE13);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.olderOwnerTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.R16UI,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
gl.RED_INTEGER,
|
||||
gl.UNSIGNED_SHORT,
|
||||
this.state,
|
||||
);
|
||||
|
||||
if (
|
||||
this.stateFramebuffer &&
|
||||
this.prevStateFramebuffer &&
|
||||
this.olderStateFramebuffer &&
|
||||
this.stateTexture &&
|
||||
this.prevOwnerTexture
|
||||
this.prevOwnerTexture &&
|
||||
this.olderOwnerTexture
|
||||
) {
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.stateFramebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
@@ -640,9 +685,19 @@ export class TerritoryWebGLRenderer {
|
||||
0,
|
||||
);
|
||||
const prevStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.olderStateFramebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER,
|
||||
gl.COLOR_ATTACHMENT0,
|
||||
gl.TEXTURE_2D,
|
||||
this.olderOwnerTexture,
|
||||
0,
|
||||
);
|
||||
const olderStatus = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
|
||||
this.prevStateCopySupported =
|
||||
stateStatus === gl.FRAMEBUFFER_COMPLETE &&
|
||||
prevStatus === gl.FRAMEBUFFER_COMPLETE;
|
||||
prevStatus === gl.FRAMEBUFFER_COMPLETE &&
|
||||
olderStatus === gl.FRAMEBUFFER_COMPLETE;
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
}
|
||||
|
||||
@@ -652,8 +707,10 @@ export class TerritoryWebGLRenderer {
|
||||
this.jfaTextureB &&
|
||||
this.jfaFramebufferA &&
|
||||
this.jfaFramebufferB &&
|
||||
this.jfaResultOlderTexture &&
|
||||
this.jfaResultOldTexture &&
|
||||
this.jfaResultNewTexture &&
|
||||
this.jfaResultOlderFramebuffer &&
|
||||
this.jfaResultOldFramebuffer &&
|
||||
this.jfaResultNewFramebuffer
|
||||
) {
|
||||
@@ -710,6 +767,24 @@ export class TerritoryWebGLRenderer {
|
||||
0,
|
||||
);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE12);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOlderTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(
|
||||
gl.TEXTURE_2D,
|
||||
0,
|
||||
gl.RG16F,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
gl.RG,
|
||||
gl.HALF_FLOAT,
|
||||
null,
|
||||
);
|
||||
|
||||
gl.activeTexture(gl.TEXTURE10);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOldTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
@@ -746,6 +821,14 @@ export class TerritoryWebGLRenderer {
|
||||
null,
|
||||
);
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultOlderFramebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER,
|
||||
gl.COLOR_ATTACHMENT0,
|
||||
gl.TEXTURE_2D,
|
||||
this.jfaResultOlderTexture,
|
||||
0,
|
||||
);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
||||
gl.framebufferTexture2D(
|
||||
gl.FRAMEBUFFER,
|
||||
@@ -770,6 +853,9 @@ export class TerritoryWebGLRenderer {
|
||||
|
||||
gl.useProgram(this.program);
|
||||
gl.uniform1i(this.uniforms.state, 0);
|
||||
if (this.uniforms.latestState) {
|
||||
gl.uniform1i(this.uniforms.latestState, 12);
|
||||
}
|
||||
gl.uniform1i(this.uniforms.palette, 1);
|
||||
gl.uniform1i(this.uniforms.relations, 2);
|
||||
gl.uniform1i(this.uniforms.patterns, 3);
|
||||
@@ -905,7 +991,7 @@ export class TerritoryWebGLRenderer {
|
||||
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
|
||||
}
|
||||
if (this.uniforms.contestDuration) {
|
||||
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationMs);
|
||||
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationTicks);
|
||||
}
|
||||
if (this.uniforms.smoothProgress) {
|
||||
gl.uniform1f(this.uniforms.smoothProgress, this.smoothProgress);
|
||||
@@ -1167,9 +1253,9 @@ export class TerritoryWebGLRenderer {
|
||||
this.needsContestStrengthsUpload = true;
|
||||
}
|
||||
|
||||
setContestNow(nowPacked: number, durationMs: number) {
|
||||
setContestNow(nowPacked: number, durationTicks: number) {
|
||||
this.contestNow = nowPacked | 0;
|
||||
this.contestDurationMs = Math.max(0, durationMs);
|
||||
this.contestDurationTicks = Math.max(0, durationTicks);
|
||||
}
|
||||
|
||||
snapshotStateForSmoothing() {
|
||||
@@ -1177,11 +1263,27 @@ export class TerritoryWebGLRenderer {
|
||||
!this.gl ||
|
||||
!this.prevStateCopySupported ||
|
||||
!this.stateFramebuffer ||
|
||||
!this.prevStateFramebuffer
|
||||
!this.prevStateFramebuffer ||
|
||||
!this.olderStateFramebuffer
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const gl = this.gl;
|
||||
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.prevStateFramebuffer);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.olderStateFramebuffer);
|
||||
gl.blitFramebuffer(
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
gl.COLOR_BUFFER_BIT,
|
||||
gl.NEAREST,
|
||||
);
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.stateFramebuffer);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.prevStateFramebuffer);
|
||||
gl.blitFramebuffer(
|
||||
@@ -1198,6 +1300,44 @@ export class TerritoryWebGLRenderer {
|
||||
);
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
||||
|
||||
if (
|
||||
this.jfaSupported &&
|
||||
this.jfaResultOlderFramebuffer &&
|
||||
this.jfaResultOldFramebuffer &&
|
||||
this.jfaResultNewFramebuffer
|
||||
) {
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOlderFramebuffer);
|
||||
gl.blitFramebuffer(
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
gl.COLOR_BUFFER_BIT,
|
||||
gl.NEAREST,
|
||||
);
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultNewFramebuffer);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
||||
gl.blitFramebuffer(
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
gl.COLOR_BUFFER_BIT,
|
||||
gl.NEAREST,
|
||||
);
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
||||
}
|
||||
this.jfaDirty = true;
|
||||
}
|
||||
|
||||
@@ -1214,6 +1354,10 @@ export class TerritoryWebGLRenderer {
|
||||
!!this.jfaResultNewTexture;
|
||||
}
|
||||
|
||||
setInterpolationPair(pair: "prevCurrent" | "olderPrev") {
|
||||
this.interpolationPair = pair;
|
||||
}
|
||||
|
||||
markAllDirty() {
|
||||
this.needsFullUpload = true;
|
||||
this.dirtyRows.clear();
|
||||
@@ -1270,7 +1414,7 @@ export class TerritoryWebGLRenderer {
|
||||
uploadContestStrengthsSpan,
|
||||
);
|
||||
|
||||
if (this.jfaSupported && this.smoothEnabled) {
|
||||
if (this.jfaSupported) {
|
||||
this.updateJfa();
|
||||
}
|
||||
|
||||
@@ -1278,9 +1422,23 @@ export class TerritoryWebGLRenderer {
|
||||
gl.viewport(0, 0, this.viewWidth, this.viewHeight);
|
||||
gl.useProgram(this.program);
|
||||
gl.bindVertexArray(this.vao);
|
||||
if (this.stateTexture) {
|
||||
|
||||
const canUseOlderPair =
|
||||
this.interpolationPair === "olderPrev" &&
|
||||
!!this.prevOwnerTexture &&
|
||||
!!this.olderOwnerTexture &&
|
||||
!!this.jfaResultOldTexture &&
|
||||
!!this.jfaResultOlderTexture;
|
||||
const renderPair = canUseOlderPair ? "olderPrev" : "prevCurrent";
|
||||
|
||||
const toStateTexture =
|
||||
renderPair === "olderPrev" ? this.prevOwnerTexture : this.stateTexture;
|
||||
const fromStateTexture =
|
||||
renderPair === "olderPrev" ? this.olderOwnerTexture : this.prevOwnerTexture;
|
||||
|
||||
if (toStateTexture) {
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
||||
gl.bindTexture(gl.TEXTURE_2D, toStateTexture);
|
||||
}
|
||||
if (this.paletteTexture) {
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
@@ -1306,17 +1464,27 @@ export class TerritoryWebGLRenderer {
|
||||
gl.activeTexture(gl.TEXTURE6);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.contestTimesTexture);
|
||||
}
|
||||
if (this.prevOwnerTexture) {
|
||||
if (fromStateTexture) {
|
||||
gl.activeTexture(gl.TEXTURE7);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.prevOwnerTexture);
|
||||
gl.bindTexture(gl.TEXTURE_2D, fromStateTexture);
|
||||
}
|
||||
if (this.jfaResultOldTexture) {
|
||||
|
||||
const seedsOld =
|
||||
renderPair === "olderPrev" ? this.jfaResultOlderTexture : this.jfaResultOldTexture;
|
||||
const seedsNew =
|
||||
renderPair === "olderPrev" ? this.jfaResultOldTexture : this.jfaResultNewTexture;
|
||||
if (seedsOld) {
|
||||
gl.activeTexture(gl.TEXTURE8);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultOldTexture);
|
||||
gl.bindTexture(gl.TEXTURE_2D, seedsOld);
|
||||
}
|
||||
if (this.jfaResultNewTexture) {
|
||||
if (seedsNew) {
|
||||
gl.activeTexture(gl.TEXTURE9);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.jfaResultNewTexture);
|
||||
gl.bindTexture(gl.TEXTURE_2D, seedsNew);
|
||||
}
|
||||
|
||||
if (this.stateTexture) {
|
||||
gl.activeTexture(gl.TEXTURE12);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this.stateTexture);
|
||||
}
|
||||
if (this.contestSpeedsTexture) {
|
||||
gl.activeTexture(gl.TEXTURE10);
|
||||
@@ -1377,7 +1545,7 @@ export class TerritoryWebGLRenderer {
|
||||
gl.uniform1i(this.uniforms.contestNow, this.contestNow);
|
||||
}
|
||||
if (this.uniforms.contestDuration) {
|
||||
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationMs);
|
||||
gl.uniform1f(this.uniforms.contestDuration, this.contestDurationTicks);
|
||||
}
|
||||
if (this.uniforms.smoothProgress) {
|
||||
gl.uniform1f(this.uniforms.smoothProgress, this.smoothProgress);
|
||||
@@ -1408,7 +1576,7 @@ export class TerritoryWebGLRenderer {
|
||||
jfaDisabledReason: this.jfaDisabledReason,
|
||||
jfaDirty: this.jfaDirty,
|
||||
prevStateCopySupported: this.prevStateCopySupported,
|
||||
contestDurationMs: this.contestDurationMs,
|
||||
contestDurationTicks: this.contestDurationTicks,
|
||||
contestNow: this.contestNow,
|
||||
hoveredPlayerId: this.hoveredPlayerId,
|
||||
};
|
||||
@@ -1662,11 +1830,8 @@ export class TerritoryWebGLRenderer {
|
||||
!this.jfaFramebufferB ||
|
||||
!this.jfaTextureA ||
|
||||
!this.jfaTextureB ||
|
||||
!this.prevOwnerTexture ||
|
||||
!this.stateTexture ||
|
||||
!this.jfaResultOldFramebuffer ||
|
||||
!this.jfaResultNewFramebuffer ||
|
||||
!this.jfaResultOldTexture ||
|
||||
!this.jfaResultNewTexture ||
|
||||
!this.jfaVao
|
||||
) {
|
||||
@@ -1749,11 +1914,47 @@ export class TerritoryWebGLRenderer {
|
||||
);
|
||||
};
|
||||
|
||||
runJfa(this.prevOwnerTexture, this.jfaResultOldFramebuffer);
|
||||
runJfa(this.stateTexture, this.jfaResultNewFramebuffer);
|
||||
|
||||
this.jfaDirty = false;
|
||||
|
||||
if (
|
||||
!this.jfaHistoryInitialized &&
|
||||
this.jfaResultOlderFramebuffer &&
|
||||
this.jfaResultOldFramebuffer
|
||||
) {
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.jfaResultNewFramebuffer);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOldFramebuffer);
|
||||
gl.blitFramebuffer(
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
gl.COLOR_BUFFER_BIT,
|
||||
gl.NEAREST,
|
||||
);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.jfaResultOlderFramebuffer);
|
||||
gl.blitFramebuffer(
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
0,
|
||||
0,
|
||||
this.mapWidth,
|
||||
this.mapHeight,
|
||||
gl.COLOR_BUFFER_BIT,
|
||||
gl.NEAREST,
|
||||
);
|
||||
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
|
||||
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
|
||||
this.jfaHistoryInitialized = true;
|
||||
}
|
||||
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
if (prevBlend) {
|
||||
gl.enable(gl.BLEND);
|
||||
@@ -2142,6 +2343,7 @@ export class TerritoryWebGLRenderer {
|
||||
precision highp usampler2D;
|
||||
|
||||
uniform usampler2D u_state;
|
||||
uniform usampler2D u_latestState;
|
||||
uniform sampler2D u_palette;
|
||||
uniform usampler2D u_relations;
|
||||
uniform usampler2D u_patterns;
|
||||
@@ -2152,7 +2354,7 @@ export class TerritoryWebGLRenderer {
|
||||
uniform usampler2D u_contestStrengths;
|
||||
uniform bool u_jfaAvailable;
|
||||
uniform int u_contestNow;
|
||||
uniform float u_contestDurationMs;
|
||||
uniform float u_contestDurationTicks;
|
||||
uniform usampler2D u_prevOwner;
|
||||
uniform sampler2D u_jfaSeedsOld;
|
||||
uniform sampler2D u_jfaSeedsNew;
|
||||
@@ -2377,6 +2579,8 @@ export class TerritoryWebGLRenderer {
|
||||
uint owner = state & 0xFFFu;
|
||||
bool hasFallout = (state & 0x2000u) != 0u;
|
||||
bool isDefended = (state & 0x1000u) != 0u;
|
||||
uint latestState = texelFetch(u_latestState, texCoord, 0).r;
|
||||
uint latestOwner = latestState & 0xFFFu;
|
||||
uint oldOwner = prevOwnerAtTex(texCoord);
|
||||
bool smoothActive = u_smoothEnabled &&
|
||||
u_smoothProgress < 1.0 &&
|
||||
@@ -2398,7 +2602,7 @@ export class TerritoryWebGLRenderer {
|
||||
uint elapsed = nowTime >= lastTime
|
||||
? (nowTime - lastTime)
|
||||
: (CONTEST_WRAP - lastTime + nowTime);
|
||||
contested = float(elapsed) < u_contestDurationMs;
|
||||
contested = float(elapsed) < u_contestDurationTicks;
|
||||
}
|
||||
|
||||
bool isBorder = false;
|
||||
@@ -2511,8 +2715,15 @@ export class TerritoryWebGLRenderer {
|
||||
|
||||
vec3 contestedFillColor = fillColor;
|
||||
float contestedFillAlpha = fillAlpha;
|
||||
if (contested && owner != 0u) {
|
||||
vec3 defenderBase = ownerBase;
|
||||
bool useContestedFill = false;
|
||||
if (contested && latestOwner != 0u) {
|
||||
useContestedFill = true;
|
||||
vec3 latestOwnerBase = texelFetch(
|
||||
u_palette,
|
||||
ivec2(int(latestOwner) * 2, 0),
|
||||
0
|
||||
).rgb;
|
||||
vec3 defenderBase = latestOwnerBase;
|
||||
if (defender != 0u) {
|
||||
vec4 defenderColor = texelFetch(
|
||||
u_palette,
|
||||
@@ -2523,12 +2734,12 @@ export class TerritoryWebGLRenderer {
|
||||
}
|
||||
float strength = contestStrength(contestId);
|
||||
float noise = blueNoise(texCoord);
|
||||
contestedFillColor = noise < strength ? ownerBase : defenderBase;
|
||||
contestedFillColor = noise < strength ? latestOwnerBase : defenderBase;
|
||||
contestedFillAlpha = u_alpha;
|
||||
}
|
||||
|
||||
vec3 color = contested ? contestedFillColor : fillColor;
|
||||
float a = contested ? contestedFillAlpha : fillAlpha;
|
||||
vec3 color = useContestedFill ? contestedFillColor : fillColor;
|
||||
float a = useContestedFill ? contestedFillAlpha : fillAlpha;
|
||||
|
||||
if (isBorder && owner != 0u) {
|
||||
color = borderColor;
|
||||
@@ -2661,7 +2872,27 @@ export class TerritoryWebGLRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
if (contested && owner != 0u && u_jfaAvailable) {
|
||||
bool pendingOwnerChange = latestOwner != owner;
|
||||
if (pendingOwnerChange && !useContestedFill && !u_alternativeView) {
|
||||
vec3 hintColor = vec3(1.0);
|
||||
if (latestOwner != 0u) {
|
||||
hintColor = texelFetch(
|
||||
u_palette,
|
||||
ivec2(int(latestOwner) * 2, 0),
|
||||
0
|
||||
).rgb;
|
||||
}
|
||||
const float HINT_ALPHA_RATIO = 0.12;
|
||||
float hintAlpha = u_alpha * HINT_ALPHA_RATIO;
|
||||
if (a < hintAlpha) {
|
||||
a = hintAlpha;
|
||||
color = hintColor;
|
||||
} else {
|
||||
color = mix(color, hintColor, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
if (useContestedFill && u_jfaAvailable) {
|
||||
vec2 seedOld = jfaSeedOldAtTex(texCoord);
|
||||
vec2 seedNew = jfaSeedNewAtTex(texCoord);
|
||||
if (seedOld.x >= 0.0 && seedNew.x >= 0.0) {
|
||||
|
||||
Reference in New Issue
Block a user