import { Config } from "../configuration/Config"; import { ClientID, GameID, PlayerStats } from "../Schemas"; import { createRandomName } from "../Util"; import { WorkerClient } from "../worker/WorkerClient"; import { Cell, EmojiMessage, GameUpdates, Gold, NameViewData, nukeTypes, Player, PlayerActions, PlayerBorderTiles, PlayerID, PlayerInfo, PlayerProfile, PlayerType, Team, TerrainType, TerraNullius, Tick, UnitInfo, UnitType, } from "./Game"; import { GameMap, TileRef, TileUpdate } from "./GameMap"; import { AttackUpdate, GameUpdateType, GameUpdateViewData, PlayerUpdate, UnitUpdate, } from "./GameUpdates"; import { TerraNulliusImpl } from "./TerraNulliusImpl"; import { UnitGrid } from "./UnitGrid"; import { UserSettings } from "./UserSettings"; const userSettings: UserSettings = new UserSettings(); export class UnitView { public _wasUpdated = true; public lastPos: TileRef[] = []; constructor( private gameView: GameView, private data: UnitUpdate, ) { this.lastPos.push(data.pos); } wasUpdated(): boolean { return this._wasUpdated; } lastTiles(): TileRef[] { return this.lastPos; } lastTile(): TileRef { if (this.lastPos.length == 0) { return this.data.pos; } return this.lastPos[0]; } update(data: UnitUpdate) { this.lastPos.push(data.pos); this._wasUpdated = true; this.data = data; } id(): number { return this.data.id; } type(): UnitType { return this.data.unitType; } troops(): number { return this.data.troops; } createdAtTick(): number | undefined { return this.data.createdAtTick; } tile(): TileRef { return this.data.pos; } owner(): PlayerView { return this.gameView.playerBySmallID(this.data.ownerID) as PlayerView; } isActive(): boolean { return this.data.isActive; } hasHealth(): boolean { return this.data.health != undefined; } health(): number { return this.data.health ?? 0; } constructionType(): UnitType | undefined { return this.data.constructionType; } dstPortId(): number { if (this.type() != UnitType.TradeShip) { throw Error("Must be a trade ship"); } return this.data.dstPortId; } detonationDst(): TileRef { if (!nukeTypes.includes(this.type())) { throw Error("Must be a nuke"); } return this.data.detonationDst; } warshipTargetId(): number { if (this.type() != UnitType.Warship) { throw Error("Must be a warship"); } return this.data.warshipTargetId; } ticksLeftInCooldown(): Tick { return this.data.ticksLeftInCooldown; } isCooldown(): boolean { return this.data.ticksLeftInCooldown > 0; } } export class PlayerView { public anonymousName: string; constructor( private game: GameView, public data: PlayerUpdate, public nameData: NameViewData, ) { if (data.clientID == game.myClientID()) { this.anonymousName = this.data.name; } else { this.anonymousName = createRandomName( this.data.name, this.data.playerType, ); } } async actions(tile: TileRef): Promise { return this.game.worker.playerInteraction( this.id(), this.game.x(tile), this.game.y(tile), ); } async borderTiles(): Promise { return this.game.worker.playerBorderTiles(this.id()); } outgoingAttacks(): AttackUpdate[] { return this.data.outgoingAttacks; } incomingAttacks(): AttackUpdate[] { return this.data.incomingAttacks; } units(...types: UnitType[]): UnitView[] { return this.game .units(...types) .filter((u) => u.owner().smallID() == this.smallID()); } nameLocation(): NameViewData { return this.nameData; } smallID(): number { return this.data.smallID; } flag(): string { return this.data.flag; } name(): string { return userSettings.anonymousNames() && this.anonymousName !== null ? this.anonymousName : this.data.name; } displayName(): string { return userSettings.anonymousNames() && this.anonymousName !== null ? this.anonymousName : this.data.name; } clientID(): ClientID { return this.data.clientID; } id(): PlayerID { return this.data.id; } team(): Team | null { return this.data.team ?? null; } type(): PlayerType { return this.data.playerType; } isAlive(): boolean { return this.data.isAlive; } isPlayer(): this is Player { return true; } numTilesOwned(): number { return this.data.tilesOwned; } allies(): PlayerView[] { return this.data.allies.map( (a) => this.game.playerBySmallID(a) as PlayerView, ); } targets(): PlayerView[] { return this.data.targets.map( (id) => this.game.playerBySmallID(id) as PlayerView, ); } gold(): Gold { return this.data.gold; } population(): number { return this.data.population; } adjustedPopulation(): number { return this.data.adjustedPopulation; } maxPopulation(): number { return this.data.maxPopulation; } workers(): number { return this.data.workers; } targetTroopRatio(): number { return this.data.targetTroopRatio; } troops(): number { return this.data.troops; } isAlliedWith(other: PlayerView): boolean { return this.data.allies.some((n) => other.smallID() == n); } isOnSameTeam(other: PlayerView): boolean { return ( this.data.team !== null && this.data.team === other.data.team && this.data.playerType !== PlayerType.Bot ); } isFriendly(other: PlayerView): boolean { return this.isAlliedWith(other) || this.isOnSameTeam(other); } isRequestingAllianceWith(other: PlayerView) { return this.data.outgoingAllianceRequests.some((id) => other.id() == id); } hasEmbargoAgainst(other: PlayerView): boolean { return this.data.embargoes.has(other.id()); } profile(): Promise { return this.game.worker.playerProfile(this.smallID()); } bestTransportShipSpawn(targetTile: TileRef): Promise { return this.game.worker.transportShipSpawn(this.id(), targetTile); } transitiveTargets(): PlayerView[] { return [...this.targets(), ...this.allies().flatMap((p) => p.targets())]; } isTraitor(): boolean { return this.data.isTraitor; } outgoingEmojis(): EmojiMessage[] { return this.data.outgoingEmojis; } info(): PlayerInfo { return new PlayerInfo( this.flag(), this.name(), this.type(), this.clientID(), this.id(), ); } stats(): PlayerStats { return this.data.stats; } hasSpawned(): boolean { return this.data.hasSpawned; } } export class GameView implements GameMap { private lastUpdate: GameUpdateViewData; private smallIDToID = new Map(); private _players = new Map(); private _units = new Map(); private updatedTiles: TileRef[] = []; private _myPlayer: PlayerView | null = null; private _focusedPlayer: PlayerView | null = null; private unitGrid: UnitGrid; private toDelete = new Set(); constructor( public worker: WorkerClient, private _config: Config, private _map: GameMap, private _myClientID: ClientID, private _gameID: GameID, ) { this.lastUpdate = { tick: 0, packedTileUpdates: new BigUint64Array([]), // TODO: make this empty map instead of null? updates: null, playerNameViewData: {}, }; this.unitGrid = new UnitGrid(_map); } isOnEdgeOfMap(ref: TileRef): boolean { return this._map.isOnEdgeOfMap(ref); } public updatesSinceLastTick(): GameUpdates { return this.lastUpdate.updates; } public update(gu: GameUpdateViewData) { this.toDelete.forEach((id) => this._units.delete(id)); this.toDelete.clear(); this.lastUpdate = gu; this.updatedTiles = []; this.lastUpdate.packedTileUpdates.forEach((tu) => { this.updatedTiles.push(this.updateTile(tu)); }); gu.updates[GameUpdateType.Player].forEach((pu) => { this.smallIDToID.set(pu.smallID, pu.id); if (this._players.has(pu.id)) { this._players.get(pu.id).data = pu; this._players.get(pu.id).nameData = gu.playerNameViewData[pu.id]; } else { this._players.set( pu.id, new PlayerView(this, pu, gu.playerNameViewData[pu.id]), ); } }); for (const unit of this._units.values()) { unit._wasUpdated = false; unit.lastPos = unit.lastPos.slice(-1); } 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 { unit = new UnitView(this, update); this._units.set(update.id, unit); } if (update.isActive) { this.unitGrid.addUnit(unit); } else { this.unitGrid.removeUnit(unit); } if (!unit.isActive()) { // Wait until next tick to delete the unit. this.toDelete.add(unit.id()); } }); } recentlyUpdatedTiles(): TileRef[] { return this.updatedTiles; } 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 { return this._myClientID; } myPlayer(): PlayerView | null { if (this._myPlayer == null) { this._myPlayer = this.playerByClientID(this._myClientID); } return this._myPlayer; } player(id: PlayerID): PlayerView { if (this._players.has(id)) { return this._players.get(id); } throw Error(`player id ${id} not found`); } players(): PlayerView[] { return Array.from(this._players.values()); } playerBySmallID(id: number): PlayerView | TerraNullius { if (id == 0) { return new TerraNulliusImpl(); } if (!this.smallIDToID.has(id)) { throw new Error(`small id ${id} not found`); } return this.player(this.smallIDToID.get(id)); } playerByClientID(id: ClientID): PlayerView | null { const player = Array.from(this._players.values()).filter((p) => p.clientID() == id)[0] ?? null; if (player == null) { return null; } return player; } hasPlayer(id: PlayerID): boolean { return false; } playerViews(): PlayerView[] { return Array.from(this._players.values()); } owner(tile: TileRef): PlayerView | TerraNullius { return this.playerBySmallID(this.ownerID(tile)); } ticks(): Tick { return this.lastUpdate.tick; } inSpawnPhase(): boolean { return this.lastUpdate.tick <= this._config.numSpawnPhaseTurns(); } config(): Config { return this._config; } units(...types: UnitType[]): UnitView[] { if (types.length == 0) { return Array.from(this._units.values()).filter((u) => u.isActive()); } return Array.from(this._units.values()).filter( (u) => u.isActive() && types.includes(u.type()), ); } unit(id: number): UnitView { return this._units.get(id); } unitInfo(type: UnitType): UnitInfo { return this._config.unitInfo(type); } ref(x: number, y: number): TileRef { return this._map.ref(x, y); } x(ref: TileRef): number { return this._map.x(ref); } y(ref: TileRef): number { return this._map.y(ref); } cell(ref: TileRef): Cell { return this._map.cell(ref); } width(): number { return this._map.width(); } height(): number { return this._map.height(); } numLandTiles(): number { return this._map.numLandTiles(); } isValidCoord(x: number, y: number): boolean { return this._map.isValidCoord(x, y); } isLand(ref: TileRef): boolean { return this._map.isLand(ref); } isOceanShore(ref: TileRef): boolean { return this._map.isOceanShore(ref); } isOcean(ref: TileRef): boolean { return this._map.isOcean(ref); } isShoreline(ref: TileRef): boolean { return this._map.isShoreline(ref); } magnitude(ref: TileRef): number { return this._map.magnitude(ref); } ownerID(ref: TileRef): number { return this._map.ownerID(ref); } hasOwner(ref: TileRef): boolean { return this._map.hasOwner(ref); } setOwnerID(ref: TileRef, playerId: number): void { return this._map.setOwnerID(ref, playerId); } hasFallout(ref: TileRef): boolean { return this._map.hasFallout(ref); } setFallout(ref: TileRef, value: boolean): void { return this._map.setFallout(ref, value); } isBorder(ref: TileRef): boolean { return this._map.isBorder(ref); } neighbors(ref: TileRef): TileRef[] { return this._map.neighbors(ref); } isWater(ref: TileRef): boolean { return this._map.isWater(ref); } isLake(ref: TileRef): boolean { return this._map.isLake(ref); } isShore(ref: TileRef): boolean { return this._map.isShore(ref); } cost(ref: TileRef): number { return this._map.cost(ref); } terrainType(ref: TileRef): TerrainType { return this._map.terrainType(ref); } forEachTile(fn: (tile: TileRef) => void): void { return this._map.forEachTile(fn); } manhattanDist(c1: TileRef, c2: TileRef): number { return this._map.manhattanDist(c1, c2); } euclideanDistSquared(c1: TileRef, c2: TileRef): number { return this._map.euclideanDistSquared(c1, c2); } bfs( tile: TileRef, filter: (gm: GameMap, tile: TileRef) => boolean, ): Set { return this._map.bfs(tile, filter); } toTileUpdate(tile: TileRef): bigint { return this._map.toTileUpdate(tile); } updateTile(tu: TileUpdate): TileRef { return this._map.updateTile(tu); } numTilesWithFallout(): number { return this._map.numTilesWithFallout(); } gameID(): GameID { return this._gameID; } focusedPlayer(): PlayerView | null { // TODO: renable when performance issues are fixed. return this.myPlayer(); if (userSettings.focusLocked()) return this.myPlayer(); return this._focusedPlayer; } setFocusedPlayer(player: PlayerView | null): void { this._focusedPlayer = player; } }