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.
This commit is contained in:
scamiv
2026-01-11 23:01:42 +01:00
parent c576c512b4
commit 370d8eec7b
2 changed files with 86 additions and 216 deletions
+81 -99
View File
@@ -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<number, number>();
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<string, number>();
const involvedIds = new Set<number>();
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<number, number>();
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<number>,
): Map<number, number> {
const totals = new Map<number, number>();
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<number>,
): Map<number, Map<number, number>> {
const totals = new Map<number, Map<number, number>>();
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<number, number>();
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<number, number>,
attackTroopsById: Map<number, Map<number, number>>,
) {
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,
@@ -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<number, DirtySpan> = new Map();
private readonly contestDirtyRows: Map<number, DirtySpan> = 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);