Files
OpenFrontIO/src/core/game/GameView.ts
Aotumuri 0402e609a4 replaces player names with randomized name (#340)
This PR replaces player names with randomized name
The goal is to reduce exposure to inappropriate or offensive names.
Additionally, content creators no longer need to worry about displaying
other players’ usernames.
<img width="1276" alt="スクリーンショット 2025-03-25 23 03 37"
src="https://github.com/user-attachments/assets/3d396bb0-336f-41a0-8d56-ff5229fe05f8"
/>
<img width="1048" alt="スクリーンショット 2025-03-25 23 03 48"
src="https://github.com/user-attachments/assets/a72711cf-9743-4879-8f2f-b8187b10a272"
/>
Use the upper left button (Ninja) to change settings.
<img width="1173" alt="スクリーンショット 2025-03-25 23 04 03"
src="https://github.com/user-attachments/assets/2d2fcbbd-7342-40b0-97c1-ecc184e5fbb6"
/>

Fixes #489

---------

Co-authored-by: evanpelle <evanpelle@gmail.com>
2025-04-22 20:26:45 -07:00

583 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;
}
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;
}
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;
}
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;
}
}