From 370d8eec7beaec73bf5548e57cf8c6ceb54c869b Mon Sep 17 00:00:00 2001 From: scamiv <6170744+scamiv@users.noreply.github.com> Date: Sun, 11 Jan 2026 23:01:42 +0100 Subject: [PATCH] Removed contest speed tracking and related properties from TerritoryLayer and TerritoryWebGLRenderer to simplify contest handling. - Updated contest strength calculations to utilize troop counts directly, enhancing accuracy in contest dynamics. - Streamlined rendering logic by eliminating unnecessary checks and textures related to contest speeds, improving performance and clarity. - Refactored related methods to focus on contest strength, ensuring a more cohesive approach to contest state management. --- src/client/graphics/layers/TerritoryLayer.ts | 180 ++++++++---------- .../graphics/layers/TerritoryWebGLRenderer.ts | 122 +----------- 2 files changed, 86 insertions(+), 216 deletions(-) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index f4f37256a..5e97f8ca3 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -21,10 +21,6 @@ const CONTEST_ID_MASK = 0x7fff; const CONTEST_ATTACKER_EVER_BIT = 0x8000; const CONTEST_TIME_WRAP = 32768; 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; -const CONTEST_SPEED_DT_MAX_MS = 200; const CONTEST_STRENGTH_EMA_ALPHA = 0.8; const CONTEST_STRENGTH_MIN = 0.01; const CONTEST_STRENGTH_MAX = 0.95; @@ -36,7 +32,6 @@ type ContestComponent = { defender: number; lastActivityPacked: number; tiles: TileRef[]; - speed: number; strength: number; }; @@ -83,8 +78,6 @@ export class TerritoryLayer implements Layer { private tickNumberOlder: number | null = null; private interpolationDelayMs = 100; private lastInterpolationPair: "prevCurrent" | "olderPrev" = "prevCurrent"; - private contestSpeedDeltas = new Map(); - private contestSpeedLastUpdateMs = 0; constructor( private game: GameView, @@ -136,11 +129,9 @@ export class TerritoryLayer implements Layer { this.game.recentlyUpdatedTiles().forEach((t) => this.markTile(t)); const ownerUpdates = this.game.recentlyUpdatedOwnerTiles(); - this.contestSpeedDeltas.clear(); const nowTickPacked = this.packContestTick(this.game.ticks()); this.applyContestChanges(ownerUpdates, nowTickPacked); this.updateContestState(nowTickPacked); - this.updateContestSpeeds(now); this.updateContestStrengths(); const updates = this.game.updatesSinceLastTick(); @@ -629,11 +620,6 @@ export class TerritoryLayer implements Layer { this.territoryRenderer.setSmoothEnabled(true); } - private recordContestSpeed(componentId: number) { - const current = this.contestSpeedDeltas.get(componentId) ?? 0; - this.contestSpeedDeltas.set(componentId, current + 1); - } - private applyContestChanges( changes: Array<{ tile: TileRef; previousOwner: number; newOwner: number }>, nowTickPacked: number, @@ -650,30 +636,24 @@ export class TerritoryLayer implements Layer { const tile = change.tile; const currentId = this.contestId(tile); if (currentId === 0) { - const component = this.startContestForTile( + this.startContestForTile( tile, change.previousOwner, change.newOwner, nowTickPacked, ); - if (component) { - this.recordContestSpeed(component.id); - } continue; } const component = this.contestComponents.get(currentId); if (!component) { this.clearContestTile(tile); - const newComponent = this.startContestForTile( + this.startContestForTile( tile, change.previousOwner, change.newOwner, nowTickPacked, ); - if (newComponent) { - this.recordContestSpeed(newComponent.id); - } continue; } @@ -692,57 +672,18 @@ export class TerritoryLayer implements Layer { ); component.lastActivityPacked = nowTickPacked; this.territoryRenderer.setContestTime(component.id, nowTickPacked); - this.recordContestSpeed(component.id); } else { this.removeTileFromComponent(tile, component); - const newComponent = this.startContestForTile( + this.startContestForTile( tile, change.previousOwner, change.newOwner, nowTickPacked, ); - if (newComponent) { - this.recordContestSpeed(newComponent.id); - } } } } - private updateContestSpeeds(now: number) { - if (!this.territoryRenderer) { - return; - } - if (this.contestComponents.size === 0) { - this.contestSpeedLastUpdateMs = now; - return; - } - if (this.contestSpeedLastUpdateMs <= 0) { - this.contestSpeedLastUpdateMs = now; - } - let dt = now - this.contestSpeedLastUpdateMs; - dt = Math.max(1, Math.min(CONTEST_SPEED_DT_MAX_MS, dt)); - this.contestSpeedLastUpdateMs = now; - - const decay = Math.pow(0.5, dt / CONTEST_SPEED_DECAY_HALFLIFE_MS); - for (const component of this.contestComponents.values()) { - const delta = this.contestSpeedDeltas.get(component.id) ?? 0; - if (delta > 0) { - const tilesPerSecond = (delta / dt) * 1000; - const instant = Math.min( - 1, - tilesPerSecond / CONTEST_SPEED_TPS_MAX, - ); - component.speed = - component.speed * (1 - CONTEST_SPEED_EMA_ALPHA) + - instant * CONTEST_SPEED_EMA_ALPHA; - } else { - component.speed *= decay; - } - component.speed = Math.max(0, Math.min(1, component.speed)); - this.territoryRenderer.setContestSpeed(component.id, component.speed); - } - } - private updateContestStrengths() { if (!this.territoryRenderer) { return; @@ -750,14 +691,25 @@ export class TerritoryLayer implements Layer { if (this.contestComponents.size === 0) { return; } - const pairStrength = new Map(); + + const involvedIds = new Set(); for (const component of this.contestComponents.values()) { - const key = `${component.attacker}:${component.defender}`; + involvedIds.add(component.attacker); + involvedIds.add(component.defender); + } + const totalTroopsById = this.buildTotalTroopsLookup(involvedIds); + const attackTroopsById = this.buildAttackTroopsLookup(involvedIds); + + const pairStrength = new Map(); + for (const component of this.contestComponents.values()) { + const key = (component.attacker << 16) | component.defender; let strength = pairStrength.get(key); if (strength === undefined) { strength = this.computeContestStrength( component.attacker, component.defender, + totalTroopsById, + attackTroopsById, ); pairStrength.set(key, strength); } @@ -775,21 +727,68 @@ export class TerritoryLayer implements Layer { } } - private computeContestStrength(attackerId: number, defenderId: number) { - const attacker = this.game.playerBySmallID(attackerId); - const defender = this.game.playerBySmallID(defenderId); - if ( - !attacker || - !defender || - !(attacker instanceof PlayerView) || - !(defender instanceof PlayerView) - ) { + private buildTotalTroopsLookup( + involvedIds: Set, + ): Map { + const totals = new Map(); + for (const id of involvedIds) { + const player = this.game.playerBySmallID(id); + if (player instanceof PlayerView) { + totals.set(id, player.troops()); + } + } + return totals; + } + + private buildAttackTroopsLookup( + involvedIds: Set, + ): Map> { + const totals = new Map>(); + for (const id of involvedIds) { + const player = this.game.playerBySmallID(id); + if (!(player instanceof PlayerView)) { + continue; + } + const outgoing = player.outgoingAttacks(); + if (outgoing.length === 0) { + continue; + } + for (const attack of outgoing) { + if (!involvedIds.has(attack.targetID)) { + continue; + } + let byTarget = totals.get(id); + if (!byTarget) { + byTarget = new Map(); + totals.set(id, byTarget); + } + byTarget.set( + attack.targetID, + (byTarget.get(attack.targetID) ?? 0) + attack.troops, + ); + } + } + return totals; + } + + private computeContestStrength( + attackerId: number, + defenderId: number, + totalTroopsById: Map, + attackTroopsById: Map>, + ) { + const attackerTroops = totalTroopsById.get(attackerId); + const defenderTroops = totalTroopsById.get(defenderId); + if (attackerTroops === undefined || defenderTroops === undefined) { return 0.5; } - const attackerAttackTroops = this.attackTroops(attacker, defenderId); - const defenderAttackTroops = this.attackTroops(defender, attackerId); - const attackerPower = attacker.troops() + attackerAttackTroops; - const defenderPower = defender.troops() + defenderAttackTroops; + + const attackerAttackTroops = + attackTroopsById.get(attackerId)?.get(defenderId) ?? 0; + const defenderAttackTroops = + attackTroopsById.get(defenderId)?.get(attackerId) ?? 0; + const attackerPower = attackerTroops + attackerAttackTroops; + const defenderPower = defenderTroops + defenderAttackTroops; const totalPower = attackerPower + defenderPower; if (totalPower <= 0) { return 0.5; @@ -797,16 +796,6 @@ export class TerritoryLayer implements Layer { return Math.max(0, Math.min(1, attackerPower / totalPower)); } - private attackTroops(attacker: PlayerView, targetId: number) { - let total = 0; - for (const attack of attacker.outgoingAttacks()) { - if (attack.targetID === targetId) { - total += attack.troops; - } - } - return total; - } - private updateContestState(nowTickPacked: number) { if (!this.territoryRenderer) { return; @@ -849,7 +838,11 @@ export class TerritoryLayer implements Layer { const neighbors = this.collectNeighborComponents(tile, attacker, defender); let component: ContestComponent; if (neighbors.length === 0) { - component = this.createContestComponent(attacker, defender, nowTickPacked); + component = this.createContestComponent( + attacker, + defender, + nowTickPacked, + ); } else { component = neighbors[0]; for (let i = 1; i < neighbors.length; i++) { @@ -899,13 +892,11 @@ export class TerritoryLayer implements Layer { defender, lastActivityPacked: nowTickPacked, tiles: [], - speed: 0, strength: 0.5, }; this.contestComponents.set(id, component); this.contestActive = true; this.territoryRenderer?.ensureContestTimeCapacity(id); - this.territoryRenderer?.setContestSpeed(id, 0); this.territoryRenderer?.setContestStrength(id, 0.5); return component; } @@ -957,7 +948,6 @@ export class TerritoryLayer implements Layer { this.contestTileIndices![tile] = -1; this.clearContestTile(tile); if (component.tiles.length === 0) { - this.territoryRenderer?.setContestSpeed(component.id, 0); this.territoryRenderer?.setContestStrength(component.id, 0); this.contestComponents.delete(component.id); this.releaseContestComponentId(component.id); @@ -973,10 +963,6 @@ export class TerritoryLayer implements Layer { const sourceSize = source.tiles.length; const totalSize = targetSize + sourceSize; if (totalSize > 0) { - target.speed = Math.min( - 1, - (target.speed * targetSize + source.speed * sourceSize) / totalSize, - ); target.strength = Math.min( 1, (target.strength * targetSize + source.strength * sourceSize) / @@ -1004,7 +990,6 @@ export class TerritoryLayer implements Layer { target.lastActivityPacked, ); this.contestComponents.delete(source.id); - this.territoryRenderer?.setContestSpeed(source.id, 0); this.territoryRenderer?.setContestStrength(source.id, 0); this.releaseContestComponentId(source.id); } @@ -1015,7 +1000,6 @@ export class TerritoryLayer implements Layer { this.clearContestTile(tile); } component.tiles.length = 0; - this.territoryRenderer?.setContestSpeed(component.id, 0); this.territoryRenderer?.setContestStrength(component.id, 0); this.contestComponents.delete(component.id); this.releaseContestComponentId(component.id); @@ -1083,7 +1067,6 @@ export class TerritoryLayer implements Layer { } if (maxId > 0) { this.territoryRenderer.ensureContestTimeCapacity(maxId); - this.territoryRenderer.ensureContestSpeedCapacity(maxId); this.territoryRenderer.ensureContestStrengthCapacity(maxId); } for (const component of this.contestComponents.values()) { @@ -1091,7 +1074,6 @@ export class TerritoryLayer implements Layer { component.id, component.lastActivityPacked, ); - this.territoryRenderer.setContestSpeed(component.id, component.speed); this.territoryRenderer.setContestStrength( component.id, component.strength, diff --git a/src/client/graphics/layers/TerritoryWebGLRenderer.ts b/src/client/graphics/layers/TerritoryWebGLRenderer.ts index de79fde7f..579dc0637 100644 --- a/src/client/graphics/layers/TerritoryWebGLRenderer.ts +++ b/src/client/graphics/layers/TerritoryWebGLRenderer.ts @@ -40,7 +40,6 @@ export class TerritoryWebGLRenderer { private readonly contestOwnersTexture: WebGLTexture | null; private readonly contestIdsTexture: WebGLTexture | null; private readonly contestTimesTexture: WebGLTexture | null; - private readonly contestSpeedsTexture: WebGLTexture | null; private readonly contestStrengthsTexture: WebGLTexture | null; private readonly prevOwnerTexture: WebGLTexture | null; private readonly olderOwnerTexture: WebGLTexture | null; @@ -93,7 +92,6 @@ export class TerritoryWebGLRenderer { contestOwners: WebGLUniformLocation | null; contestIds: WebGLUniformLocation | null; contestTimes: WebGLUniformLocation | null; - contestSpeeds: WebGLUniformLocation | null; contestStrengths: WebGLUniformLocation | null; jfaAvailable: WebGLUniformLocation | null; contestNow: WebGLUniformLocation | null; @@ -134,14 +132,12 @@ export class TerritoryWebGLRenderer { private contestOwnersState: Uint16Array; private contestIdsState: Uint16Array; private contestTimesState: Uint16Array; - private contestSpeedsState: Uint16Array; private contestStrengthsState: Uint16Array; private readonly dirtyRows: Map = new Map(); private readonly contestDirtyRows: Map = new Map(); private needsFullUpload = true; private needsContestFullUpload = true; private needsContestTimesUpload = true; - private needsContestSpeedsUpload = true; private needsContestStrengthsUpload = true; private alternativeView = false; private paletteWidth = 0; @@ -188,7 +184,6 @@ export class TerritoryWebGLRenderer { this.contestOwnersState = new Uint16Array(state.length * 2); this.contestIdsState = new Uint16Array(state.length); this.contestTimesState = new Uint16Array(1); - this.contestSpeedsState = new Uint16Array(1); this.contestStrengthsState = new Uint16Array(1); this.gl = this.canvas.getContext("webgl2", { @@ -210,7 +205,6 @@ export class TerritoryWebGLRenderer { this.contestOwnersTexture = null; this.contestIdsTexture = null; this.contestTimesTexture = null; - this.contestSpeedsTexture = null; this.contestStrengthsTexture = null; this.prevOwnerTexture = null; this.olderOwnerTexture = null; @@ -256,7 +250,6 @@ export class TerritoryWebGLRenderer { contestOwners: null, contestIds: null, contestTimes: null, - contestSpeeds: null, contestStrengths: null, jfaAvailable: null, contestNow: null, @@ -301,7 +294,6 @@ export class TerritoryWebGLRenderer { this.contestOwnersTexture = null; this.contestIdsTexture = null; this.contestTimesTexture = null; - this.contestSpeedsTexture = null; this.contestStrengthsTexture = null; this.prevOwnerTexture = null; this.olderOwnerTexture = null; @@ -347,7 +339,6 @@ export class TerritoryWebGLRenderer { contestOwners: null, contestIds: null, contestTimes: null, - contestSpeeds: null, contestStrengths: null, jfaAvailable: null, contestNow: null, @@ -439,7 +430,6 @@ export class TerritoryWebGLRenderer { contestOwners: gl.getUniformLocation(this.program, "u_contestOwners"), contestIds: gl.getUniformLocation(this.program, "u_contestIds"), contestTimes: gl.getUniformLocation(this.program, "u_contestTimes"), - contestSpeeds: gl.getUniformLocation(this.program, "u_contestSpeeds"), contestStrengths: gl.getUniformLocation( this.program, "u_contestStrengths", @@ -540,7 +530,6 @@ export class TerritoryWebGLRenderer { this.contestOwnersTexture = gl.createTexture(); this.contestIdsTexture = gl.createTexture(); this.contestTimesTexture = gl.createTexture(); - this.contestSpeedsTexture = gl.createTexture(); this.contestStrengthsTexture = gl.createTexture(); this.prevOwnerTexture = gl.createTexture(); this.olderOwnerTexture = gl.createTexture(); @@ -654,25 +643,6 @@ export class TerritoryWebGLRenderer { this.contestTimesState, ); - gl.activeTexture(gl.TEXTURE10); - gl.bindTexture(gl.TEXTURE_2D, this.contestSpeedsTexture); - 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.contestSpeedsState.length, - 1, - 0, - gl.RED_INTEGER, - gl.UNSIGNED_SHORT, - this.contestSpeedsState, - ); - gl.activeTexture(gl.TEXTURE11); gl.bindTexture(gl.TEXTURE_2D, this.contestStrengthsTexture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); @@ -998,7 +968,6 @@ export class TerritoryWebGLRenderer { gl.uniform1i(this.uniforms.contestOwners, 4); gl.uniform1i(this.uniforms.contestIds, 5); gl.uniform1i(this.uniforms.contestTimes, 6); - gl.uniform1i(this.uniforms.contestSpeeds, 10); gl.uniform1i(this.uniforms.contestStrengths, 11); gl.uniform1i(this.uniforms.prevOwner, 7); gl.uniform1i(this.uniforms.jfaSeedsOld, 8); @@ -1336,34 +1305,6 @@ export class TerritoryWebGLRenderer { this.needsContestTimesUpload = true; } - setContestSpeed(componentId: number, speed: number) { - if (componentId <= 0) { - return; - } - this.ensureContestSpeedCapacity(componentId); - const clamped = Math.max(0, Math.min(1, speed)); - const packed = Math.round(clamped * 65535) & 0xffff; - if (this.contestSpeedsState[componentId] === packed) { - return; - } - this.contestSpeedsState[componentId] = packed; - this.needsContestSpeedsUpload = true; - } - - ensureContestSpeedCapacity(componentId: number) { - if (componentId < this.contestSpeedsState.length) { - return; - } - let nextLength = Math.max(1, this.contestSpeedsState.length); - while (nextLength <= componentId) { - nextLength *= 2; - } - const nextState = new Uint16Array(nextLength); - nextState.set(this.contestSpeedsState); - this.contestSpeedsState = nextState; - this.needsContestSpeedsUpload = true; - } - setContestStrength(componentId: number, strength: number) { if (componentId <= 0) { return; @@ -1544,7 +1485,6 @@ export class TerritoryWebGLRenderer { this.dirtyRows.clear(); this.needsContestFullUpload = true; this.needsContestTimesUpload = true; - this.needsContestSpeedsUpload = true; this.needsContestStrengthsUpload = true; this.contestDirtyRows.clear(); this.jfaDirty = true; @@ -1582,13 +1522,6 @@ export class TerritoryWebGLRenderer { uploadContestTimesSpan, ); - const uploadContestSpeedsSpan = FrameProfiler.start(); - this.uploadContestSpeedsTexture(); - FrameProfiler.end( - "TerritoryWebGLRenderer:uploadContestSpeeds", - uploadContestSpeedsSpan, - ); - const uploadContestStrengthsSpan = FrameProfiler.start(); this.uploadContestStrengthsTexture(); FrameProfiler.end( @@ -1684,10 +1617,6 @@ export class TerritoryWebGLRenderer { gl.activeTexture(gl.TEXTURE13); gl.bindTexture(gl.TEXTURE_2D, changeMaskTexture); } - if (this.contestSpeedsTexture) { - gl.activeTexture(gl.TEXTURE10); - gl.bindTexture(gl.TEXTURE_2D, this.contestSpeedsTexture); - } if (this.contestStrengthsTexture) { gl.activeTexture(gl.TEXTURE11); gl.bindTexture(gl.TEXTURE_2D, this.contestStrengthsTexture); @@ -1960,34 +1889,6 @@ export class TerritoryWebGLRenderer { return { rows: 1, bytes }; } - private uploadContestSpeedsTexture(): { rows: number; bytes: number } { - if (!this.gl || !this.contestSpeedsTexture) { - return { rows: 0, bytes: 0 }; - } - if (!this.needsContestSpeedsUpload) { - return { rows: 0, bytes: 0 }; - } - const gl = this.gl; - gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); - gl.activeTexture(gl.TEXTURE10); - gl.bindTexture(gl.TEXTURE_2D, this.contestSpeedsTexture); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.R16UI, - this.contestSpeedsState.length, - 1, - 0, - gl.RED_INTEGER, - gl.UNSIGNED_SHORT, - this.contestSpeedsState, - ); - this.needsContestSpeedsUpload = false; - const bytes = - this.contestSpeedsState.length * Uint16Array.BYTES_PER_ELEMENT; - return { rows: 1, bytes }; - } - private uploadContestStrengthsTexture(): { rows: number; bytes: number } { if (!this.gl || !this.contestStrengthsTexture) { return { rows: 0, bytes: 0 }; @@ -2712,7 +2613,6 @@ export class TerritoryWebGLRenderer { uniform usampler2D u_contestOwners; uniform usampler2D u_contestIds; uniform usampler2D u_contestTimes; - uniform usampler2D u_contestSpeeds; uniform usampler2D u_contestStrengths; uniform bool u_jfaAvailable; uniform int u_contestNow; @@ -2800,18 +2700,6 @@ export class TerritoryWebGLRenderer { return texelFetch(u_contestIds, clamped, 0).r; } - float contestSpeed(uint contestId) { - if (contestId == 0u) { - return 0.0; - } - uint speedRaw = texelFetch( - u_contestSpeeds, - ivec2(int(contestId), 0), - 0 - ).r; - return clamp(float(speedRaw) / 65535.0, 0.0, 1.0); - } - float contestStrength(uint contestId) { if (contestId == 0u) { return 0.5; @@ -3212,18 +3100,18 @@ export class TerritoryWebGLRenderer { if (seedOld.x >= 0.0 && seedNew.x >= 0.0) { float oldDistance = length(seedOld - vec2(texCoord)); float newDistance = length(seedNew - vec2(texCoord)); - float bandWidth = mix(1.6, 0.8, contestSpeed(contestId)); + float battle = clamp(abs(contestStrength(contestId) - 0.5) * 2.0, 0.0, 1.0); + float bandWidth = mix(1.6, 0.9, battle); float frontDistance = min(oldDistance, newDistance); float band = 1.0 - smoothstep(bandWidth, bandWidth + 0.6, frontDistance); - float speed = contestSpeed(contestId); - float scale = mix(0.1, 0.22, speed); - float drift = mix(0.05, 0.18, speed); + float scale = mix(0.1, 0.2, battle); + float drift = mix(0.05, 0.14, battle); vec2 p = vec2(texCoord) * scale + vec2(u_time * drift, -u_time * drift * 0.6); float n = fbm(p); float cloud = smoothstep(0.55, 0.82, n); - float intensity = mix(0.08, 0.28, speed); + float intensity = mix(0.06, 0.22, battle); float alpha = cloud * band * intensity; vec3 smoke = vec3(0.85, 0.83, 0.8); color = mix(color, smoke, alpha);