mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 18:16:38 +00:00
597 lines
14 KiB
TypeScript
597 lines
14 KiB
TypeScript
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<PlayerActions> {
|
|
return this.game.worker.playerInteraction(
|
|
this.id(),
|
|
this.game.x(tile),
|
|
this.game.y(tile),
|
|
);
|
|
}
|
|
|
|
async borderTiles(): Promise<PlayerBorderTiles> {
|
|
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<PlayerProfile> {
|
|
return this.game.worker.playerProfile(this.smallID());
|
|
}
|
|
|
|
bestTransportShipSpawn(targetTile: TileRef): Promise<TileRef | false> {
|
|
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<number, PlayerID>();
|
|
private _players = new Map<PlayerID, PlayerView>();
|
|
private _units = new Map<number, UnitView>();
|
|
private updatedTiles: TileRef[] = [];
|
|
|
|
private _myPlayer: PlayerView | null = null;
|
|
private _focusedPlayer: PlayerView | null = null;
|
|
|
|
private unitGrid: UnitGrid;
|
|
|
|
private toDelete = new Set<number>();
|
|
|
|
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<TileRef> {
|
|
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;
|
|
}
|
|
}
|