From fbdc54c01abdf74b330c964e3d05a3192feb1174 Mon Sep 17 00:00:00 2001 From: ralfisalhon Date: Mon, 16 Mar 2026 23:28:10 +0000 Subject: [PATCH] Batch attack cluster positions into one worker request per tick; raise z-index above names --- .../graphics/layers/TroopAdvantageLayer.ts | 109 ++++++++---------- src/core/GameRunner.ts | 18 +-- src/core/game/GameView.ts | 4 +- src/core/worker/Worker.worker.ts | 6 +- src/core/worker/WorkerClient.ts | 12 +- src/core/worker/WorkerMessages.ts | 4 +- 6 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/client/graphics/layers/TroopAdvantageLayer.ts b/src/client/graphics/layers/TroopAdvantageLayer.ts index b435f51cf..81ed332b5 100644 --- a/src/client/graphics/layers/TroopAdvantageLayer.ts +++ b/src/client/graphics/layers/TroopAdvantageLayer.ts @@ -1,6 +1,6 @@ import { EventBus } from "../../../core/EventBus"; import { Cell } from "../../../core/game/Game"; -import { GameView, PlayerView } from "../../../core/game/GameView"; +import { GameView } from "../../../core/game/GameView"; import { UserSettings } from "../../../core/game/UserSettings"; import { AlternateViewEvent } from "../../InputHandler"; import { renderTroops } from "../../Utils"; @@ -26,12 +26,14 @@ interface AttackLabel { elements: HTMLDivElement[]; positions: (Cell | null)[]; isIncoming: boolean; + attackerTroops: number; + defenderTroops: number; } export class TroopAdvantageLayer implements Layer { private container: HTMLDivElement; private labels = new Map(); - private inFlightPositionRequests = new Set(); + private inFlightRequest = false; private isVisible = true; private onAlternateView: (e: AlternateViewEvent) => void; @@ -52,7 +54,7 @@ export class TroopAdvantageLayer implements Layer { this.container.style.left = "50%"; this.container.style.top = "50%"; this.container.style.pointerEvents = "none"; - this.container.style.zIndex = "2"; + this.container.style.zIndex = "4"; document.body.appendChild(this.container); this.onAlternateView = (e) => { @@ -110,15 +112,7 @@ export class TroopAdvantageLayer implements Layer { this.removeLabel(attack.id); continue; } - - this.processAttack( - myPlayer, - attack.id, - attack.attackerID, - attack.troops, - defender.troops(), - false, - ); + this.ensureLabel(attack.id, attack.troops, defender.troops(), false); } // Incoming attacks — red if attacker > my troops, orange if attacker < my troops @@ -128,66 +122,65 @@ export class TroopAdvantageLayer implements Layer { this.removeLabel(attack.id); continue; } - - this.processAttack( - myPlayer, - attack.id, - attack.attackerID, - attack.troops, - myTroops, - true, - ); + this.ensureLabel(attack.id, attack.troops, myTroops, true); } + + // Single request per tick for all attack cluster positions + if (this.inFlightRequest) return; + this.inFlightRequest = true; + + void myPlayer + .attackClusterPositions(myPlayer.smallID()) + .then((attacks) => { + for (const { id, clusters } of attacks) { + const lbl = this.labels.get(id); + if (!lbl) continue; + + while (lbl.elements.length < clusters.length) { + lbl.elements.push( + this.createLabelElement( + lbl.attackerTroops, + lbl.defenderTroops, + lbl.isIncoming, + ), + ); + lbl.positions.push(null); + } + while (lbl.elements.length > clusters.length) { + lbl.elements.pop()!.remove(); + lbl.positions.pop(); + } + + for (let i = 0; i < clusters.length; i++) { + lbl.positions[i] = clusters[i]; + } + } + }) + .catch(() => { + for (const lbl of this.labels.values()) lbl.positions.fill(null); + }) + .finally(() => { + this.inFlightRequest = false; + }); } - private processAttack( - myPlayer: PlayerView, + private ensureLabel( attackID: string, - attackerSmallID: number, attackerTroops: number, defenderTroops: number, isIncoming: boolean, ) { let label = this.labels.get(attackID); if (!label) { - label = { elements: [], positions: [], isIncoming }; + label = { elements: [], positions: [], isIncoming, attackerTroops, defenderTroops }; this.labels.set(attackID, label); + } else { + label.attackerTroops = attackerTroops; + label.defenderTroops = defenderTroops; } for (const el of label.elements) { this.updateLabelContent(el, attackerTroops, defenderTroops, isIncoming); } - - if (this.inFlightPositionRequests.has(attackID)) return; - this.inFlightPositionRequests.add(attackID); - - void myPlayer - .attackClusterPositions(attackerSmallID, attackID) - .then((clusters) => { - const lbl = this.labels.get(attackID); - if (!lbl) return; - - while (lbl.elements.length < clusters.length) { - lbl.elements.push( - this.createLabelElement(attackerTroops, defenderTroops, isIncoming), - ); - lbl.positions.push(null); - } - while (lbl.elements.length > clusters.length) { - lbl.elements.pop()!.remove(); - lbl.positions.pop(); - } - - for (let i = 0; i < clusters.length; i++) { - lbl.positions[i] = clusters[i]; - } - }) - .catch(() => { - const lbl = this.labels.get(attackID); - if (lbl) lbl.positions.fill(null); - }) - .finally(() => { - this.inFlightPositionRequests.delete(attackID); - }); } renderLayer(_context: CanvasRenderingContext2D) { @@ -263,7 +256,6 @@ export class TroopAdvantageLayer implements Layer { if (!label) return; for (const el of label.elements) el.remove(); this.labels.delete(attackID); - this.inFlightPositionRequests.delete(attackID); } private clearAllLabels() { @@ -271,6 +263,5 @@ export class TroopAdvantageLayer implements Layer { for (const el of label.elements) el.remove(); } this.labels.clear(); - this.inFlightPositionRequests.clear(); } } diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index e3742a7c8..b41d6ad90 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -256,20 +256,22 @@ export class GameRunner { public attackClusterPositions( playerID: number, - attackID: string, - ): { x: number; y: number }[] { + attackID?: string, + ): { id: string; clusters: { x: number; y: number }[] }[] { const player = this.game.playerBySmallID(playerID); if (!player.isPlayer()) { throw new Error(`player with id ${playerID} not found`); } - const condition = (a: Attack) => a.id() === attackID; - const attack = - player.outgoingAttacks().find(condition) ?? - player.incomingAttacks().find(condition); - if (attack === undefined) return []; + const allAttacks = [ + ...player.outgoingAttacks(), + ...player.incomingAttacks(), + ]; + const attacks = attackID + ? allAttacks.filter((a) => a.id() === attackID) + : allAttacks; - return attack.clusterPositions(); + return attacks.map((a) => ({ id: a.id(), clusters: a.clusterPositions() })); } public attackAveragePosition( diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 7215c6a31..e5abfcc7e 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -462,8 +462,8 @@ export class PlayerView { async attackClusterPositions( playerID: number, - attackID: string, - ): Promise { + attackID?: string, + ): Promise<{ id: string; clusters: Cell[] }[]> { return this.game.worker.attackClusterPositions(playerID, attackID); } diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts index b2add8455..c109041c8 100644 --- a/src/core/worker/Worker.worker.ts +++ b/src/core/worker/Worker.worker.ts @@ -271,21 +271,21 @@ ctx.addEventListener("message", async (e: MessageEvent) => { } try { - const clusters = (await gameRunner).attackClusterPositions( + const attacks = (await gameRunner).attackClusterPositions( message.playerID, message.attackID, ); sendMessage({ type: "attack_cluster_positions_result", id: message.id, - clusters, + attacks, } as AttackClusterPositionsResultMessage); } catch (error) { console.error("Failed to get attack cluster positions:", error); sendMessage({ type: "attack_cluster_positions_result", id: message.id, - clusters: [], + attacks: [], } as AttackClusterPositionsResultMessage); } break; diff --git a/src/core/worker/WorkerClient.ts b/src/core/worker/WorkerClient.ts index 8726e2a46..440954e84 100644 --- a/src/core/worker/WorkerClient.ts +++ b/src/core/worker/WorkerClient.ts @@ -266,7 +266,10 @@ export class WorkerClient { }); } - attackClusterPositions(playerID: number, attackID: string): Promise { + attackClusterPositions( + playerID: number, + attackID?: string, + ): Promise<{ id: string; clusters: Cell[] }[]> { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Worker not initialized")); @@ -277,7 +280,12 @@ export class WorkerClient { this.messageHandlers.set(messageId, (message) => { if (message.type === "attack_cluster_positions_result") { - resolve(message.clusters.map((c) => new Cell(c.x, c.y))); + resolve( + message.attacks.map((a) => ({ + id: a.id, + clusters: a.clusters.map((c) => new Cell(c.x, c.y)), + })), + ); } }); diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts index eef76eac5..557e3a550 100644 --- a/src/core/worker/WorkerMessages.ts +++ b/src/core/worker/WorkerMessages.ts @@ -125,12 +125,12 @@ export interface AttackAveragePositionResultMessage extends BaseWorkerMessage { export interface AttackClusterPositionsMessage extends BaseWorkerMessage { type: "attack_cluster_positions"; playerID: number; - attackID: string; + attackID?: string; } export interface AttackClusterPositionsResultMessage extends BaseWorkerMessage { type: "attack_cluster_positions_result"; - clusters: { x: number; y: number }[]; + attacks: { id: string; clusters: { x: number; y: number }[] }[]; } export interface TransportShipSpawnMessage extends BaseWorkerMessage {