mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
reimplement defense posts
This commit is contained in:
@@ -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 },
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Unit } from "./Game";
|
||||
import { GameMap, TileRef } from "./GameMap";
|
||||
import { UnitView } from "./GameView";
|
||||
|
||||
export class DefenseGrid {
|
||||
private grid: Set<Unit | UnitView>[][];
|
||||
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<Unit | UnitView>())
|
||||
);
|
||||
}
|
||||
|
||||
// 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<Unit | UnitView> {
|
||||
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> = [];
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
+17
-17
@@ -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);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ export interface UnitUpdate {
|
||||
troops: number;
|
||||
id: number;
|
||||
ownerID: number;
|
||||
// TODO: make these tilerefs
|
||||
pos: MapPos;
|
||||
lastPos: MapPos;
|
||||
isActive: boolean;
|
||||
|
||||
+22
-11
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user