From 6b0744696533956a8f13c81e7aadb17d1c272fc1 Mon Sep 17 00:00:00 2001 From: Readixyee <49241765+Readixyee@users.noreply.github.com> Date: Wed, 26 Mar 2025 18:00:37 +0100 Subject: [PATCH] unitgrid instead of defensepostgrid (#265) added a unitgrid instead of a defensepostgrid for all units to be able to see if a tile is inside the range of a unit by a custom distance this is used for the sam launcher to draw a circle around the area it protects removed all parts that use the defensepostgrid and made it use the unitgrid instead --- src/client/graphics/layers/TerritoryLayer.ts | 9 ++- src/core/configuration/DefaultConfig.ts | 8 ++- src/core/execution/SAMLauncherExecution.ts | 51 ++++++------- src/core/execution/WarshipExecution.ts | 72 +++++++++++-------- src/core/game/Game.ts | 6 +- src/core/game/GameImpl.ts | 28 ++++---- src/core/game/GameView.ts | 28 +++++--- src/core/game/PlayerImpl.ts | 5 +- .../game/{DefensePostGrid.ts => UnitGrid.ts} | 50 ++++++------- src/core/game/UnitImpl.ts | 4 +- 10 files changed, 144 insertions(+), 117 deletions(-) rename src/core/game/{DefensePostGrid.ts => UnitGrid.ts} (63%) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index a46d17ee84..bd9be5383e 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -240,8 +240,13 @@ export class TerritoryLayer implements Layer { const owner = this.game.owner(tile) as PlayerView; if (this.game.isBorder(tile)) { if ( - this.game.nearbyDefenses(tile).filter((u) => u.owner() == owner) - .length > 0 + this.game + .nearbyUnits( + tile, + this.game.config().defensePostRange(), + UnitType.DefensePost, + ) + .filter((u) => u.unit.owner() == owner).length > 0 ) { this.paintCell( this.game.x(tile), diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 2bdc9ed02e..56d6c4bd5d 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -381,8 +381,12 @@ export class DefaultConfig implements Config { throw new Error(`terrain type ${type} not supported`); } if (defender.isPlayer()) { - for (const dp of gm.nearbyDefensePosts(tileToConquer)) { - if (dp.owner() == defender) { + for (const dp of gm.nearbyUnits( + tileToConquer, + gm.config().defensePostRange(), + UnitType.DefensePost, + )) { + if (dp.unit.owner() == defender) { mag *= this.defensePostDefenseBonus(); speed *= this.defensePostDefenseBonus(); break; diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 810e9a5e6e..be5d4cfe75 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -62,41 +62,36 @@ export class SAMLauncherExecution implements Execution { } const nukes = this.mg - .units(UnitType.AtomBomb, UnitType.HydrogenBomb) - .filter((u) => { - // (x - center_x)² + (y - center_y)² < radius² - const x = this.mg.x(u.tile()); - const y = this.mg.y(u.tile()); - const centerX = this.mg.x(this.post.tile()); - const centerY = this.mg.y(this.post.tile()); - const isInRange = - (x - centerX) ** 2 + (y - centerY) ** 2 < this.searchRangeRadius ** 2; - return isInRange; - }) - .filter((u) => u.owner() !== this.player) - .filter((u) => !u.owner().isAlliedWith(this.player)); + .nearbyUnits(this.post.tile(), this.searchRangeRadius, [ + UnitType.AtomBomb, + UnitType.HydrogenBomb, + ]) + .filter( + ({ unit }) => + unit.owner() !== this.player && + !unit.owner().isAlliedWith(this.player), + ); this.target = nukes.sort((a, b) => { - // Prioritize HydrogenBombs first + const { unit: unitA, distSquared: distA } = a; + const { unit: unitB, distSquared: distB } = b; + + // Prioritize Hydrogen Bombs if ( - a.type() === UnitType.HydrogenBomb && - b.type() !== UnitType.HydrogenBomb - ) { + unitA.type() === UnitType.HydrogenBomb && + unitB.type() !== UnitType.HydrogenBomb + ) return -1; - } if ( - a.type() !== UnitType.HydrogenBomb && - b.type() === UnitType.HydrogenBomb - ) { + unitA.type() !== UnitType.HydrogenBomb && + unitB.type() === UnitType.HydrogenBomb + ) return 1; - } - // If both are the same type, sort by distance - return ( - this.mg.manhattanDist(this.post.tile(), a.tile()) - - this.mg.manhattanDist(this.post.tile(), b.tile()) - ); - })[0] ?? null; + + // If both are the same type, sort by distance (lower `distSquared` means closer) + return distA - distB; + })[0]?.unit ?? null; const cooldown = this.lastMissileAttack != 0 && diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts index 3b19021efa..36fe7a8a9d 100644 --- a/src/core/execution/WarshipExecution.ts +++ b/src/core/execution/WarshipExecution.ts @@ -138,44 +138,54 @@ export class WarshipExecution implements Execution { } const hasPort = this._owner.units(UnitType.Port).length > 0; const ships = this.mg - .units(UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip) - .filter((u) => this.mg.manhattanDist(u.tile(), this.warship.tile()) < 130) - .filter((u) => u.owner() != this.warship.owner()) - .filter((u) => u != this.warship) - .filter((u) => !u.owner().isAlliedWith(this.warship.owner())) - .filter((u) => !this.alreadySentShell.has(u)) - // Do not target trade ships if we don't have port - .filter((u) => u.type() != UnitType.TradeShip || hasPort) - .filter((u) => { - const portOwner = u.dstPort() ? u.dstPort().owner() : null; - return u.type() != UnitType.TradeShip || portOwner != this.owner(); - }); + .nearbyUnits( + this.warship.tile(), + 130, // Search range + [UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip], + ) + .filter( + ({ unit }) => + unit.owner() !== this.warship.owner() && + unit !== this.warship && + !unit.owner().isAlliedWith(this.warship.owner()) && + !this.alreadySentShell.has(unit) && + (unit.type() !== UnitType.TradeShip || hasPort) && + (unit.type() !== UnitType.TradeShip || + unit.dstPort()?.owner() !== this.owner()), + ); this.target = ships.sort((a, b) => { - // First compare by Warship type - if (a.type() === UnitType.Warship && b.type() !== UnitType.Warship) { - return -1; - } - if (a.type() !== UnitType.Warship && b.type() === UnitType.Warship) { - return 1; - } - // Then favor transport ship + const { unit: unitA, distSquared: distA } = a; + const { unit: unitB, distSquared: distB } = b; + + // Prioritize Warships if ( - a.type() === UnitType.TransportShip && - b.type() !== UnitType.TransportShip - ) { + unitA.type() === UnitType.Warship && + unitB.type() !== UnitType.Warship + ) return -1; - } if ( - a.type() !== UnitType.TransportShip && - b.type() === UnitType.TransportShip - ) { + unitA.type() !== UnitType.Warship && + unitB.type() === UnitType.Warship + ) return 1; - } - // If both are same type, sort by distance - return distSortUnit(this.mg, this.warship)(a, b); - })[0] ?? null; + + // Then favor Transport Ships over Trade Ships + if ( + unitA.type() === UnitType.TransportShip && + unitB.type() !== UnitType.TransportShip + ) + return -1; + if ( + unitA.type() !== UnitType.TransportShip && + unitB.type() === UnitType.TransportShip + ) + return 1; + + // If both are the same type, sort by distance (lower `distSquared` means closer) + return distA - distB; + })[0]?.unit ?? null; if (this.warship.moveTarget()) { this.goToMoveTarget(this.warship.moveTarget()); diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index f9a99d94b4..215dc8f8b2 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -411,7 +411,11 @@ export interface Game extends GameMap { // Units units(...types: UnitType[]): Unit[]; unitInfo(type: UnitType): UnitInfo; - nearbyDefensePosts(tile: TileRef): Unit[]; + nearbyUnits( + tile: TileRef, + searchRange: number, + types: UnitType | UnitType[], + ): Array<{ unit: Unit; distSquared: number }>; addExecution(...exec: Execution[]): void; displayMessage( diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index bdb6819e63..e80d0bc723 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -29,7 +29,7 @@ import { MessageType } from "./Game"; import { UnitImpl } from "./UnitImpl"; import { consolex } from "../Consolex"; import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; -import { DefenseGrid } from "./DefensePostGrid"; +import { UnitGrid } from "./UnitGrid"; import { StatsImpl } from "./StatsImpl"; import { Stats } from "./Stats"; @@ -66,7 +66,7 @@ export class GameImpl implements Game { private _nextUnitID = 1; private updates: GameUpdates = createGameUpdatesMap(); - private defenseGrid: DefenseGrid; + private unitGrid: UnitGrid; private _stats: StatsImpl = new StatsImpl(); @@ -88,10 +88,7 @@ export class GameImpl implements Game { n.strength, ), ); - this.defenseGrid = new DefenseGrid( - this._map, - this._config.defensePostRange(), - ); + this.unitGrid = new UnitGrid(this._map); } isOnEdgeOfMap(ref: TileRef): boolean { return this._map.isOnEdgeOfMap(ref); @@ -541,15 +538,22 @@ export class GameImpl implements Game { }); } - addDefensePost(dp: Unit) { - this.defenseGrid.addDefense(dp); + addUnit(u: Unit) { + this.unitGrid.addUnit(u); } - removeDefensePost(dp: Unit) { - this.defenseGrid.removeDefense(dp); + removeUnit(u: Unit) { + this.unitGrid.removeUnit(u); } - nearbyDefensePosts(tile: TileRef): Unit[] { - return this.defenseGrid.nearbyDefenses(tile) as Unit[]; + nearbyUnits( + tile: TileRef, + searchRange: number, + types: UnitType | UnitType[], + ): Array<{ unit: Unit; distSquared: number }> { + return this.unitGrid.nearbyUnits(tile, searchRange, types) as Array<{ + unit: Unit; + distSquared: number; + }>; } ref(x: number, y: number): TileRef { diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 6a04f96ad2..3a4a759363 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -30,8 +30,9 @@ import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { WorkerClient } from "../worker/WorkerClient"; import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; import { GameUpdateViewData } from "./GameUpdates"; -import { DefenseGrid } from "./DefensePostGrid"; +import { UnitGrid } from "./UnitGrid"; import { consolex } from "../Consolex"; +import { SAMLauncherExecution } from "../execution/SAMLauncherExecution"; export class UnitView { public _wasUpdated = true; @@ -254,7 +255,7 @@ export class GameView implements GameMap { private _myPlayer: PlayerView | null = null; - private defensePostGrid: DefenseGrid; + private unitGrid: UnitGrid; private toDelete = new Set(); @@ -272,7 +273,7 @@ export class GameView implements GameMap { updates: null, playerNameViewData: {}, }; - this.defensePostGrid = new DefenseGrid(_map, _config.defensePostRange()); + this.unitGrid = new UnitGrid(_map); } isOnEdgeOfMap(ref: TileRef): boolean { return this._map.isOnEdgeOfMap(ref); @@ -318,12 +319,10 @@ export class GameView implements GameMap { unit = new UnitView(this, update); this._units.set(update.id, unit); } - if (update.unitType == UnitType.DefensePost) { - if (update.isActive) { - this.defensePostGrid.addDefense(unit); - } else { - this.defensePostGrid.removeDefense(unit); - } + if (update.isActive) { + this.unitGrid.addUnit(unit); + } else { + this.unitGrid.removeUnit(unit); } if (!unit.isActive()) { // Wait until next tick to delete the unit. @@ -336,8 +335,15 @@ export class GameView implements GameMap { return this.updatedTiles; } - nearbyDefenses(tile: TileRef): UnitView[] { - return this.defensePostGrid.nearbyDefenses(tile) as UnitView[]; + nearbyUnits( + tile: TileRef, + searchRange: number, + types: UnitType | UnitType[], + ): Array<{ unit: UnitView; distSquared: number }> { + return this.unitGrid.nearbyUnits(tile, searchRange, types) as Array<{ + unit: UnitView; + distSquared: number; + }>; } myClientID(): ClientID { diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 162e3c681b..a76c64490b 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -666,9 +666,8 @@ export class PlayerImpl implements Player { this.removeGold(cost); this.removeTroops(troops); this.mg.addUpdate(b.toUpdate()); - if (type == UnitType.DefensePost) { - this.mg.addDefensePost(b); - } + this.mg.addUnit(b); + return b; } diff --git a/src/core/game/DefensePostGrid.ts b/src/core/game/UnitGrid.ts similarity index 63% rename from src/core/game/DefensePostGrid.ts rename to src/core/game/UnitGrid.ts index 32427982c6..79b7b0b329 100644 --- a/src/core/game/DefensePostGrid.ts +++ b/src/core/game/UnitGrid.ts @@ -1,15 +1,12 @@ -import { Unit } from "./Game"; +import { Unit, UnitType } from "./Game"; import { GameMap, TileRef } from "./GameMap"; import { UnitView } from "./GameView"; -export class DefenseGrid { +export class UnitGrid { private grid: Set[][]; private readonly cellSize = 100; - constructor( - private gm: GameMap, - private searchRange: number, - ) { + constructor(private gm: GameMap) { this.grid = Array(Math.ceil(gm.height() / this.cellSize)) .fill(null) .map(() => @@ -24,8 +21,8 @@ export class DefenseGrid { return [Math.floor(x / this.cellSize), Math.floor(y / this.cellSize)]; } - // Add a defense unit to the grid - addDefense(unit: Unit | UnitView) { + // Add a unit to the grid + addUnit(unit: Unit | UnitView) { const tile = unit.tile(); const [gridX, gridY] = this.getGridCoords(this.gm.x(tile), this.gm.y(tile)); @@ -34,8 +31,8 @@ export class DefenseGrid { } } - // Remove a defense unit from the grid - removeDefense(unit: Unit | UnitView) { + // Remove a unit from the grid + removeUnit(unit: Unit | UnitView) { const tile = unit.tile(); const [gridX, gridY] = this.getGridCoords(this.gm.x(tile), this.gm.y(tile)); @@ -53,35 +50,40 @@ export class DefenseGrid { ); } - // Get all defense units within range of a point + // Get all units within range of a point // Returns [unit, distanceSquared] pairs for efficient filtering - nearbyDefenses(tile: TileRef): Array { + nearbyUnits( + tile: TileRef, + searchRange: number, + types: UnitType | UnitType[], + ): Array<{ unit: Unit | UnitView; distSquared: number }> { const x = this.gm.x(tile); const y = this.gm.y(tile); const [gridX, gridY] = this.getGridCoords(x, y); - const cellsToCheck = Math.ceil(this.searchRange / this.cellSize); - const nearby: Array = []; + const cellsToCheck = Math.ceil(searchRange / this.cellSize); + const nearby: Array<{ unit: Unit | UnitView; distSquared: number }> = []; - // Pre-calculate range bounds for efficiency const startGridX = Math.max(0, gridX - cellsToCheck); const endGridX = Math.min(this.grid[0].length - 1, gridX + cellsToCheck); const startGridY = Math.max(0, gridY - cellsToCheck); const endGridY = Math.min(this.grid.length - 1, gridY + cellsToCheck); - // Squared range for faster comparison (avoid sqrt) - const rangeSquared = this.searchRange * this.searchRange; + const rangeSquared = searchRange * searchRange; + const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]); for (let cy = startGridY; cy <= endGridY; cy++) { for (let cx = startGridX; cx <= endGridX; cx++) { for (const unit of this.grid[cy][cx]) { - const tileX = this.gm.x(unit.tile()); - const tileY = this.gm.y(unit.tile()); - const dx = tileX - x; - const dy = tileY - y; - const distSquared = dx * dx + dy * dy; + if (unit.isActive()) { + const tileX = this.gm.x(unit.tile()); + const tileY = this.gm.y(unit.tile()); + const dx = tileX - x; + const dy = tileY - y; + const distSquared = dx * dx + dy * dy; - if (distSquared <= rangeSquared) { - nearby.push(unit); + if (distSquared <= rangeSquared && typeSet.has(unit.type())) { + nearby.push({ unit, distSquared }); + } } } } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index d31e6b111f..cc75026230 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -131,9 +131,7 @@ export class UnitImpl implements Unit { this._owner._units = this._owner._units.filter((b) => b != this); this._active = false; this.mg.addUpdate(this.toUpdate()); - if (this.type() == UnitType.DefensePost) { - this.mg.removeDefensePost(this); - } + this.mg.removeUnit(this); if (displayMessage) { this.mg.displayMessage( `Your ${this.type()} was destroyed`,