diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 416cdcadd..fb4d779d6 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -124,6 +124,8 @@ export class ClientGameRunner { this.renderer.initialize() this.input.initialize() this.worker.start((gu: GameUpdateViewData) => { + const size = gu.packedTileUpdates.length * 4 / 1000 + console.log(`game update size: ${size}kb`) this.gameView.update(gu) this.renderer.tick() }) diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts index fc34ed53c..161c22646 100644 --- a/src/client/graphics/layers/TerritoryLayer.ts +++ b/src/client/graphics/layers/TerritoryLayer.ts @@ -36,6 +36,10 @@ export class TerritoryLayer implements Layer { } tick() { + this.game.recentlyUpdatedTiles() + .forEach(t => this.enqueue(t)) + + if (!this.game.inSpawnPhase()) { return } diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts index 6b8b99a78..b5a36a63a 100644 --- a/src/core/GameRunner.ts +++ b/src/core/GameRunner.ts @@ -5,7 +5,7 @@ import { WinCheckExecution } from "./execution/WinCheckExecution"; import { Game, MutableGame, MutableTile, PlayerID, Tile, TileEvent } from "./game/Game"; import { createGame } from "./game/GameImpl"; import { loadTerrainMap } from "./game/TerrainMapLoader"; -import { GameUpdateViewData, PlayerViewData } from "./GameView"; +import { GameUpdateViewData, packTileData, PlayerViewData } from "./GameView"; import { GameConfig, Turn } from "./Schemas"; export async function createGameRunner(gameID: string, gameConfig: GameConfig, callBack: (gu: GameUpdateViewData) => void): Promise { @@ -19,7 +19,8 @@ export async function createGameRunner(gameID: string, gameConfig: GameConfig, c } export class GameRunner { - private updatedTiles: MutableTile[] + private updatedTiles: Set = new Set() + private borderOnlyUpdated: Set = new Set() private tickInterval = null private turns: Turn[] = [] private currTurn = 0 @@ -34,7 +35,14 @@ export class GameRunner { } init() { - this.eventBus.on(TileEvent, (e) => { this.updatedTiles.push(e.tile as MutableTile) }) + this.eventBus.on(TileEvent, (e) => { + this.updatedTiles.add(e.tile as MutableTile) + if (e.borderOnlyChange) { + this.borderOnlyUpdated.add(e.tile as MutableTile) + } else { + this.updatedTiles.add(e.tile as MutableTile) + } + }) this.game.addExecution(...this.execManager.spawnBots(this.game.config().numBots())) if (this.game.config().spawnNPCs()) { this.game.addExecution(...this.execManager.fakeHumanExecutions()) @@ -55,19 +63,34 @@ export class GameRunner { return } this.isExecuting = true - this.updatedTiles = [] + this.updatedTiles.clear() + this.borderOnlyUpdated.clear() this.game.addExecution(...this.execManager.createExecs(this.turns[this.currTurn])) this.currTurn++ this.game.executeNextTick() + + + + this.updatedTiles.forEach(t => this.borderOnlyUpdated.delete(t)) + const updatedData = Array.from(this.updatedTiles).map(t => t.toViewData()) + updatedData.forEach(t => t.borderOnlyChange = false) + + const borderOnlyData = Array.from(this.borderOnlyUpdated).map(t => t.toViewData()) + borderOnlyData.forEach(t => t.borderOnlyChange = true) + + updatedData.concat(borderOnlyData) + + + this.callBack({ tick: this.game.ticks(), units: this.game.units().map(u => u.toViewData()), - tileUpdates: this.updatedTiles.map(t => t.toViewData()), + packedTileUpdates: updatedData.map(d => packTileData(d)), players: Object.fromEntries( - this.game.players().map(p => [p.id(), p.toViewData()]) + this.game.allPlayers().map(p => [p.id(), p.toViewData()]) ) as Record }) diff --git a/src/core/GameView.ts b/src/core/GameView.ts index 9d8357b19..90e0036a8 100644 --- a/src/core/GameView.ts +++ b/src/core/GameView.ts @@ -15,15 +15,16 @@ export interface ViewData { export interface TileViewData extends ViewData { x: number y: number - owner: PlayerID, + smallID: number, hasFallout: boolean hasDefenseBonus: boolean isBorder: boolean + borderOnlyChange: boolean } export class TileView { - constructor(private game: GameView, private data: TileViewData, private _terrain: TerrainTile) { } + constructor(private game: GameView, public data: TileViewData, private _terrain: TerrainTile) { } type(): TerrainType { return this._terrain.type() @@ -32,10 +33,10 @@ export class TileView { if (!this.hasOwner()) { return new TerraNulliusImpl() } - return this.game.player(this.data?.owner) + return this.game.playerBySmallID(this.data?.smallID) } hasOwner(): boolean { - return this.data?.owner != undefined + return this.data?.smallID !== undefined && this.data.smallID !== 0; } isBorder(): boolean { return this.data?.isBorder @@ -50,16 +51,15 @@ export class TileView { return this._terrain } - neighbors(): TileView[] { - throw new Error("Method not implemented."); + neighbors(): Tile[] { + return this._terrain.neighbors().map(t => this.game.tile(t.cell())) } - hasDefenseBonus(): boolean { - throw new Error("Method not implemented."); + return this.data?.hasDefenseBonus ?? false } cost(): number { - throw new Error("Method not implemented."); + return this._terrain.cost() } } @@ -104,6 +104,7 @@ export interface PlayerViewData extends ViewData { name: string, displayName: string, id: PlayerID, + smallID: number, type: PlayerType, isAlive: boolean, tilesOwned: number, @@ -117,6 +118,9 @@ export interface PlayerViewData extends ViewData { export class PlayerView implements Player { constructor(private game: GameView, private data: PlayerViewData) { } + smallID(): number { + return this.data.smallID + } lastTileChange(): Tick { return 0 } @@ -221,7 +225,7 @@ export class PlayerView implements Player { return false } info(): PlayerInfo { - return null + return new PlayerInfo(this.name(), this.type(), this.clientID(), this.id()) } } @@ -229,32 +233,47 @@ export interface GameUpdateViewData extends ViewData { tick: number units: UnitViewData[] players: Record - tileUpdates: TileViewData[] + tileUpdates?: TileViewData[] + packedTileUpdates: Uint16Array[] } export class GameView { private data: GameUpdateViewData - private tiles: TileViewData[][] = [] + private tiles: TileView[][] = [] + private smallIDToID = new Map() constructor(private _config: Config, private _terrainMap: TerrainMap) { + // Initialize the 2D array this.tiles = Array(_terrainMap.width()).fill(null).map(() => Array(_terrainMap.height()).fill(null)); + + // Fill the array with new TileView objects + for (let x = 0; x < _terrainMap.width(); x++) { + for (let y = 0; y < _terrainMap.height(); y++) { + this.tiles[x][y] = new TileView(this, null, _terrainMap.terrain(new Cell(x, y))); + } + } this.data = { tick: 0, units: [], tileUpdates: [], + packedTileUpdates: [], players: {} } } public update(gu: GameUpdateViewData) { this.data = gu + this.data.tileUpdates = this.data.packedTileUpdates.map(tu => unpackTileData(tu)) + Object.entries(gu.players).forEach(([key, value]) => { + this.smallIDToID.set(value.smallID, key); + }); gu.tileUpdates.forEach(tu => { - this.tiles[tu.x][tu.y] = tu + this.tiles[tu.x][tu.y].data = tu }) } recentlyUpdatedTiles(): TileView[] { - return this.data.tileUpdates.map(tu => new TileView(this, tu, this._terrainMap.terrain(new Cell(tu.x, tu.y)))) + return this.data.tileUpdates.filter(d => true).map(tu => new TileView(this, tu, this._terrainMap.terrain(new Cell(tu.x, tu.y)))) } player(id: PlayerID): Player { @@ -263,6 +282,14 @@ export class GameView { } throw Error(`player id ${id} not found`) } + + playerBySmallID(id: number): Player { + if (!this.smallIDToID.has(id)) { + throw new Error(`small id ${id} not found`) + } + return this.player(this.smallIDToID.get(id)) + } + playerByClientID(id: ClientID): Player | null { return null } @@ -273,7 +300,7 @@ export class GameView { return [] } tile(cell: Cell): Tile { - return new TileView(this, this.tiles[cell.x][cell.y], this._terrainMap.terrain(cell)) + return this.tiles[cell.x][cell.y] } isOnMap(cell: Cell): boolean { return this._terrainMap.isOnMap(cell) @@ -312,4 +339,31 @@ export class GameView { terrainMap(): TerrainMap { return this._terrainMap } +} + +export function packTileData(tile: TileViewData): Uint16Array { + const packed = new Uint16Array(4); + packed[0] = tile.x; + packed[1] = tile.y; + packed[2] = tile.smallID; + + // Pack booleans into bits + packed[3] = (tile.hasFallout ? 1 : 0) | + (tile.hasDefenseBonus ? 2 : 0) | + (tile.isBorder ? 4 : 0) | + (tile.borderOnlyChange ? 8 : 0) + + return packed; +} + +export function unpackTileData(packed: Uint16Array): TileViewData { + return { + x: packed[0], + y: packed[1], + smallID: packed[2], + hasFallout: !!(packed[3] & 1), + hasDefenseBonus: !!(packed[3] & 2), + isBorder: !!(packed[3] & 4), + borderOnlyChange: !!(packed[4] & 8) + }; } \ No newline at end of file diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts index c3806e19f..6f7e53783 100644 --- a/src/core/game/Game.ts +++ b/src/core/game/Game.ts @@ -227,6 +227,7 @@ export interface TerraNullius { } export interface Player { + smallID(): number info(): PlayerInfo name(): string displayName(): string @@ -347,6 +348,7 @@ export interface MutableGame extends Game { player(id: PlayerID): MutablePlayer playerByClientID(id: ClientID): MutablePlayer | null players(): MutablePlayer[] + allPlayers(): MutablePlayer[] addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer executions(): Execution[] units(...types: UnitType[]): MutableUnit[] @@ -356,7 +358,7 @@ export interface MutableGame extends Game { } export class TileEvent implements GameEvent { - constructor(public readonly tile: Tile) { } + constructor(public readonly tile: Tile, public readonly borderOnlyChange: boolean = false) { } } export class PlayerEvent implements GameEvent { diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts index 7ecaaa2ee..b485c36e9 100644 --- a/src/core/game/GameImpl.ts +++ b/src/core/game/GameImpl.ts @@ -40,6 +40,8 @@ export class GameImpl implements MutableGame { allianceRequests: AllianceRequestImpl[] = [] alliances_: AllianceImpl[] = [] + private nextID = 1 + constructor( private _terrainMap: TerrainMapImpl, @@ -211,6 +213,10 @@ export class GameImpl implements MutableGame { return Array.from(this._players.values()).filter(p => p.isAlive()) } + allPlayers(): MutablePlayer[] { + return Array.from(this._players.values()) + } + executions(): Execution[] { return [...this.execs, ...this.unInitExecs] } @@ -245,7 +251,8 @@ export class GameImpl implements MutableGame { } addPlayer(playerInfo: PlayerInfo, manpower: number): MutablePlayer { - let player = new PlayerImpl(this, playerInfo, manpower) + let player = new PlayerImpl(this, this.nextID, playerInfo, manpower) + this.nextID++ this._players.set(playerInfo.id, player) this.eventBus.emit(new PlayerEvent(player)) return player @@ -365,6 +372,7 @@ export class GameImpl implements MutableGame { tile.neighbors().forEach(t => tiles.push(t as TileImpl)) for (const t of tiles) { + this.eventBus.emit(new TileEvent(t, true)) if (!t.hasOwner()) { t._isBorder = false continue diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts index 46549c782..11edd1445 100644 --- a/src/core/game/PlayerImpl.ts +++ b/src/core/game/PlayerImpl.ts @@ -46,7 +46,8 @@ export class PlayerImpl implements MutablePlayer { private relations = new Map() - constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, startPopulation: number) { + + constructor(private gs: GameImpl, private _smallID: number, private readonly playerInfo: PlayerInfo, startPopulation: number) { this._name = playerInfo.name; this._targetTroopRatio = 1 this._troops = startPopulation * this._targetTroopRatio; @@ -61,6 +62,7 @@ export class PlayerImpl implements MutablePlayer { name: this.name(), displayName: this.displayName(), id: this.id(), + smallID: this.smallID(), type: this.type(), isAlive: this.isAlive(), tilesOwned: this.numTilesOwned(), @@ -73,6 +75,10 @@ export class PlayerImpl implements MutablePlayer { } } + smallID(): number { + return this._smallID + } + name(): string { return this._name; } diff --git a/src/core/game/TileImpl.ts b/src/core/game/TileImpl.ts index e883703d2..70a1322bd 100644 --- a/src/core/game/TileImpl.ts +++ b/src/core/game/TileImpl.ts @@ -27,10 +27,11 @@ export class TileImpl implements MutableTile { return { x: this._cell.x, y: this._cell.y, - owner: this._owner?.id(), + smallID: this._owner.isPlayer() ? this._owner.smallID() : 0, hasFallout: this._hasFallout, hasDefenseBonus: this.hasDefenseBonus(), - isBorder: this.isBorder() + isBorder: this.isBorder(), + borderOnlyChange: false, } }