diff --git a/TODO.txt b/TODO.txt index a519958ee..a459bba7a 100644 --- a/TODO.txt +++ b/TODO.txt @@ -202,7 +202,8 @@ * add info view on top right DONE 11/30/2024 * add info view for units DONE 11/30/2024 * add defense post -* record single player games +* use mini A* for all pathfinding +* record single player game stats * add radiation from nuke * add cities * create alternate view to show friendly & enemy units diff --git a/resources/images/ShieldIcon.png b/resources/images/ShieldIcon.png index 391902d6c..e1db2168e 100644 Binary files a/resources/images/ShieldIcon.png and b/resources/images/ShieldIcon.png differ diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts index 110e42de8..f5e2e5674 100644 --- a/src/client/graphics/layers/StructureLayer.ts +++ b/src/client/graphics/layers/StructureLayer.ts @@ -1,4 +1,4 @@ -import { Colord } from "colord"; +import { colord, Colord } from "colord"; import { Theme } from "../../../core/configuration/Config"; import { Unit, UnitEvent, Cell, Game, Tile, UnitType } from "../../../core/game/Game"; import { bfs, dist, euclDist } from "../../../core/Util"; @@ -160,6 +160,19 @@ export class StructureLayer implements Layer { onUnitEvent(event: UnitEvent) { this.handleUnitRendering(event); + if (event.unit.type() == UnitType.DefensePost) { + if (!event.unit.isActive()) { + return + } + // Array.from( + // bfs( + // event.unit.tile(), + // dist(event.unit.tile(), this.game.config().defensePostRange()) + // ) + // ).filter(t => t.isBorder() && t.owner() == event.unit.owner()).forEach(t => { + // this.paintCell(t.cell(), colord({ r: 255, g: 255, b: 255 }), 255) + // }) + } } paintCell(cell: Cell, color: Colord, alpha: number) { diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index c6dfd6c31..6dbe4128c 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -1,7 +1,7 @@ import { PriorityQueue } from "@datastructures-js/priority-queue"; -import { Cell, Game, Player, Tile, TileEvent } from "../../../core/game/Game"; +import { Cell, Game, Player, Tile, TileEvent, UnitEvent, UnitType } from "../../../core/game/Game"; import { PseudoRandom } from "../../../core/PseudoRandom"; -import { Colord } from "colord"; +import { colord, Colord } from "colord"; import { bfs, dist } from "../../../core/Util"; import { Theme } from "../../../core/configuration/Config"; import { Layer } from "./Layer"; @@ -13,12 +13,10 @@ export class TerritoryLayer implements Layer { private context: CanvasRenderingContext2D private imageData: ImageData - private tileToRenderQueue: PriorityQueue<{ tileEvent: TileEvent, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate }) + private tileToRenderQueue: PriorityQueue<{ tile: Tile, lastUpdate: number }> = new PriorityQueue((a, b) => { return a.lastUpdate - b.lastUpdate }) private random = new PseudoRandom(123) private theme: Theme = null - - constructor(private game: Game, eventBus: EventBus) { this.theme = game.config().theme() eventBus.on(TileEvent, e => this.tileUpdate(e)) @@ -70,9 +68,9 @@ export class TerritoryLayer implements Layer { while (numToRender > 0) { numToRender-- - const event = this.tileToRenderQueue.pop().tileEvent - this.paintTerritory(event.tile) - event.tile.neighbors().forEach(t => this.paintTerritory(t)) + const tile = this.tileToRenderQueue.pop().tile + this.paintTerritory(tile) + tile.neighbors().forEach(t => this.paintTerritory(t)) } } @@ -83,11 +81,19 @@ export class TerritoryLayer implements Layer { } const owner = tile.owner() as Player if (tile.isBorder()) { - this.paintCell( - tile.cell(), - this.theme.borderColor(owner.info()), - 255 - ) + if (tile.defenseBonuses().filter(db => db.unit.owner() == owner).length > 0) { + this.paintCell( + tile.cell(), + colord({ r: 0, g: 0, b: 0 }), + 255 + ) + } else { + this.paintCell( + tile.cell(), + this.theme.borderColor(owner.info()), + 255 + ) + } } else { this.paintCell( tile.cell(), @@ -112,7 +118,17 @@ export class TerritoryLayer implements Layer { this.imageData.data[offset + 3] = 0; // Set alpha to 0 (fully transparent) } + unitEvent(event: UnitEvent) { + if (event.unit.type() == UnitType.DefensePost) { + bfs(event.unit.tile(), dist(event.unit.tile(), this.game.config().defensePostRange())).forEach(t => this.enqueue(t)) + } + } + tileUpdate(event: TileEvent) { - this.tileToRenderQueue.push({ tileEvent: event, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) }) + this.enqueue(event.tile) + } + + enqueue(tile: Tile) { + this.tileToRenderQueue.push({ tile: tile, lastUpdate: this.game.ticks() + this.random.nextFloat(0, .5) }) } } \ No newline at end of file diff --git a/src/client/index.html b/src/client/index.html index 6727c65b3..186f72409 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -28,7 +28,7 @@

OpenFront.io

-

(v0.10.0)

+

(v0.10.1)


@@ -42,11 +42,11 @@

-
+
- Single Player + diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 719b7212b..99a39cfe2 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -60,6 +60,8 @@ export interface Config { unitInfo(type: UnitType): UnitInfo tradeShipGold(src: Unit, dst: Unit): Gold tradeShipSpawnRate(): number + defensePostRange(): number + defensePostDefenseBonus(): number } export interface Theme { diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 8db846c5d..085329574 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -7,6 +7,12 @@ import { pastelTheme } from "./PastelTheme"; export class DefaultConfig implements Config { + defensePostRange(): number { + return 20 + } + defensePostDefenseBonus(): number { + return 3 + } spawnNPCs(): boolean { return true } @@ -140,7 +146,8 @@ export class DefaultConfig implements Config { speed = 30 break } - // speed = mag + mag *= tileToConquer.defenseBonus(attacker) + speed *= tileToConquer.defenseBonus(attacker) if (attacker.isPlayer() && defender.isPlayer()) { if (attacker.type() == PlayerType.Human && defender.type() == PlayerType.Bot) { diff --git a/src/core/configuration/DevConfig.ts b/src/core/configuration/DevConfig.ts index bb3edb960..77e7d0344 100644 --- a/src/core/configuration/DevConfig.ts +++ b/src/core/configuration/DevConfig.ts @@ -13,14 +13,14 @@ export const devConfig = new class extends DefaultConfig { return 95 } numSpawnPhaseTurns(): number { - return 40 - // return 100 + // return 40 + return 100 } gameCreationRate(): number { - return 20 * 1000 + return 10 * 1000 } lobbyLifetime(): number { - return 20 * 1000 + return 10 * 1000 } turnIntervalMs(): number { return 100 diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts index 0427f3ff2..86869f1d4 100644 --- a/src/core/execution/DefensePostExecution.ts +++ b/src/core/execution/DefensePostExecution.ts @@ -1,4 +1,5 @@ -import { Cell, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { Cell, DefenseBonus, Execution, MutableGame, MutablePlayer, MutableUnit, PlayerID, Tile, UnitType } from "../game/Game"; +import { bfs, dist } from "../Util"; export class DefensePostExecution implements Execution { @@ -8,6 +9,8 @@ export class DefensePostExecution implements Execution { private tile: Tile private active: boolean = true + private defenseBonuses: DefenseBonus[] = [] + constructor(private ownerId: PlayerID, private cell: Cell) { } init(mg: MutableGame, ticks: number): void { @@ -25,6 +28,16 @@ export class DefensePostExecution implements Execution { return } this.post = this.player.buildUnit(UnitType.DefensePost, 0, spawnTile) + bfs(spawnTile, dist(spawnTile, this.mg.config().defensePostRange())).forEach(t => { + if (t.isLand()) { + 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/Game.ts b/src/core/game/Game.ts index f6ee5b53f..b46bf0dae 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -144,6 +144,13 @@ export interface TerrainTile extends SearchNode { terrainType(): TerrainType } +export interface DefenseBonus { + // Unit providing the defense bonus + unit: Unit + amount: number + tile: Tile +} + export interface Tile extends SearchNode { isLand(): boolean isShore(): boolean @@ -163,6 +170,10 @@ export interface Tile extends SearchNode { neighbors(): Tile[] neighborsWrapped(): Tile[] onShore(): boolean + + defenseBonuses(): DefenseBonus[] + // defense bonus against this player + defenseBonus(player: Player): number } export interface Unit { @@ -301,6 +312,8 @@ export interface MutableGame extends Game { addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer executions(): Execution[] units(...types: UnitType[]): MutableUnit[] + addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus + removeTileDefenseBonus(bonus: DefenseBonus): void } export class TileEvent implements GameEvent { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index dfda7a9e9..fd80ca762 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -1,7 +1,7 @@ import { info } from "console"; import { Config } from "../configuration/Config"; import { EventBus } from "../EventBus"; -import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo, TerrainMap } from "./Game"; +import { Cell, Execution, MutableGame, Game, MutablePlayer, PlayerEvent, PlayerID, PlayerInfo, Player, TerraNullius, Tile, TileEvent, Unit, UnitEvent as UnitEvent, PlayerType, MutableAllianceRequest, AllianceRequestReplyEvent, AllianceRequestEvent, BrokeAllianceEvent, MutableAlliance, Alliance, AllianceExpiredEvent, Nation, UnitType, UnitInfo, TerrainMap, DefenseBonus } from "./Game"; import { createMiniMap, TerrainMapImpl } from "./TerrainMapLoader"; import { PlayerImpl } from "./PlayerImpl"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; @@ -64,6 +64,20 @@ export class GameImpl implements MutableGame { this._terrainMiniMap = m }) } + + addTileDefenseBonus(tile: Tile, unit: Unit, amount: number): DefenseBonus { + const df = { unit: unit, tile: tile, amount: amount }; + (tile as TileImpl)._defenseBonuses.push(df) + this.eventBus.emit(new TileEvent(tile)) + return df + } + + removeTileDefenseBonus(bonus: DefenseBonus): void { + const t = bonus.tile as TileImpl + t._defenseBonuses = t._defenseBonuses.filter(db => db != bonus) + this.eventBus.emit(new TileEvent(bonus.tile)) + } + units(...types: UnitType[]): UnitImpl[] { return Array.from(this._players.values()).flatMap(p => p.units(...types)) } diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index d55818423..17e5c4142 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -1,4 +1,4 @@ -import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer, TerrainTile } from "./Game"; +import { Tile, Cell, TerrainType, Player, TerraNullius, MutablePlayer, TerrainTile, DefenseBonus } from "./Game"; import { SearchNode } from "../pathfinding/AStar"; import { TerrainTileImpl } from "./TerrainMapLoader"; import { GameImpl } from "./GameImpl"; @@ -11,6 +11,8 @@ export class TileImpl implements Tile { public _isBorder = false; private _neighbors: Tile[] = null; + public _defenseBonuses: DefenseBonus[] = [] + constructor( private readonly gs: GameImpl, public _owner: PlayerImpl | TerraNulliusImpl, @@ -18,6 +20,23 @@ export class TileImpl implements Tile { private readonly _terrain: TerrainTileImpl ) { } + defenseBonus(player: Player): number { + if (this.owner() == player) { + throw Error(`cannot get defense bonus of tile already owned by player`) + } + let bonusAmount = 0 + for (const bonus of this._defenseBonuses) { + if (bonus.unit.owner() != player) { + bonusAmount += bonus.amount + } + } + return Math.max(bonusAmount, 1) + } + + defenseBonuses(): DefenseBonus[] { + return this._defenseBonuses + } + neighborsWrapped(): Tile[] { const x = this._cell.x; const y = this._cell.y;