diff --git a/src/client/graphics/layers/BuildMenu.ts b/src/client/graphics/layers/BuildMenu.ts index d7201b9a8..e261b42ad 100644 --- a/src/client/graphics/layers/BuildMenu.ts +++ b/src/client/graphics/layers/BuildMenu.ts @@ -16,6 +16,7 @@ import missileSiloIcon from "../../../../resources/images/MissileSiloIconWhite.s import goldCoinIcon from "../../../../resources/images/GoldCoinIcon.svg"; import portIcon from "../../../../resources/images/PortIcon.svg"; import cityIcon from "../../../../resources/images/CityIconWhite.svg"; +import shieldIcon from "../../../../resources/images/ShieldIconWhite.svg"; import { renderNumber } from "../../Utils"; import { GameView, PlayerView } from "../../../core/game/GameView"; @@ -31,7 +32,7 @@ const buildTable: BuildItemDisplay[][] = [ { unitType: UnitType.Warship, icon: warshipIcon }, { unitType: UnitType.Port, icon: portIcon }, { unitType: UnitType.MissileSilo, icon: missileSiloIcon }, - // { unitType: UnitType.DefensePost, icon: shieldIcon }, + { unitType: UnitType.DefensePost, icon: shieldIcon }, { unitType: UnitType.City, icon: cityIcon }, ], ]; diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index a0d4e7884..31c1139ac 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -7,7 +7,7 @@ import { Unit, UnitType, } from "../../../core/game/Game"; -import { UnitUpdate } from "../../../core/game/GameUpdates"; +import { GameUpdateType, UnitUpdate } from "../../../core/game/GameUpdates"; import { PseudoRandom } from "../../../core/PseudoRandom"; import { colord, Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; @@ -19,7 +19,11 @@ import { MouseDownEvent, } from "../../InputHandler"; import { GameView, PlayerView } from "../../../core/game/GameView"; -import { euclDistFN, TileRef } from "../../../core/game/GameMap"; +import { + euclDistFN, + manhattanDistFN, + TileRef, +} from "../../../core/game/GameMap"; export class TerritoryLayer implements Layer { private canvas: HTMLCanvasElement; @@ -46,10 +50,7 @@ export class TerritoryLayer implements Layer { private refreshRate = 50; private lastRefresh = 0; - constructor( - private game: GameView, - private eventBus: EventBus, - ) { + constructor(private game: GameView, private eventBus: EventBus) { this.theme = game.config().theme(); } @@ -59,6 +60,25 @@ export class TerritoryLayer implements Layer { tick() { this.game.recentlyUpdatedTiles().forEach((t) => this.enqueueTile(t)); + this.game.updatesSinceLastTick()[GameUpdateType.Unit].forEach((u) => { + const update = u as UnitUpdate; + if (update.unitType == UnitType.DefensePost && update.isActive) { + const tile = this.game.ref(update.pos.x, update.pos.y); + this.game + .bfs( + tile, + manhattanDistFN(tile, this.game.config().defensePostRange()) + ) + .forEach((t) => { + if ( + this.game.isBorder(t) && + this.game.ownerID(t) == update.ownerID + ) { + this.enqueueTile(t); + } + }); + } + }); if (!this.game.inSpawnPhase()) { return; @@ -71,7 +91,7 @@ export class TerritoryLayer implements Layer { 0, 0, this.game.width(), - this.game.height(), + this.game.height() ); const humans = this.game .playerViews() @@ -91,7 +111,7 @@ export class TerritoryLayer implements Layer { this.paintHighlightCell( new Cell(this.game.x(tile), this.game.y(tile)), this.theme.spawnHighlightColor(), - 255, + 255 ); } } @@ -117,7 +137,7 @@ export class TerritoryLayer implements Layer { 0, 0, this.game.width(), - this.game.height(), + this.game.height() ); this.initImageData(); this.canvas.width = this.game.width(); @@ -164,7 +184,7 @@ export class TerritoryLayer implements Layer { -this.game.width() / 2, -this.game.height() / 2, this.game.width(), - this.game.height(), + this.game.height() ); if (this.game.inSpawnPhase()) { context.drawImage( @@ -172,7 +192,7 @@ export class TerritoryLayer implements Layer { -this.game.width() / 2, -this.game.height() / 2, this.game.width(), - this.game.height(), + this.game.height() ); } } @@ -203,27 +223,39 @@ export class TerritoryLayer implements Layer { this.game.x(tile), this.game.y(tile), this.theme.falloutColor(), - 150, + 150 ); return; } this.clearCell(new Cell(this.game.x(tile), this.game.y(tile))); return; } - const owner = this.game.owner(tile) as Player; + const owner = this.game.owner(tile) as PlayerView; if (this.game.isBorder(tile)) { - this.paintCell( - this.game.x(tile), - this.game.y(tile), - this.theme.borderColor(owner.info()), - 255, - ); + if ( + this.game.nearbyDefenses(tile).filter((u) => u.owner() == owner) + .length > 0 + ) { + this.paintCell( + this.game.x(tile), + this.game.y(tile), + this.theme.defendedBorderColor(owner.info()), + 255 + ); + } else { + this.paintCell( + this.game.x(tile), + this.game.y(tile), + this.theme.borderColor(owner.info()), + 255 + ); + } } else { this.paintCell( this.game.x(tile), this.game.y(tile), this.theme.territoryColor(owner.info()), - 150, + 150 ); } } diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 285b5148d..6f1bae2dc 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -1,5 +1,6 @@ import { Difficulty, + Game, GameType, Gold, Player, @@ -81,7 +82,7 @@ export interface Config { numAdjacentTilesWithEnemy: number, ): number; attackLogic( - gm: GameMap, + gm: Game, attackTroops: number, attacker: Player, defender: Player | TerraNullius, diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 17e1afc64..52aeb4dbf 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -1,5 +1,6 @@ import { Difficulty, + Game, GameType, Gold, Player, @@ -66,7 +67,7 @@ export class DefaultConfig implements Config { } defensePostRange(): number { - return 40; + return 30; } defensePostDefenseBonus(): number { return 5; @@ -205,7 +206,7 @@ export class DefaultConfig implements Config { } attackLogic( - gm: GameMap, + gm: Game, attackTroops: number, attacker: Player, defender: Player | TerraNullius, @@ -234,9 +235,16 @@ export class DefaultConfig implements Config { default: throw new Error(`terrain type ${type} not supported`); } - // TODO - // mag *= tileToConquer.defenseBonus(attacker) - // speed *= tileToConquer.defenseBonus(attacker) + if (defender.isPlayer()) { + for (const dp of gm.nearbyDefensePosts(tileToConquer)) { + if (dp.owner() == defender) { + mag *= this.defensePostDefenseBonus(); + speed *= this.defensePostDefenseBonus(); + break; + } + } + } + if (gm.hasFallout(tileToConquer)) { mag *= this.falloutDefenseModifier(); speed *= this.falloutDefenseModifier(); diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index ea5dcd96b..52a690a40 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -1,7 +1,6 @@ import { consolex } from "../Consolex"; import { Cell, - DefenseBonus, Execution, Game, Player, @@ -17,12 +16,7 @@ export class DefensePostExecution implements Execution { private post: Unit; private active: boolean = true; - private defenseBonuses: DefenseBonus[] = []; - - constructor( - private ownerId: PlayerID, - private tile: TileRef, - ) {} + constructor(private ownerId: PlayerID, private tile: TileRef) {} init(mg: Game, ticks: number): void { this.mg = mg; @@ -38,25 +32,8 @@ export class DefensePostExecution implements Execution { return; } this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile); - this.mg - .bfs( - spawnTile, - manhattanDistFN(spawnTile, this.mg.config().defensePostRange()), - ) - .forEach((t) => { - if (this.mg.isLake(t)) { - this.defenseBonuses.push( - this.mg.addTileDefenseBonus( - t, - this.post, - this.mg.config().defensePostDefenseBonus(), - ), - ); - } - }); } if (!this.post.isActive()) { - this.defenseBonuses.forEach((df) => this.mg.removeTileDefenseBonus(df)); this.active = false; return; } diff --git a/src/core/game/DefensePostGrid.ts b/src/core/game/DefensePostGrid.ts new file mode 100644 index 000000000..bafffe6c8 --- /dev/null +++ b/src/core/game/DefensePostGrid.ts @@ -0,0 +1,89 @@ +import { Unit } from "./Game"; +import { GameMap, TileRef } from "./GameMap"; +import { UnitView } from "./GameView"; + +export class DefenseGrid { + private grid: Set[][]; + private readonly cellSize = 100; + + constructor(private gm: GameMap, private searchRange: number) { + this.grid = Array(Math.ceil(gm.height() / this.cellSize)) + .fill(null) + .map(() => + Array(Math.ceil(gm.width() / this.cellSize)) + .fill(null) + .map(() => new Set()) + ); + } + + // Get grid coordinates from pixel coordinates + private getGridCoords(x: number, y: number): [number, number] { + return [Math.floor(x / this.cellSize), Math.floor(y / this.cellSize)]; + } + + // Add a defense unit to the grid + addDefense(unit: Unit | UnitView) { + const tile = unit.tile(); + const [gridX, gridY] = this.getGridCoords(this.gm.x(tile), this.gm.y(tile)); + + if (this.isValidCell(gridX, gridY)) { + this.grid[gridY][gridX].add(unit); + } + } + + // Remove a defense unit from the grid + removeDefense(unit: Unit | UnitView) { + const tile = unit.tile(); + const [gridX, gridY] = this.getGridCoords(this.gm.x(tile), this.gm.y(tile)); + + if (this.isValidCell(gridX, gridY)) { + this.grid[gridY][gridX].delete(unit); + } + } + + private isValidCell(gridX: number, gridY: number): boolean { + return ( + gridX >= 0 && + gridX < this.grid[0].length && + gridY >= 0 && + gridY < this.grid.length + ); + } + + // Get all defense units within range of a point + // Returns [unit, distanceSquared] pairs for efficient filtering + nearbyDefenses(tile: TileRef): Array { + 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 = []; + + // 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; + + 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 (distSquared <= rangeSquared) { + nearby.push(unit); + } + } + } + } + + return nearby; + } +} diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index a7d243cef..d12497f74 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -165,13 +165,6 @@ export class PlayerInfo { ) {} } -export interface DefenseBonus { - // Unit providing the defense bonus - unit: Unit; - amount: number; - tile: TileRef; -} - export interface Unit { // Properties type(): UnitType; @@ -323,8 +316,7 @@ export interface Game extends GameMap { // Units units(...types: UnitType[]): Unit[]; unitInfo(type: UnitType): UnitInfo; - addTileDefenseBonus(tile: TileRef, unit: Unit, amount: number): DefenseBonus; - removeTileDefenseBonus(bonus: DefenseBonus): void; + nearbyDefensePosts(tile: TileRef): Unit[]; // Events & Messages executions(): Execution[]; diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index c0b89002f..ea6114a11 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -13,7 +13,6 @@ import { Nation, UnitType, UnitInfo, - DefenseBonus, AllPlayers, GameUpdates, TerrainType, @@ -31,6 +30,7 @@ import { MessageType } from "./Game"; import { UnitImpl } from "./UnitImpl"; import { consolex } from "../Consolex"; import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; +import { DefenseGrid } from "./DefensePostGrid"; export function createGame( gameMap: GameMap, @@ -65,6 +65,7 @@ export class GameImpl implements Game { private _nextUnitID = 1; private updates: GameUpdates = createGameUpdatesMap(); + private defenseGrid: DefenseGrid; constructor( private _map: GameMap, @@ -83,6 +84,10 @@ export class GameImpl implements Game { n.strength ) ); + this.defenseGrid = new DefenseGrid( + this._map, + this._config.defensePostRange() + ); } owner(ref: TileRef): Player | TerraNullius { @@ -125,21 +130,6 @@ export class GameImpl implements Game { }); } - addTileDefenseBonus(tile: TileRef, unit: Unit, amount: number): DefenseBonus { - // TODO!! - const df = { unit: unit, tile: tile, amount: amount }; - // (tile as TileImpl)._defenseBonuses.push(df) - // this.addUpdate((tile as TileImpl).toUpdate()) - return df; - } - - removeTileDefenseBonus(bonus: DefenseBonus): void { - // TODO!! - // const t = bonus.tile as TileImpl - // t._defenseBonuses = t._defenseBonuses.filter(db => db != bonus) - // this.addUpdate(t.toUpdate()) - } - units(...types: UnitType[]): UnitImpl[] { return Array.from(this._players.values()).flatMap((p) => p.units(...types)); } @@ -425,7 +415,6 @@ export class GameImpl implements Game { } else { (this.owner(t) as PlayerImpl)._borderTiles.delete(t); } - // this.updates.push(t.toUpdate()) } } @@ -531,6 +520,17 @@ export class GameImpl implements Game { }); } + addDefensePost(dp: Unit) { + this.defenseGrid.addDefense(dp); + } + removeDefensePost(dp: Unit) { + this.defenseGrid.removeDefense(dp); + } + + nearbyDefensePosts(tile: TileRef): Unit[] { + return this.defenseGrid.nearbyDefenses(tile) as Unit[]; + } + ref(x: number, y: number): TileRef { return this._map.ref(x, y); } diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts index 840cbd03a..ad1c3eaf8 100644 --- a/src/core/game/GameUpdates.ts +++ b/src/core/game/GameUpdates.ts @@ -62,6 +62,7 @@ export interface UnitUpdate { troops: number; id: number; ownerID: number; + // TODO: make these tilerefs pos: MapPos; lastPos: MapPos; isActive: boolean; diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts index 1af2860b6..7b87026f5 100644 --- a/src/core/game/GameView.ts +++ b/src/core/game/GameView.ts @@ -13,19 +13,12 @@ import { NameViewData } from "./Game"; import { GameUpdateType } from "./GameUpdates"; import { Config } from "../configuration/Config"; import { - Alliance, - AllianceRequest, - AllPlayers, Cell, - DefenseBonus, EmojiMessage, - Game, Gold, - Nation, PlayerID, PlayerInfo, PlayerType, - Relation, TerrainType, TerraNullius, Tick, @@ -37,6 +30,7 @@ import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { WorkerClient } from "../worker/WorkerClient"; import { GameMap, GameMapImpl, TileRef, TileUpdate } from "./GameMap"; import { GameUpdateViewData } from "./GameUpdates"; +import { DefenseGrid } from "./DefensePostGrid"; export class UnitView { public _wasUpdated = true; @@ -204,6 +198,8 @@ export class GameView implements GameMap { private _myPlayer: PlayerView | null = null; + private defensePostGrid: DefenseGrid; + constructor( public worker: WorkerClient, private _config: Config, @@ -217,6 +213,7 @@ export class GameView implements GameMap { updates: null, playerNameViewData: {}, }; + this.defensePostGrid = new DefenseGrid(_map, _config.defensePostRange()); } public updatesSinceLastTick(): GameUpdates { @@ -247,11 +244,21 @@ export class GameView implements GameMap { unit._wasUpdated = false; unit.lastPos = unit.lastPos.slice(-1); } - gu.updates[GameUpdateType.Unit].forEach((unit) => { - if (this._units.has(unit.id)) { - this._units.get(unit.id).update(unit); + gu.updates[GameUpdateType.Unit].forEach((update) => { + let unit: UnitView = null; + if (this._units.has(update.id)) { + unit = this._units.get(update.id); + unit.update(update); } else { - this._units.set(unit.id, new UnitView(this, unit)); + 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); + } } }); } @@ -260,6 +267,10 @@ export class GameView implements GameMap { return this.updatedTiles; } + nearbyDefenses(tile: TileRef): UnitView[] { + return this.defensePostGrid.nearbyDefenses(tile) as UnitView[]; + } + myClientID(): ClientID { return this._myClientID; } diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 9c0ce7904..97dac7417 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -556,6 +556,9 @@ export class PlayerImpl implements Player { this.removeGold(cost); this.removeTroops(troops); this.mg.addUpdate(b.toUpdate()); + if (type == UnitType.DefensePost) { + this.mg.addDefensePost(b); + } return b; } diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts index f31089e8b..4429714c9 100644 --- a/src/core/game/UnitImpl.ts +++ b/src/core/game/UnitImpl.ts @@ -101,6 +101,9 @@ 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); + } if (displayMessage) { this.mg.displayMessage( `Your ${this.type()} was destroyed`,