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
This commit is contained in:
Readixyee
2025-03-26 10:00:37 -07:00
committed by GitHub
parent b8dd53513d
commit 6b07446965
10 changed files with 144 additions and 117 deletions
+6 -2
View File
@@ -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;
+23 -28
View File
@@ -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 &&
+41 -31
View File
@@ -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());
+5 -1
View File
@@ -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(
+16 -12
View File
@@ -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 {
+17 -11
View File
@@ -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<number>();
@@ -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 {
+2 -3
View File
@@ -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;
}
@@ -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<Unit | UnitView>[][];
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<Unit | UnitView> {
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<Unit | UnitView> = [];
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 });
}
}
}
}
+1 -3
View File
@@ -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`,