From 2dad1dc9a6309a6245501722a90e255bd5538c70 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Thu, 19 Jun 2025 15:58:27 -0400 Subject: [PATCH] Record MIRV warhead intercepted stats, perf improvements (#1220) ## Description: - Record MIRV warhead intercepted stats. - Refactor `nearbyUnits()` to accept a predicate, and combine related unnecessary `filter()` and `map()` calls. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> Co-authored-by: evanpelle --- src/client/ClientGameRunner.ts | 5 +- src/client/graphics/layers/UnitInfoModal.ts | 9 +- src/core/configuration/DefaultConfig.ts | 9 +- src/core/execution/SAMLauncherExecution.ts | 98 +++++++++++---------- src/core/execution/SAMMissileExecution.ts | 6 +- src/core/game/Game.ts | 1 + src/core/game/GameView.ts | 8 +- src/core/game/PlayerImpl.ts | 6 +- src/core/game/Stats.ts | 2 +- src/core/game/StatsImpl.ts | 4 +- src/core/game/UnitGrid.ts | 17 ++-- tests/Stats.test.ts | 2 +- 12 files changed, 89 insertions(+), 78 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 80771b257..dc49b8718 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -528,11 +528,10 @@ export class ClientGameRunner { UnitType.TradeShip, UnitType.TransportShip, ]) - .sort((a, b) => a.distSquared - b.distSquared) - .map((u) => u.unit); + .sort((a, b) => a.distSquared - b.distSquared); if (units.length > 0) { - this.gameView.setFocusedPlayer(units[0].owner() as PlayerView); + this.gameView.setFocusedPlayer(units[0].unit.owner() as PlayerView); } else { this.gameView.setFocusedPlayer(null); } diff --git a/src/client/graphics/layers/UnitInfoModal.ts b/src/client/graphics/layers/UnitInfoModal.ts index 6768205e3..675218b56 100644 --- a/src/client/graphics/layers/UnitInfoModal.ts +++ b/src/client/graphics/layers/UnitInfoModal.ts @@ -53,9 +53,12 @@ export class UnitInfoModal extends LitElement implements Layer { const targetRef = this.game.ref(tileX, tileY); const allUnitTypes = Object.values(UnitType); - const matchingUnits = this.game - .nearbyUnits(targetRef, 10, allUnitTypes) - .filter(({ unit }) => unit.isActive()); + const matchingUnits = this.game.nearbyUnits( + targetRef, + 10, + allUnitTypes, + ({ unit }) => unit.isActive(), + ); if (matchingUnits.length > 0) { matchingUnits.sort((a, b) => a.distSquared - b.distSquared); diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index d9c54bfdc..eaa43d089 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -543,12 +543,11 @@ export class DefaultConfig implements Config { tileToConquer, gm.config().defensePostRange(), UnitType.DefensePost, + ({ unit }) => unit.owner() === defender, )) { - if (dp.unit.owner() === defender) { - mag *= this.defensePostDefenseBonus(); - speed *= this.defensePostDefenseBonus(); - break; - } + mag *= this.defensePostDefenseBonus(); + speed *= this.defensePostDefenseBonus(); + break; } } diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 25518860e..6ba46f1b8 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -39,17 +39,15 @@ export class SAMLauncherExecution implements Execution { private getSingleTarget(): Unit | null { if (this.sam === null) return null; - const nukes = this.mg - .nearbyUnits(this.sam.tile(), this.searchRangeRadius, [ - UnitType.AtomBomb, - UnitType.HydrogenBomb, - ]) - .filter( - ({ unit }) => - unit.owner() !== this.player && - !this.player.isFriendly(unit.owner()) && - unit.isTargetable(), - ); + const nukes = this.mg.nearbyUnits( + this.sam.tile(), + this.searchRangeRadius, + [UnitType.AtomBomb, UnitType.HydrogenBomb], + ({ unit }) => + unit.owner() !== this.player && + !this.player.isFriendly(unit.owner()) && + unit.isTargetable(), + ); return ( nukes.sort((a, b) => { @@ -117,18 +115,13 @@ export class SAMLauncherExecution implements Execution { this.pseudoRandom = new PseudoRandom(this.sam.id()); } - const mirvWarheadTargets = this.mg - .nearbyUnits( - this.sam.tile(), - this.MIRVWarheadSearchRadius, - UnitType.MIRVWarhead, - ) - .map(({ unit }) => unit) - .filter( - (unit) => - unit.owner() !== this.player && !this.player.isFriendly(unit.owner()), - ) - .filter((unit) => { + const mirvWarheadTargets = this.mg.nearbyUnits( + this.sam.tile(), + this.MIRVWarheadSearchRadius, + UnitType.MIRVWarhead, + ({ unit }) => { + if (unit.owner() === this.player) return false; + if (this.player.isFriendly(unit.owner())) return false; const dst = unit.targetTile(); return ( this.sam !== null && @@ -136,7 +129,8 @@ export class SAMLauncherExecution implements Execution { this.mg.manhattanDist(dst, this.sam.tile()) < this.MIRVWarheadProtectionRadius ); - }); + }, + ); let target: Unit | null = null; if (mirvWarheadTargets.length === 0) { @@ -160,31 +154,41 @@ export class SAMLauncherExecution implements Execution { MessageType.SAM_MISS, this.sam.owner().id(), ); - } else { - if (mirvWarheadTargets.length > 0) { - // Message - this.mg.displayMessage( - `${mirvWarheadTargets.length} MIRV warheads intercepted`, - MessageType.SAM_HIT, - this.sam.owner().id(), - ); + } else if (mirvWarheadTargets.length > 0) { + const samOwner = this.sam.owner(); + + // Message + this.mg.displayMessage( + `${mirvWarheadTargets.length} MIRV warheads intercepted`, + MessageType.SAM_HIT, + samOwner.id(), + ); + + mirvWarheadTargets.forEach(({ unit: u }) => { // Delete warheads - mirvWarheadTargets.forEach((u) => { - u.delete(); - }); - } else if (target !== null) { - target.setTargetedBySAM(true); - this.mg.addExecution( - new SAMMissileExecution( - this.sam.tile(), - this.sam.owner(), - this.sam, - target, - ), + u.delete(); + }); + + // Record stats + this.mg + .stats() + .bombIntercept( + samOwner, + UnitType.MIRVWarhead, + mirvWarheadTargets.length, ); - } else { - throw new Error("target is null"); - } + } else if (target !== null) { + target.setTargetedBySAM(true); + this.mg.addExecution( + new SAMMissileExecution( + this.sam.tile(), + this.sam.owner(), + this.sam, + target, + ), + ); + } else { + throw new Error("target is null"); } } diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts index c4db6d622..2d94a0518 100644 --- a/src/core/execution/SAMMissileExecution.ts +++ b/src/core/execution/SAMMissileExecution.ts @@ -72,11 +72,7 @@ export class SAMMissileExecution implements Execution { // Record stats this.mg .stats() - .bombIntercept( - this._owner, - this.target.owner(), - this.target.type() as NukeType, - ); + .bombIntercept(this._owner, this.target.type() as NukeType, 1); return; } else { this.SAMMissile.move(result); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index d893494b5..4f1962c26 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -602,6 +602,7 @@ export interface Game extends GameMap { tile: TileRef, searchRange: number, types: UnitType | UnitType[], + predicate?: (value: { unit: Unit; distSquared: number }) => boolean, ): Array<{ unit: Unit; distSquared: number }>; addExecution(...exec: Execution[]): void; diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index a3f8527cb..d0afe9101 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -391,8 +391,14 @@ export class GameView implements GameMap { tile: TileRef, searchRange: number, types: UnitType | UnitType[], + predicate?: (value: { unit: UnitView; distSquared: number }) => boolean, ): Array<{ unit: UnitView; distSquared: number }> { - return this.unitGrid.nearbyUnits(tile, searchRange, types) as Array<{ + return this.unitGrid.nearbyUnits( + tile, + searchRange, + types, + predicate, + ) as Array<{ unit: UnitView; distSquared: number; }>; diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 3cbc07ae0..9e71d4c22 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -897,9 +897,7 @@ export class PlayerImpl implements Player { return this.mg.config().unitInfo(unitTypeValue).territoryBound; }); - const nearbyUnits = this.mg - .nearbyUnits(tile, searchRadius * 2, types) - .map((u) => u.unit); + const nearbyUnits = this.mg.nearbyUnits(tile, searchRadius * 2, types); const nearbyTiles = this.mg.bfs(tile, (gm, t) => { return ( this.mg.euclideanDistSquared(tile, t) < searchRadiusSquared && @@ -910,7 +908,7 @@ export class PlayerImpl implements Player { const minDistSquared = this.mg.config().structureMinDist() ** 2; for (const t of nearbyTiles) { - for (const unit of nearbyUnits) { + for (const { unit } of nearbyUnits) { if (this.mg.euclideanDistSquared(unit.tile(), t) < minDistSquared) { validSet.delete(t); break; diff --git a/src/core/game/Stats.ts b/src/core/game/Stats.ts index c3af26c7e..d328c6d71 100644 --- a/src/core/game/Stats.ts +++ b/src/core/game/Stats.ts @@ -71,7 +71,7 @@ export interface Stats { bombLand(player: Player, target: Player | TerraNullius, type: NukeType): void; // Player's SAM intercepts a bomb from attacker - bombIntercept(player: Player, attacker: Player, type: NukeType): void; + bombIntercept(player: Player, type: NukeType, count: number | bigint): void; // Player earns gold from conquering tiles or trade ships from captured goldWar(player: Player, captured: Player, gold: number | bigint): void; diff --git a/src/core/game/StatsImpl.ts b/src/core/game/StatsImpl.ts index 88469b7b8..801a17cf0 100644 --- a/src/core/game/StatsImpl.ts +++ b/src/core/game/StatsImpl.ts @@ -215,8 +215,8 @@ export class StatsImpl implements Stats { this._addBomb(player, type, BOMB_INDEX_LAND, 1); } - bombIntercept(player: Player, attacker: Player, type: NukeType): void { - this._addBomb(player, type, BOMB_INDEX_INTERCEPT, 1); + bombIntercept(player: Player, type: NukeType, count: BigIntLike): void { + this._addBomb(player, type, BOMB_INDEX_INTERCEPT, count); } goldWork(player: Player, gold: BigIntLike): void { diff --git a/src/core/game/UnitGrid.ts b/src/core/game/UnitGrid.ts index af0bb7084..af2a4b26d 100644 --- a/src/core/game/UnitGrid.ts +++ b/src/core/game/UnitGrid.ts @@ -96,6 +96,10 @@ export class UnitGrid { tile: TileRef, searchRange: number, types: UnitType | UnitType[], + predicate?: (value: { + unit: Unit | UnitView; + distSquared: number; + }) => boolean, ): Array<{ unit: Unit | UnitView; distSquared: number }> { const nearby: Array<{ unit: Unit | UnitView; distSquared: number }> = []; const { startGridX, endGridX, startGridY, endGridY } = this.getCellsInRange( @@ -107,12 +111,13 @@ export class UnitGrid { for (let cy = startGridY; cy <= endGridY; cy++) { for (let cx = startGridX; cx <= endGridX; cx++) { for (const unit of this.grid[cy][cx]) { - if (typeSet.has(unit.type()) && unit.isActive()) { - const distSquared = this.squaredDistanceFromTile(unit, tile); - if (distSquared <= rangeSquared) { - nearby.push({ unit, distSquared }); - } - } + if (!typeSet.has(unit.type())) continue; + if (!unit.isActive()) continue; + const distSquared = this.squaredDistanceFromTile(unit, tile); + if (distSquared > rangeSquared) continue; + const value = { unit, distSquared }; + if (predicate !== undefined && !predicate(value)) continue; + nearby.push(value); } } } diff --git a/tests/Stats.test.ts b/tests/Stats.test.ts index 46b3b1b36..819d895e4 100644 --- a/tests/Stats.test.ts +++ b/tests/Stats.test.ts @@ -163,7 +163,7 @@ describe("Stats", () => { }); test("bombIntercept", () => { - stats.bombIntercept(player1, player2, UnitType.MIRVWarhead); + stats.bombIntercept(player1, UnitType.MIRVWarhead, 1); expect(stats.stats()).toStrictEqual({ client1: { bombs: { mirvw: [0n, 0n, 1n] } }, });