;
-}
+};
-export interface CenterButtonElement {
+export type CenterButtonElement = {
disabled: (params: MenuElementParams) => boolean;
action: (params: MenuElementParams) => void;
-}
+};
export const COLORS = {
build: "#ebe250",
@@ -208,7 +208,7 @@ const allyDonateGoldElement: MenuElement = {
id: "ally_donate_gold",
name: "donate gold",
disabled: (params: MenuElementParams) =>
- !params.playerActions?.interaction?.canDonate,
+ !params.playerActions?.interaction?.canDonateGold,
color: COLORS.ally,
icon: donateGoldIcon,
action: (params: MenuElementParams) => {
@@ -221,7 +221,7 @@ const allyDonateTroopsElement: MenuElement = {
id: "ally_donate_troops",
name: "donate troops",
disabled: (params: MenuElementParams) =>
- !params.playerActions?.interaction?.canDonate,
+ !params.playerActions?.interaction?.canDonateTroops,
color: COLORS.ally,
icon: donateTroopIcon,
action: (params: MenuElementParams) => {
@@ -566,8 +566,7 @@ export const rootMenuElement: MenuElement = {
const tileOwner = params.game.owner(params.tile);
const isOwnTerritory =
- tileOwner.isPlayer() &&
- (tileOwner as PlayerView).id() === params.myPlayer.id();
+ tileOwner.isPlayer() && tileOwner.id() === params.myPlayer.id();
const menuItems: (MenuElement | null)[] = [
infoMenuElement,
diff --git a/src/client/graphics/layers/RailroadLayer.ts b/src/client/graphics/layers/RailroadLayer.ts
index 1a77cb77a..8d111b93e 100644
--- a/src/client/graphics/layers/RailroadLayer.ts
+++ b/src/client/graphics/layers/RailroadLayer.ts
@@ -8,7 +8,7 @@ import {
RailTile,
RailType,
} from "../../../core/game/GameUpdates";
-import { GameView, PlayerView } from "../../../core/game/GameView";
+import { GameView } from "../../../core/game/GameView";
import { Layer } from "./Layer";
import { getRailroadRects } from "./RailroadSprites";
@@ -151,7 +151,7 @@ export class RailroadLayer implements Layer {
const x = this.game.x(railRoad.tile);
const y = this.game.y(railRoad.tile);
const owner = this.game.owner(railRoad.tile);
- const recipient = owner.isPlayer() ? (owner as PlayerView) : null;
+ const recipient = owner.isPlayer() ? owner : null;
const color = recipient
? this.theme.railroadColor(recipient)
: new Colord({ r: 255, g: 255, b: 255, a: 1 });
diff --git a/src/client/graphics/layers/ReplayPanel.ts b/src/client/graphics/layers/ReplayPanel.ts
index e68288596..a2fad7bba 100644
--- a/src/client/graphics/layers/ReplayPanel.ts
+++ b/src/client/graphics/layers/ReplayPanel.ts
@@ -12,8 +12,8 @@ import { Layer } from "./Layer";
export class ShowReplayPanelEvent {
constructor(
- public visible: boolean = true,
- public isSingleplayer: boolean = false,
+ public visible = true,
+ public isSingleplayer = false,
) {}
}
@@ -23,7 +23,7 @@ export class ReplayPanel extends LitElement implements Layer {
public eventBus: EventBus | undefined;
@property({ type: Boolean })
- visible: boolean = false;
+ visible = false;
@state()
private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier;
diff --git a/src/client/graphics/layers/SettingsModal.ts b/src/client/graphics/layers/SettingsModal.ts
index 54f0205b9..69b53ec9f 100644
--- a/src/client/graphics/layers/SettingsModal.ts
+++ b/src/client/graphics/layers/SettingsModal.ts
@@ -1,5 +1,5 @@
import { html, LitElement } from "lit";
-import { customElement, query, state } from "lit/decorators.js";
+import { customElement, property, query, state } from "lit/decorators.js";
import structureIcon from "../../../../resources/images/CityIconWhite.svg";
import darkModeIcon from "../../../../resources/images/DarkModeIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
@@ -12,11 +12,16 @@ import treeIcon from "../../../../resources/images/TreeIconWhite.svg";
import { EventBus } from "../../../core/EventBus";
import { UserSettings } from "../../../core/game/UserSettings";
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
+import { PauseGameEvent } from "../../Transport";
import { translateText } from "../../Utils";
import { Layer } from "./Layer";
export class ShowSettingsModalEvent {
- constructor(public readonly isVisible: boolean = true) {}
+ constructor(
+ public readonly isVisible = true,
+ public readonly shouldPause = false,
+ public readonly isPaused = false,
+ ) {}
}
@customElement("settings-modal")
@@ -25,17 +30,26 @@ export class SettingsModal extends LitElement implements Layer {
public userSettings: UserSettings;
@state()
- private isVisible: boolean = false;
+ private isVisible = false;
@state()
- private alternateView: boolean = false;
+ private alternateView = false;
@query(".modal-overlay")
private modalOverlay!: HTMLElement;
+ @property({ type: Boolean })
+ shouldPause = false;
+
+ @property({ type: Boolean })
+ wasPausedWhenOpened = false;
+
init() {
this.eventBus.on(ShowSettingsModalEvent, (event) => {
this.isVisible = event.isVisible;
+ this.shouldPause = event.shouldPause;
+ this.wasPausedWhenOpened = event.isPaused;
+ this.pauseGame(true);
});
}
@@ -81,6 +95,12 @@ export class SettingsModal extends LitElement implements Layer {
this.isVisible = false;
document.body.style.overflow = "";
this.requestUpdate();
+ this.pauseGame(false);
+ }
+
+ private pauseGame(pause: boolean) {
+ if (this.shouldPause && !this.wasPausedWhenOpened)
+ this.eventBus.emit(new PauseGameEvent(pause));
}
private onTerrainButtonClick() {
@@ -354,8 +374,8 @@ export class SettingsModal extends LitElement implements Layer {
${this.userSettings.performanceOverlay()
? translateText("user_setting.performance_overlay_enabled")
: translateText(
- "user_setting.performance_overlay_disabled",
- )}
+ "user_setting.performance_overlay_disabled",
+ )}
diff --git a/src/client/graphics/layers/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts
index f8f39294f..311d2dcc2 100644
--- a/src/client/graphics/layers/SpawnAd.ts
+++ b/src/client/graphics/layers/SpawnAd.ts
@@ -13,12 +13,12 @@ export class SpawnAd extends LitElement implements Layer {
public g: GameView;
@state()
- private isVisible: boolean = false;
+ private isVisible = false;
@state()
- private adLoaded: boolean = false;
+ private adLoaded = false;
- private gamesPlayed: number = 0;
+ private gamesPlayed = 0;
// Override createRenderRoot to disable shadow DOM
createRenderRoot() {
diff --git a/src/client/graphics/layers/StructureIconsLayer.ts b/src/client/graphics/layers/StructureIconsLayer.ts
index f28c05a88..5c6169ab4 100644
--- a/src/client/graphics/layers/StructureIconsLayer.ts
+++ b/src/client/graphics/layers/StructureIconsLayer.ts
@@ -19,15 +19,15 @@ import { Layer } from "./Layer";
type ShapeType = "triangle" | "square" | "pentagon" | "octagon" | "circle";
class StructureRenderInfo {
- public isOnScreen: boolean = false;
+ public isOnScreen = false;
constructor(
public unit: UnitView,
public owner: PlayerID,
public iconContainer: PIXI.Container,
public levelContainer: PIXI.Container,
public dotContainer: PIXI.Container,
- public level: number = 0,
- public underConstruction: boolean = true,
+ public level = 0,
+ public underConstruction = true,
) {}
}
@@ -58,7 +58,7 @@ export class StructureIconsLayer implements Layer {
private iconsStage: PIXI.Container;
private levelsStage: PIXI.Container;
private dotsStage: PIXI.Container;
- private shouldRedraw: boolean = true;
+ private shouldRedraw = true;
private textureCache: Map = new Map();
private theme: Theme;
private renderer: PIXI.Renderer;
@@ -353,12 +353,12 @@ export class StructureIconsLayer implements Layer {
const shape = STRUCTURE_SHAPES[structureType];
const texture = shape
? this.createIcon(
- unit.owner(),
- structureType,
- isConstruction,
- shape,
- renderIcon,
- )
+ unit.owner(),
+ structureType,
+ isConstruction,
+ shape,
+ renderIcon,
+ )
: PIXI.Texture.EMPTY;
this.textureCache.set(cacheKey, texture);
diff --git a/src/client/graphics/layers/StructureLayer.ts b/src/client/graphics/layers/StructureLayer.ts
index f999edf09..46fe69dc6 100644
--- a/src/client/graphics/layers/StructureLayer.ts
+++ b/src/client/graphics/layers/StructureLayer.ts
@@ -23,11 +23,11 @@ const BASE_TERRITORY_RADIUS = 13.5;
const RADIUS_SCALE_FACTOR = 0.5;
const ZOOM_THRESHOLD = 4.3; // below this zoom level, structures are not rendered
-interface UnitRenderConfig {
+type UnitRenderConfig = {
icon: string;
borderRadius: number;
territoryRadius: number;
-}
+};
export class StructureLayer implements Layer {
private canvas: HTMLCanvasElement;
diff --git a/src/client/graphics/layers/TeamStats.ts b/src/client/graphics/layers/TeamStats.ts
index 687d807c6..2a1af49d6 100644
--- a/src/client/graphics/layers/TeamStats.ts
+++ b/src/client/graphics/layers/TeamStats.ts
@@ -6,14 +6,14 @@ import { GameView, PlayerView } from "../../../core/game/GameView";
import { renderNumber, translateText } from "../../Utils";
import { Layer } from "./Layer";
-interface TeamEntry {
+type TeamEntry = {
teamName: string;
totalScoreStr: string;
totalGold: string;
totalTroops: string;
totalScoreSort: number;
players: PlayerView[];
-}
+};
@customElement("team-stats")
export class TeamStats extends LitElement implements Layer {
diff --git a/src/client/graphics/layers/TerritoryLayer.ts b/src/client/graphics/layers/TerritoryLayer.ts
index e104559d2..045e5adf2 100644
--- a/src/client/graphics/layers/TerritoryLayer.ts
+++ b/src/client/graphics/layers/TerritoryLayer.ts
@@ -391,7 +391,7 @@ export class TerritoryLayer implements Layer {
}
}
- paintTerritory(tile: TileRef, isBorder: boolean = false) {
+ paintTerritory(tile: TileRef, isBorder = false) {
if (isBorder && !this.game.hasOwner(tile)) {
return;
}
diff --git a/src/client/jwt.ts b/src/client/jwt.ts
index c6f6ba381..8b365adf6 100644
--- a/src/client/jwt.ts
+++ b/src/client/jwt.ts
@@ -77,7 +77,7 @@ export function getAuthHeader(): string {
return `Bearer ${token}`;
}
-export async function logOut(allSessions: boolean = false) {
+export async function logOut(allSessions = false) {
const token = getToken();
if (token === null) return;
clearToken();
diff --git a/src/client/utilities/RenderUnitTypeOptions.ts b/src/client/utilities/RenderUnitTypeOptions.ts
index 0392935d6..725f2bff5 100644
--- a/src/client/utilities/RenderUnitTypeOptions.ts
+++ b/src/client/utilities/RenderUnitTypeOptions.ts
@@ -3,10 +3,10 @@ import { html, TemplateResult } from "lit";
import { UnitType } from "../../core/game/Game";
import { translateText } from "../Utils";
-export interface UnitTypeRenderContext {
+export type UnitTypeRenderContext = {
disabledUnits: UnitType[];
toggleUnit: (unit: UnitType, checked: boolean) => void;
-}
+};
const unitOptions: { type: UnitType; translationKey: string }[] = [
{ type: UnitType.City, translationKey: "unit_type.city" },
diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts
index e4d3f74fa..493fab6f4 100644
--- a/src/core/ApiSchemas.ts
+++ b/src/core/ApiSchemas.ts
@@ -1,3 +1,4 @@
+// This file contains schemas for api.openfront.io
import { z } from "zod";
import { base64urlToUuid } from "./Base64";
@@ -48,3 +49,19 @@ export const UserMeResponseSchema = z.object({
}),
});
export type UserMeResponse = z.infer;
+
+export const StripeCreateCheckoutSessionResponseSchema = z.object({
+ id: z.string(),
+ object: z.literal("checkout.session"),
+ url: z.string(),
+ payment_status: z.enum(["paid", "unpaid", "no_payment_required"]),
+ status: z.enum(["open", "complete", "expired"]),
+ client_reference_id: z.string().optional(),
+ customer: z.string().optional(),
+ payment_intent: z.string().optional(),
+ subscription: z.string().optional(),
+ metadata: z.partialRecord(z.string(), z.string()),
+});
+export type StripeCreateCheckoutSessionResponse = z.infer<
+ typeof StripeCreateCheckoutSessionResponseSchema
+>;
diff --git a/src/core/CosmeticSchemas.ts b/src/core/CosmeticSchemas.ts
index f2855ab18..af3469fe1 100644
--- a/src/core/CosmeticSchemas.ts
+++ b/src/core/CosmeticSchemas.ts
@@ -1,4 +1,4 @@
-import { z } from "zod/v4";
+import { z } from "zod";
import { RequiredPatternSchema } from "./Schemas";
export const ProductSchema = z.object({
@@ -25,7 +25,7 @@ export const CosmeticsSchema = z.object({
z.string(),
z.object({
name: z.string(),
- flares: z.array(z.string()).optional(),
+ flares: z.string().array().optional(),
}),
),
color: z.record(
@@ -33,7 +33,7 @@ export const CosmeticsSchema = z.object({
z.object({
color: z.string(),
name: z.string(),
- flares: z.array(z.string()).optional(),
+ flares: z.string().array().optional(),
}),
),
})
diff --git a/src/core/CustomFlag.ts b/src/core/CustomFlag.ts
index 18827e599..815ff8fff 100644
--- a/src/core/CustomFlag.ts
+++ b/src/core/CustomFlag.ts
@@ -1,16 +1,14 @@
import { Cosmetics } from "./CosmeticSchemas";
const ANIMATION_DURATIONS: Record = {
- rainbow: 4000,
- /* eslint-disable sort-keys */
"bright-rainbow": 4000,
"copper-glow": 3000,
- "silver-glow": 3000,
"gold-glow": 3000,
- neon: 3000,
- lava: 6000,
- /* eslint-enable sort-keys */
- water: 6200,
+ "lava": 6000,
+ "neon": 3000,
+ "rainbow": 4000,
+ "silver-glow": 3000,
+ "water": 6200,
};
// TODO: Pass in cosmetics as a parameter when
diff --git a/src/core/EventBus.ts b/src/core/EventBus.ts
index 008cc7320..d696ee013 100644
--- a/src/core/EventBus.ts
+++ b/src/core/EventBus.ts
@@ -1,8 +1,9 @@
export type GameEvent = object;
-export interface EventConstructor {
- new (...args: any[]): T;
-}
+export type EventConstructor = new (
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ...args: any[]
+) => T;
export class EventBus {
private listeners: Map void>> =
diff --git a/src/core/ExpressSchemas.ts b/src/core/ExpressSchemas.ts
new file mode 100644
index 000000000..fbbc6fb90
--- /dev/null
+++ b/src/core/ExpressSchemas.ts
@@ -0,0 +1,15 @@
+// This file contians schemas for the primary openfront express server
+import { z } from "zod";
+import { GameInfoSchema } from "./Schemas";
+
+export const ApiEnvResponseSchema = z.object({
+ game_env: z.string(),
+});
+export type ApiEnvResponse = z.infer;
+
+export const ApiPublicLobbiesResponseSchema = z.object({
+ lobbies: GameInfoSchema.array(),
+});
+export type ApiPublicLobbiesResponse = z.infer<
+ typeof ApiPublicLobbiesResponseSchema
+>;
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 403c353fd..186232380 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -57,13 +57,13 @@ export async function createGameRunner(
const nations = gameStart.config.disableNPCs
? []
: gameMap.manifest.nations.map(
- (n) =>
- new Nation(
- new Cell(n.coordinates[0], n.coordinates[1]),
- n.strength,
- new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()),
- ),
- );
+ (n) =>
+ new Nation(
+ new Cell(n.coordinates[0], n.coordinates[1]),
+ n.strength,
+ new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()),
+ ),
+ );
const game: Game = createGame(
humans,
@@ -190,14 +190,15 @@ export class GameRunner {
const other = this.game.owner(tile) as Player;
actions.interaction = {
canBreakAlliance: player.isAlliedWith(other),
- canDonate: player.canDonate(other),
+ canDonateGold: player.canDonateGold(other),
+ canDonateTroops: player.canDonateTroops(other),
canEmbargo: !player.hasEmbargoAgainst(other),
canSendAllianceRequest: player.canSendAllianceRequest(other),
canSendEmoji: player.canSendEmoji(other),
canTarget: player.canTarget(other),
sharedBorder: player.sharesBorderWith(other),
};
- const alliance = player.allianceWith(other as Player);
+ const alliance = player.allianceWith(other);
if (alliance) {
actions.interaction.allianceExpiresAt = alliance.expiresAt();
}
diff --git a/src/core/PseudoRandom.ts b/src/core/PseudoRandom.ts
index 495796a77..35e092275 100644
--- a/src/core/PseudoRandom.ts
+++ b/src/core/PseudoRandom.ts
@@ -4,9 +4,9 @@ export class PseudoRandom {
private state1: number;
// Keep these variables to maintain the exact same interface
- private m: number = 0x80000000; // 2**31
- private a: number = 1103515245;
- private c: number = 12345;
+ private m = 0x80000000; // 2**31
+ private a = 1103515245;
+ private c = 12345;
private state: number;
private static readonly POW36_8 = Math.pow(36, 8); // Pre-compute 36^8
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index e96d0c17d..3bca9fe5e 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -112,17 +112,6 @@ export type Player = z.infer;
export type GameStartInfo = z.infer;
const PlayerTypeSchema = z.enum(PlayerType);
-export interface GameInfo {
- gameID: GameID;
- clients?: ClientInfo[];
- numClients?: number;
- msUntilStart?: number;
- gameConfig?: GameConfig;
-}
-export interface ClientInfo {
- clientID: ClientID;
- username: string;
-}
export enum LogSeverity {
Debug = "DEBUG",
Info = "INFO",
@@ -148,6 +137,8 @@ export const GameConfigSchema = z.object({
difficulty: z.enum(Difficulty),
disableNPCs: z.boolean(),
disabledUnits: z.enum(UnitType).array().optional(),
+ donateGold: z.boolean(),
+ donateTroops: z.boolean(),
gameMap: z.enum(GameMapType),
gameMode: z.enum(GameMode),
gameType: z.enum(GameType),
@@ -192,6 +183,22 @@ export const ID = z
export const AllPlayersStatsSchema = z.record(ID, PlayerStatsSchema);
export const UsernameSchema = SafeString;
+
+export const ClientInfoSchema = z.object({
+ clientID: ID,
+ username: UsernameSchema,
+});
+export type ClientInfo = z.infer;
+
+export const GameInfoSchema = z.object({
+ clients: ClientInfoSchema.array().optional(),
+ gameConfig: GameConfigSchema.optional(),
+ gameID: ID,
+ msUntilStart: z.number().int().nonnegative().optional(),
+ numClients: z.number().int().nonnegative().optional(),
+});
+export type GameInfo = z.infer;
+
const countryCodes = countries.map((c) => c.code);
export const FlagSchema = z
.string()
diff --git a/src/core/Util.ts b/src/core/Util.ts
index 543d8b442..30a4ee97b 100644
--- a/src/core/Util.ts
+++ b/src/core/Util.ts
@@ -285,6 +285,7 @@ export const flattenedEmojiTable: string[] = emojiTable.flat();
/**
* JSON.stringify replacer function that converts bigint values to strings.
*/
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function replacer(_key: string, value: any): any {
return typeof value === "bigint" ? value.toString() : value;
}
diff --git a/src/core/WorkerSchemas.ts b/src/core/WorkerSchemas.ts
index 0a06b1571..dbda38b10 100644
--- a/src/core/WorkerSchemas.ts
+++ b/src/core/WorkerSchemas.ts
@@ -1,5 +1,6 @@
+// This file contians schemas for the openfront worker express server
import { z } from "zod";
-import { GameConfigSchema } from "./Schemas";
+import { GameConfigSchema, GameRecordSchema } from "./Schemas";
export const CreateGameInputSchema = GameConfigSchema.or(
z
@@ -9,3 +10,33 @@ export const CreateGameInputSchema = GameConfigSchema.or(
);
export const GameInputSchema = GameConfigSchema.partial();
+
+export const WorkerApiGameIdExistsSchema = z.object({
+ exists: z.boolean(),
+});
+export type WorkerApiGameIdExists = z.infer;
+
+export const WorkerApiArchivedGameLobbySchema = z.union([
+ z.object({
+ error: z.literal("Game not found"),
+ exists: z.literal(false),
+ success: z.literal(false),
+ }),
+ z.object({
+ details: z.object({
+ actualCommit: z.string(),
+ expectedCommit: z.string(),
+ }),
+ error: z.literal("Version mismatch"),
+ exists: z.literal(true),
+ success: z.literal(false),
+ }),
+ z.object({
+ exists: z.literal(true),
+ gameRecord: GameRecordSchema,
+ success: z.literal(true),
+ }),
+]);
+export type WorkerApiArchivedGameLobby = z.infer<
+ typeof WorkerApiArchivedGameLobbySchema
+>;
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts
index 35090ad09..c9b7134e1 100644
--- a/src/core/configuration/Config.ts
+++ b/src/core/configuration/Config.ts
@@ -26,7 +26,7 @@ export enum GameEnv {
Prod,
}
-export interface ServerConfig {
+export type ServerConfig = {
turnIntervalMs(): number;
gameCreationRate(): number;
lobbyMaxPlayers(
@@ -62,14 +62,14 @@ export interface ServerConfig {
cloudflareCredsPath(): string;
stripePublishableKey(): string;
allowedFlares(): string[] | undefined;
-}
+};
-export interface NukeMagnitude {
+export type NukeMagnitude = {
inner: number;
outer: number;
-}
+};
-export interface Config {
+export type Config = {
samHittingChance(): number;
samWarheadHittingChance(): number;
spawnImmunityDuration(): Tick;
@@ -82,7 +82,9 @@ export interface Config {
isUnitDisabled(unitType: UnitType): boolean;
bots(): number;
infiniteGold(): boolean;
+ donateGold(): boolean;
infiniteTroops(): boolean;
+ donateTroops(): boolean;
instantBuild(): boolean;
numSpawnPhaseTurns(): number;
userSettings(): UserSettings;
@@ -168,9 +170,9 @@ export interface Config {
structureMinDist(): number;
isReplay(): boolean;
allianceExtensionPromptOffset(): number;
-}
+};
-export interface Theme {
+export type Theme = {
teamColor(team: Team): Colord;
territoryColor(playerInfo: PlayerView): Colord;
specialBuildingColor(playerInfo: PlayerView): Colord;
@@ -188,4 +190,4 @@ export interface Theme {
allyColor(): Colord;
enemyColor(): Colord;
spawnHighlightColor(): Colord;
-}
+};
diff --git a/src/core/configuration/ConfigLoader.ts b/src/core/configuration/ConfigLoader.ts
index 184902694..3bb7f53fc 100644
--- a/src/core/configuration/ConfigLoader.ts
+++ b/src/core/configuration/ConfigLoader.ts
@@ -1,3 +1,4 @@
+import { ApiEnvResponseSchema } from "../ExpressSchemas";
import { UserSettings } from "../game/UserSettings";
import { GameConfig } from "../Schemas";
import { Config, GameEnv, ServerConfig } from "./Config";
@@ -11,7 +12,7 @@ export let cachedSC: ServerConfig | null = null;
export async function getConfig(
gameConfig: GameConfig,
userSettings: UserSettings | null,
- isReplay: boolean = false,
+ isReplay = false,
): Promise {
const sc = await getServerConfigFromClient();
switch (sc.env()) {
@@ -36,7 +37,8 @@ export async function getServerConfigFromClient(): Promise {
`Failed to fetch server config: ${response.status} ${response.statusText}`,
);
}
- const config = await response.json();
+ const json = await response.json();
+ const config = ApiEnvResponseSchema.parse(json);
// Log the retrieved configuration
console.log("Server config loaded:", config);
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index fee1776ec..5814793c9 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -323,9 +323,15 @@ export class DefaultConfig implements Config {
infiniteGold(): boolean {
return this._gameConfig.infiniteGold;
}
+ donateGold(): boolean {
+ return this._gameConfig.donateGold;
+ }
infiniteTroops(): boolean {
return this._gameConfig.infiniteTroops;
}
+ donateTroops(): boolean {
+ return this._gameConfig.donateTroops;
+ }
trainSpawnRate(numberOfStations: number): number {
return Math.min(1400, Math.round(20 * Math.pow(numberOfStations, 0.5)));
}
diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts
index 69608357a..497cabd0e 100644
--- a/src/core/execution/AttackExecution.ts
+++ b/src/core/execution/AttackExecution.ts
@@ -18,7 +18,7 @@ const malusForRetreat = 25;
export class AttackExecution implements Execution {
private breakAlliance = false;
private wasAlliedAtInit = false; // Store alliance state at initialization
- private active: boolean = true;
+ private active = true;
private toConquer = new FlatBinaryHeap();
private random = new PseudoRandom(123);
@@ -34,7 +34,7 @@ export class AttackExecution implements Execution {
private _owner: Player,
private _targetID: PlayerID | null,
private sourceTile: TileRef | null = null,
- private removeTroops: boolean = true,
+ private removeTroops = true,
) {}
public targetID(): PlayerID | null {
@@ -69,7 +69,7 @@ export class AttackExecution implements Execution {
}
if (this.target && this.target.isPlayer()) {
- const targetPlayer = this.target as Player;
+ const targetPlayer = this.target;
if (
targetPlayer.type() !== PlayerType.Bot &&
this._owner.type() !== PlayerType.Bot
diff --git a/src/core/execution/CityExecution.ts b/src/core/execution/CityExecution.ts
index c037c12c2..f21e19f62 100644
--- a/src/core/execution/CityExecution.ts
+++ b/src/core/execution/CityExecution.ts
@@ -5,7 +5,7 @@ import { TrainStationExecution } from "./TrainStationExecution";
export class CityExecution implements Execution {
private mg: Game;
private city: Unit | null = null;
- private active: boolean = true;
+ private active = true;
constructor(
private player: Player,
@@ -48,7 +48,7 @@ export class CityExecution implements Execution {
createStation(): void {
if (this.city !== null) {
const nearbyFactory = this.mg.hasUnitNearby(
- this.city.tile()!,
+ this.city.tile(),
this.mg.config().trainStationMaxRange(),
UnitType.Factory,
this.player.id(),
diff --git a/src/core/execution/ConstructionExecution.ts b/src/core/execution/ConstructionExecution.ts
index 8217f497d..772fe7e23 100644
--- a/src/core/execution/ConstructionExecution.ts
+++ b/src/core/execution/ConstructionExecution.ts
@@ -20,7 +20,7 @@ import { WarshipExecution } from "./WarshipExecution";
export class ConstructionExecution implements Execution {
private construction: Unit | null = null;
- private active: boolean = true;
+ private active = true;
private mg: Game;
private ticksUntilComplete: Tick;
diff --git a/src/core/execution/DefensePostExecution.ts b/src/core/execution/DefensePostExecution.ts
index ab36f81ae..40eb9c969 100644
--- a/src/core/execution/DefensePostExecution.ts
+++ b/src/core/execution/DefensePostExecution.ts
@@ -5,7 +5,7 @@ import { ShellExecution } from "./ShellExecution";
export class DefensePostExecution implements Execution {
private mg: Game;
private post: Unit | null = null;
- private active: boolean = true;
+ private active = true;
private target: Unit | null = null;
private lastShellAttack = 0;
diff --git a/src/core/execution/DeleteUnitExecution.ts b/src/core/execution/DeleteUnitExecution.ts
index 424130eea..e1291c60b 100644
--- a/src/core/execution/DeleteUnitExecution.ts
+++ b/src/core/execution/DeleteUnitExecution.ts
@@ -1,7 +1,7 @@
import { Execution, Game, MessageType, Player } from "../game/Game";
export class DeleteUnitExecution implements Execution {
- private active: boolean = true;
+ private active = true;
private mg: Game;
constructor(
diff --git a/src/core/execution/DonateGoldExecution.ts b/src/core/execution/DonateGoldExecution.ts
index 5f43b10eb..214c7e915 100644
--- a/src/core/execution/DonateGoldExecution.ts
+++ b/src/core/execution/DonateGoldExecution.ts
@@ -25,7 +25,7 @@ export class DonateGoldExecution implements Execution {
tick(ticks: number): void {
if (this.gold === null) throw new Error("not initialized");
if (
- this.sender.canDonate(this.recipient) &&
+ this.sender.canDonateGold(this.recipient) &&
this.sender.donateGold(this.recipient, this.gold)
) {
this.recipient.updateRelation(this.sender, 50);
diff --git a/src/core/execution/DonateTroopExecution.ts b/src/core/execution/DonateTroopExecution.ts
index 9e98c514e..00af5de7c 100644
--- a/src/core/execution/DonateTroopExecution.ts
+++ b/src/core/execution/DonateTroopExecution.ts
@@ -28,7 +28,7 @@ export class DonateTroopsExecution implements Execution {
tick(ticks: number): void {
if (this.troops === null) throw new Error("not initialized");
if (
- this.sender.canDonate(this.recipient) &&
+ this.sender.canDonateTroops(this.recipient) &&
this.sender.donateTroops(this.recipient, this.troops)
) {
this.recipient.updateRelation(this.sender, 50);
diff --git a/src/core/execution/FactoryExecution.ts b/src/core/execution/FactoryExecution.ts
index fd24de674..f9065d0ef 100644
--- a/src/core/execution/FactoryExecution.ts
+++ b/src/core/execution/FactoryExecution.ts
@@ -4,7 +4,7 @@ import { TrainStationExecution } from "./TrainStationExecution";
export class FactoryExecution implements Execution {
private factory: Unit | null = null;
- private active: boolean = true;
+ private active = true;
private game: Game;
constructor(
private player: Player,
@@ -47,7 +47,7 @@ export class FactoryExecution implements Execution {
createStation(): void {
if (this.factory !== null) {
const structures = this.game.nearbyUnits(
- this.factory.tile()!,
+ this.factory.tile(),
this.game.config().trainStationMaxRange(),
[UnitType.City, UnitType.Port, UnitType.Factory],
);
diff --git a/src/core/execution/FakeHumanExecution.ts b/src/core/execution/FakeHumanExecution.ts
index 0f41f7e86..03637952c 100644
--- a/src/core/execution/FakeHumanExecution.ts
+++ b/src/core/execution/FakeHumanExecution.ts
@@ -297,7 +297,7 @@ export class FakeHumanExecution implements Execution {
UnitType.SAMLauncher,
);
const structureTiles = structures.map((u) => u.tile());
- const randomTiles: (TileRef | null)[] = new Array(10);
+ const randomTiles: (TileRef | null)[] = new Array(10).fill(null);
for (let i = 0; i < randomTiles.length; i++) {
randomTiles[i] = this.randTerritoryTile(other);
}
@@ -455,8 +455,8 @@ export class FakeHumanExecution implements Execution {
const tiles =
type === UnitType.Port
? Array.from(this.player.borderTiles()).filter((t) =>
- this.mg.isOceanShore(t),
- )
+ this.mg.isOceanShore(t),
+ )
: Array.from(this.player.tiles());
if (tiles.length === 0) return null;
return this.random.randElement(tiles);
diff --git a/src/core/execution/MIRVExecution.ts b/src/core/execution/MIRVExecution.ts
index 9a6075b68..e627fc387 100644
--- a/src/core/execution/MIRVExecution.ts
+++ b/src/core/execution/MIRVExecution.ts
@@ -31,7 +31,7 @@ export class MirvExecution implements Execution {
private separateDst: TileRef;
- private speed: number = -1;
+ private speed = -1;
constructor(
private player: Player,
diff --git a/src/core/execution/NukeExecution.ts b/src/core/execution/NukeExecution.ts
index 739932a5c..5bfe1debe 100644
--- a/src/core/execution/NukeExecution.ts
+++ b/src/core/execution/NukeExecution.ts
@@ -28,7 +28,7 @@ export class NukeExecution implements Execution {
private player: Player,
private dst: TileRef,
private src?: TileRef | null,
- private speed: number = -1,
+ private speed = -1,
private waitTicks = 0,
) {}
diff --git a/src/core/execution/PlayerExecution.ts b/src/core/execution/PlayerExecution.ts
index db021bd7f..647b97eb2 100644
--- a/src/core/execution/PlayerExecution.ts
+++ b/src/core/execution/PlayerExecution.ts
@@ -28,11 +28,11 @@ export class PlayerExecution implements Execution {
tick(ticks: number) {
this.player.decayRelations();
this.player.units().forEach((u) => {
- const tileOwner = this.mg!.owner(u.tile());
+ const tileOwner = this.mg.owner(u.tile());
if (u.info().territoryBound) {
if (tileOwner.isPlayer()) {
if (tileOwner !== this.player) {
- this.mg!.player(tileOwner.id()).captureUnit(u);
+ this.mg.player(tileOwner.id()).captureUnit(u);
}
} else {
u.delete();
@@ -218,7 +218,7 @@ export class PlayerExecution implements Execution {
}
let largestNeighborAttack: Player | null = null;
- let largestTroopCount: number = 0;
+ let largestTroopCount = 0;
for (const id of neighborsIDs) {
const neighbor = this.mg.playerBySmallID(id);
if (!neighbor.isPlayer() || this.player.isFriendly(neighbor)) {
diff --git a/src/core/execution/PortExecution.ts b/src/core/execution/PortExecution.ts
index 6d3f45351..1e6bea8e8 100644
--- a/src/core/execution/PortExecution.ts
+++ b/src/core/execution/PortExecution.ts
@@ -90,7 +90,7 @@ export class PortExecution implements Execution {
createStation(): void {
if (this.port !== null) {
const nearbyFactory = this.mg.hasUnitNearby(
- this.port.tile()!,
+ this.port.tile(),
this.mg.config().trainStationMaxRange(),
UnitType.Factory,
this.player.id(),
diff --git a/src/core/execution/RailroadExecution.ts b/src/core/execution/RailroadExecution.ts
index 97f28f744..133bef25a 100644
--- a/src/core/execution/RailroadExecution.ts
+++ b/src/core/execution/RailroadExecution.ts
@@ -5,10 +5,10 @@ import { Railroad } from "../game/Railroad";
export class RailroadExecution implements Execution {
private mg: Game;
- private active: boolean = true;
- private headIndex: number = 0;
- private tailIndex: number = 0;
- private increment: number = 3;
+ private active = true;
+ private headIndex = 0;
+ private tailIndex = 0;
+ private increment = 3;
private railTiles: RailTile[] = [];
constructor(private railRoad: Railroad) {
this.tailIndex = railRoad.tiles.length;
@@ -43,9 +43,9 @@ export class RailroadExecution implements Execution {
railType:
tiles.length > 0
? this.computeExtremityDirection(
- tiles[tiles.length - 1],
- tiles[tiles.length - 2],
- )
+ tiles[tiles.length - 1],
+ tiles[tiles.length - 2],
+ )
: RailType.VERTICAL,
});
}
diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts
index 4fb2765fe..e5467f8f6 100644
--- a/src/core/execution/SAMLauncherExecution.ts
+++ b/src/core/execution/SAMLauncherExecution.ts
@@ -128,7 +128,7 @@ class SAMTargetingSystem {
export class SAMLauncherExecution implements Execution {
private mg: Game;
- private active: boolean = true;
+ private active = true;
// As MIRV go very fast we have to detect them very early but we only
// shoot the one targeting very close (MIRVWarheadProtectionRadius)
diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts
index e313acd8c..4359d9dc1 100644
--- a/src/core/execution/SAMMissileExecution.ts
+++ b/src/core/execution/SAMMissileExecution.ts
@@ -16,7 +16,7 @@ export class SAMMissileExecution implements Execution {
private pathFinder: AirPathFinder;
private SAMMissile: Unit | undefined;
private mg: Game;
- private speed: number = 0;
+ private speed = 0;
constructor(
private spawn: TileRef,
diff --git a/src/core/execution/ShellExecution.ts b/src/core/execution/ShellExecution.ts
index 4bf1103ec..9a05005eb 100644
--- a/src/core/execution/ShellExecution.ts
+++ b/src/core/execution/ShellExecution.ts
@@ -8,7 +8,7 @@ export class ShellExecution implements Execution {
private pathFinder: AirPathFinder;
private shell: Unit | undefined;
private mg: Game;
- private destroyAtTick: number = -1;
+ private destroyAtTick = -1;
private random: PseudoRandom;
constructor(
diff --git a/src/core/execution/SpawnExecution.ts b/src/core/execution/SpawnExecution.ts
index 57baff6ee..6721b2794 100644
--- a/src/core/execution/SpawnExecution.ts
+++ b/src/core/execution/SpawnExecution.ts
@@ -5,7 +5,7 @@ import { PlayerExecution } from "./PlayerExecution";
import { getSpawnTiles } from "./Util";
export class SpawnExecution implements Execution {
- active: boolean = true;
+ active = true;
private mg: Game;
constructor(
diff --git a/src/core/execution/TrainExecution.ts b/src/core/execution/TrainExecution.ts
index 34a8634e7..137fa9e88 100644
--- a/src/core/execution/TrainExecution.ts
+++ b/src/core/execution/TrainExecution.ts
@@ -16,13 +16,13 @@ export class TrainExecution implements Execution {
private mg: Game | null = null;
private train: Unit | null = null;
private cars: Unit[] = [];
- private hasCargo: boolean = false;
- private currentTile: number = 0;
+ private hasCargo = false;
+ private currentTile = 0;
private spacing = 2;
private usedTiles: TileRef[] = []; // used for cars behind
private stations: TrainStation[] = [];
private currentRailroad: OrientedRailroad | null = null;
- private speed: number = 2;
+ private speed = 2;
constructor(
private railNetwork: RailNetwork,
diff --git a/src/core/execution/TrainStationExecution.ts b/src/core/execution/TrainStationExecution.ts
index ee0174bcc..29792655c 100644
--- a/src/core/execution/TrainStationExecution.ts
+++ b/src/core/execution/TrainStationExecution.ts
@@ -5,12 +5,12 @@ import { TrainExecution } from "./TrainExecution";
export class TrainStationExecution implements Execution {
private mg: Game;
- private active: boolean = true;
+ private active = true;
private random: PseudoRandom;
private station: TrainStation | null = null;
- private numCars: number = 5;
- private lastSpawnTick: number = 0;
- private ticksCooldown: number = 10; // Minimum cooldown between two trains
+ private numCars = 5;
+ private lastSpawnTick = 0;
+ private ticksCooldown = 10; // Minimum cooldown between two trains
constructor(
private unit: Unit,
private spawnTrains?: boolean, // If set, the station will spawn trains
@@ -50,7 +50,7 @@ export class TrainStationExecution implements Execution {
private shouldSpawnTrain(clusterSize: number): boolean {
const spawnRate = this.mg.config().trainSpawnRate(clusterSize);
- for (let i = 0; i < this.unit!.level(); i++) {
+ for (let i = 0; i < this.unit.level(); i++) {
if (this.random.chance(spawnRate)) {
return true;
}
diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts
index 9e0a71309..778a56eed 100644
--- a/src/core/execution/WarshipExecution.ts
+++ b/src/core/execution/WarshipExecution.ts
@@ -79,7 +79,7 @@ export class WarshipExecution implements Execution {
const patrolRangeSquared = this.mg.config().warshipPatrolRange() ** 2;
const ships = this.mg.nearbyUnits(
- this.warship.tile()!,
+ this.warship.tile(),
this.mg.config().warshipTargettingRange(),
[UnitType.TransportShip, UnitType.Warship, UnitType.TradeShip],
);
@@ -238,11 +238,11 @@ export class WarshipExecution implements Execution {
return false;
}
- randomTile(allowShoreline: boolean = false): TileRef | undefined {
+ randomTile(allowShoreline = false): TileRef | undefined {
let warshipPatrolRange = this.mg.config().warshipPatrolRange();
- const maxAttemptBeforeExpand: number = 500;
- let attempts: number = 0;
- let expandCount: number = 0;
+ const maxAttemptBeforeExpand = 500;
+ let attempts = 0;
+ let expandCount = 0;
while (expandCount < 3) {
const x =
this.mg.x(this.warship.patrolTile()!) +
diff --git a/src/core/game/AllianceImpl.ts b/src/core/game/AllianceImpl.ts
index fa74ca766..c6b3bb3bc 100644
--- a/src/core/game/AllianceImpl.ts
+++ b/src/core/game/AllianceImpl.ts
@@ -1,8 +1,8 @@
import { Game, MutableAlliance, Player, Tick } from "./Game";
export class AllianceImpl implements MutableAlliance {
- private extensionRequestedRequestor_: boolean = false;
- private extensionRequestedRecipient_: boolean = false;
+ private extensionRequestedRequestor_ = false;
+ private extensionRequestedRecipient_ = false;
private expiresAt_: Tick;
diff --git a/src/core/game/BinaryLoaderGameMapLoader.ts b/src/core/game/BinaryLoaderGameMapLoader.ts
index 47dccca7b..bfb6109af 100644
--- a/src/core/game/BinaryLoaderGameMapLoader.ts
+++ b/src/core/game/BinaryLoaderGameMapLoader.ts
@@ -2,13 +2,13 @@ import { GameMapType } from "./Game";
import { GameMapLoader, MapData } from "./GameMapLoader";
import { MapManifest } from "./TerrainMapLoader";
-export interface BinModule {
+export type BinModule = {
default: string;
-}
+};
-interface NationMapModule {
+type NationMapModule = {
default: MapManifest;
-}
+};
export class BinaryLoaderGameMapLoader implements GameMapLoader {
private maps: Map;
diff --git a/src/core/game/FetchGameMapLoader.ts b/src/core/game/FetchGameMapLoader.ts
index 8e218b706..c9692983d 100644
--- a/src/core/game/FetchGameMapLoader.ts
+++ b/src/core/game/FetchGameMapLoader.ts
@@ -1,5 +1,6 @@
import { GameMapType } from "./Game";
import { GameMapLoader, MapData } from "./GameMapLoader";
+import { MapManifestSchema } from "./TerrainMapLoader";
export class FetchGameMapLoader implements GameMapLoader {
private maps: Map;
@@ -66,6 +67,6 @@ export class FetchGameMapLoader implements GameMapLoader {
throw new Error(`Failed to load ${url}: ${response.statusText}`);
}
- return response.json();
+ return response.json().then(MapManifestSchema.parse);
}
}
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index e8c6d12b9..105d4dd59 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -26,10 +26,10 @@ export type GameUpdates = {
[K in GameUpdateType]: UpdateTypeMap[];
};
-export interface MapPos {
+export type MapPos = {
x: number;
y: number;
-}
+};
export enum Difficulty {
Easy = "Easy",
@@ -141,7 +141,7 @@ export enum GameMode {
Team = "Team",
}
-export interface UnitInfo {
+export type UnitInfo = {
cost: (player: Player) => Gold;
// Determines if its owner changes when its tile is conquered.
territoryBound: boolean;
@@ -151,7 +151,7 @@ export interface UnitInfo {
upgradable?: boolean;
canBuildTrainStation?: boolean;
experimental?: boolean;
-}
+};
export enum UnitType {
TransportShip = "Transport",
@@ -191,15 +191,15 @@ export function isStructureType(type: UnitType): boolean {
return _structureTypes.has(type);
}
-export interface OwnerComp {
+export type OwnerComp = {
owner: Player;
-}
+};
export type TrajectoryTile = {
tile: TileRef;
targetable: boolean;
};
-export interface UnitParamsMap {
+export type UnitParamsMap = {
[UnitType.TransportShip]: {
troops?: number;
destination?: TileRef;
@@ -253,7 +253,7 @@ export interface UnitParamsMap {
};
[UnitType.Construction]: Record;
-}
+};
// Type helper to get params type for a specific unit type
export type UnitParams = UnitParamsMap[T];
@@ -320,14 +320,14 @@ export enum PlayerType {
FakeHuman = "FAKEHUMAN",
}
-export interface Execution {
+export type Execution = {
isActive(): boolean;
activeDuringSpawnPhase(): boolean;
init(mg: Game, ticks: number): void;
tick(ticks: number): void;
-}
+};
-export interface Attack {
+export type Attack = {
id(): string;
retreating(): boolean;
retreated(): boolean;
@@ -346,25 +346,25 @@ export interface Attack {
clearBorder(): void;
borderSize(): number;
averagePosition(): Cell | null;
-}
+};
-export interface AllianceRequest {
+export type AllianceRequest = {
accept(): void;
reject(): void;
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
-}
+};
-export interface Alliance {
+export type Alliance = {
requestor(): Player;
recipient(): Player;
createdAt(): Tick;
expiresAt(): Tick;
other(player: Player): Player;
-}
+};
-export interface MutableAlliance extends Alliance {
+export type MutableAlliance = {
expire(): void;
other(player: Player): Player;
bothAgreedToExtend(): boolean;
@@ -372,7 +372,7 @@ export interface MutableAlliance extends Alliance {
id(): number;
extend(): void;
onlyOneAgreedToExtend(): boolean;
-}
+} & Alliance;
export class PlayerInfo {
public readonly clan: string | null;
@@ -406,7 +406,7 @@ export function isUnit(unit: unknown): unit is Unit {
);
}
-export interface Unit {
+export type Unit = {
isUnit(): this is Unit;
// Common properties.
@@ -480,22 +480,22 @@ export interface Unit {
// Warships
setPatrolTile(tile: TileRef): void;
patrolTile(): TileRef | undefined;
-}
+};
-export interface TerraNullius {
+export type TerraNullius = {
isPlayer(): false;
id(): null;
clientID(): ClientID;
smallID(): number;
-}
+};
-export interface Embargo {
+export type Embargo = {
createdAt: Tick;
isTemporary: boolean;
target: PlayerID;
-}
+};
-export interface Player {
+export type Player = {
// Basic Info
smallID(): number;
info(): PlayerInfo;
@@ -593,7 +593,8 @@ export interface Player {
sendEmoji(recipient: Player | typeof AllPlayers, emoji: string): void;
// Donation
- canDonate(recipient: Player): boolean;
+ canDonateGold(recipient: Player): boolean;
+ canDonateTroops(recipient: Player): boolean;
donateTroops(recipient: Player, troops: number): boolean;
donateGold(recipient: Player, gold: Gold): boolean;
canDeleteUnit(): boolean;
@@ -628,9 +629,9 @@ export interface Player {
tradingPorts(port: Unit): Unit[];
// WARNING: this operation is expensive.
bestTransportShipSpawn(tile: TileRef): TileRef | false;
-}
+};
-export interface Game extends GameMap {
+export type Game = {
// Map & Dimensions
isOnMap(cell: Cell): boolean;
width(): number;
@@ -714,49 +715,50 @@ export interface Game extends GameMap {
addUpdate(update: GameUpdate): void;
railNetwork(): RailNetwork;
conquerPlayer(conqueror: Player, conquered: Player): void;
-}
+} & GameMap;
-export interface PlayerActions {
+export type PlayerActions = {
canAttack: boolean;
buildableUnits: BuildableUnit[];
canSendEmojiAllPlayers: boolean;
interaction?: PlayerInteraction;
-}
+};
-export interface BuildableUnit {
+export type BuildableUnit = {
canBuild: TileRef | false;
// unit id of the existing unit that can be upgraded, or false if it cannot be upgraded.
canUpgrade: number | false;
type: UnitType;
cost: Gold;
-}
+};
-export interface PlayerProfile {
+export type PlayerProfile = {
relations: Record;
alliances: number[];
-}
+};
-export interface PlayerBorderTiles {
+export type PlayerBorderTiles = {
borderTiles: ReadonlySet;
-}
+};
-export interface PlayerInteraction {
+export type PlayerInteraction = {
sharedBorder: boolean;
canSendEmoji: boolean;
canSendAllianceRequest: boolean;
canBreakAlliance: boolean;
canTarget: boolean;
- canDonate: boolean;
+ canDonateGold: boolean;
+ canDonateTroops: boolean;
canEmbargo: boolean;
allianceExpiresAt?: Tick;
-}
+};
-export interface EmojiMessage {
+export type EmojiMessage = {
message: string;
senderID: number;
recipientID: number | typeof AllPlayers;
createdAt: Tick;
-}
+};
export enum MessageType {
ATTACK_FAILED,
@@ -830,8 +832,8 @@ export function getMessageCategory(messageType: MessageType): MessageCategory {
return MESSAGE_TYPE_CATEGORIES[messageType];
}
-export interface NameViewData {
+export type NameViewData = {
x: number;
y: number;
size: number;
-}
+};
diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts
index 02ec95eb5..5edf2968c 100644
--- a/src/core/game/GameImpl.ts
+++ b/src/core/game/GameImpl.ts
@@ -82,7 +82,7 @@ export class GameImpl implements Game {
private _railNetwork: RailNetwork = createRailNetwork(this);
// Used to assign unique IDs to each new alliance
- private nextAllianceID: number = 0;
+ private nextAllianceID = 0;
constructor(
private _humans: PlayerInfo[],
diff --git a/src/core/game/GameMap.ts b/src/core/game/GameMap.ts
index 7a3bd8e6d..995e3b5d4 100644
--- a/src/core/game/GameMap.ts
+++ b/src/core/game/GameMap.ts
@@ -3,7 +3,7 @@ import { Cell, TerrainType } from "./Game";
export type TileRef = number;
export type TileUpdate = bigint;
-export interface GameMap {
+export type GameMap = {
ref(x: number, y: number): TileRef;
isValidRef(ref: TileRef): boolean;
x(ref: TileRef): number;
@@ -48,7 +48,7 @@ export interface GameMap {
updateTile(tu: TileUpdate): TileRef;
numTilesWithFallout(): number;
-}
+};
export class GameMapImpl implements GameMap {
private _numTilesWithFallout = 0;
@@ -92,9 +92,9 @@ export class GameMapImpl implements GameMap {
this.state = new Uint16Array(width * height);
// Precompute the LUTs
let ref = 0;
- this.refToX = new Array(width * height);
- this.refToY = new Array(width * height);
- this.yToRef = new Array(height);
+ this.refToX = new Array(width * height);
+ this.refToY = new Array(width * height);
+ this.yToRef = new Array(height);
for (let y = 0; y < height; y++) {
this.yToRef[y] = ref;
for (let x = 0; x < width; x++) {
@@ -341,7 +341,7 @@ export class GameMapImpl implements GameMap {
export function euclDistFN(
root: TileRef,
dist: number,
- center: boolean = false,
+ center = false,
): (gm: GameMap, tile: TileRef) => boolean {
const dist2 = dist * dist;
if (!center) {
@@ -364,7 +364,7 @@ export function euclDistFN(
export function manhattanDistFN(
root: TileRef,
dist: number,
- center: boolean = false,
+ center = false,
): (gm: GameMap, tile: TileRef) => boolean {
if (!center) {
return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist;
@@ -382,7 +382,7 @@ export function manhattanDistFN(
export function rectDistFN(
root: TileRef,
dist: number,
- center: boolean = false,
+ center = false,
): (gm: GameMap, tile: TileRef) => boolean {
if (!center) {
return (gm: GameMap, n: TileRef) => {
@@ -415,7 +415,7 @@ function isInIsometricTile(
export function isometricDistFN(
root: TileRef,
dist: number,
- center: boolean = false,
+ center = false,
): (gm: GameMap, tile: TileRef) => boolean {
if (!center) {
return (gm: GameMap, n: TileRef) => gm.manhattanDist(root, n) <= dist;
@@ -437,7 +437,7 @@ export function isometricDistFN(
export function hexDistFN(
root: TileRef,
dist: number,
- center: boolean = false,
+ center = false,
): (gm: GameMap, tile: TileRef) => boolean {
if (!center) {
return (gm: GameMap, n: TileRef) => {
diff --git a/src/core/game/GameMapLoader.ts b/src/core/game/GameMapLoader.ts
index abf52ccf9..dd9d378c5 100644
--- a/src/core/game/GameMapLoader.ts
+++ b/src/core/game/GameMapLoader.ts
@@ -1,13 +1,13 @@
import { GameMapType } from "./Game";
import { MapManifest } from "./TerrainMapLoader";
-export interface GameMapLoader {
+export type GameMapLoader = {
getMapData(map: GameMapType): MapData;
-}
+};
-export interface MapData {
+export type MapData = {
mapBin: () => Promise;
miniMapBin: () => Promise;
manifest: () => Promise;
webpPath: () => Promise;
-}
+};
diff --git a/src/core/game/GameUpdates.ts b/src/core/game/GameUpdates.ts
index 565960479..cf60babac 100644
--- a/src/core/game/GameUpdates.ts
+++ b/src/core/game/GameUpdates.ts
@@ -14,17 +14,17 @@ import {
} from "./Game";
import { TileRef, TileUpdate } from "./GameMap";
-export interface GameUpdateViewData {
+export type GameUpdateViewData = {
tick: number;
updates: GameUpdates;
packedTileUpdates: BigUint64Array;
playerNameViewData: Record;
-}
+};
-export interface ErrorUpdate {
+export type ErrorUpdate = {
errMsg: string;
stack?: string;
-}
+};
export enum GameUpdateType {
Tile,
@@ -67,13 +67,13 @@ export type GameUpdate =
| RailroadUpdate
| ConquestUpdate;
-export interface BonusEventUpdate {
+export type BonusEventUpdate = {
type: GameUpdateType.BonusEvent;
player: PlayerID;
tile: TileRef;
gold: number;
troops: number;
-}
+};
export enum RailType {
VERTICAL,
@@ -84,30 +84,30 @@ export enum RailType {
BOTTOM_RIGHT,
}
-export interface RailTile {
+export type RailTile = {
tile: TileRef;
railType: RailType;
-}
+};
-export interface RailroadUpdate {
+export type RailroadUpdate = {
type: GameUpdateType.RailroadEvent;
isActive: boolean;
railTiles: RailTile[];
-}
+};
-export interface ConquestUpdate {
+export type ConquestUpdate = {
type: GameUpdateType.ConquestEvent;
conquerorId: PlayerID;
conqueredId: PlayerID;
gold: Gold;
-}
+};
-export interface TileUpdateWrapper {
+export type TileUpdateWrapper = {
type: GameUpdateType.Tile;
update: TileUpdate;
-}
+};
-export interface UnitUpdate {
+export type UnitUpdate = {
type: GameUpdateType.Unit;
unitType: UnitType;
troops: number;
@@ -130,17 +130,17 @@ export interface UnitUpdate {
hasTrainStation: boolean;
trainType?: TrainType; // Only for trains
loaded?: boolean; // Only for trains
-}
+};
-export interface AttackUpdate {
+export type AttackUpdate = {
attackerID: number;
targetID: number;
troops: number;
id: string;
retreating: boolean;
-}
+};
-export interface PlayerUpdate {
+export type PlayerUpdate = {
type: GameUpdateType.Player;
nameViewData?: NameViewData;
clientID: ClientID | null;
@@ -166,65 +166,65 @@ export interface PlayerUpdate {
alliances: AllianceView[];
hasSpawned: boolean;
betrayals?: bigint;
-}
+};
-export interface AllianceView {
+export type AllianceView = {
id: number;
other: PlayerID;
createdAt: Tick;
expiresAt: Tick;
-}
+};
-export interface AllianceRequestUpdate {
+export type AllianceRequestUpdate = {
type: GameUpdateType.AllianceRequest;
requestorID: number;
recipientID: number;
createdAt: Tick;
-}
+};
-export interface AllianceRequestReplyUpdate {
+export type AllianceRequestReplyUpdate = {
type: GameUpdateType.AllianceRequestReply;
request: AllianceRequestUpdate;
accepted: boolean;
-}
+};
-export interface BrokeAllianceUpdate {
+export type BrokeAllianceUpdate = {
type: GameUpdateType.BrokeAlliance;
traitorID: number;
betrayedID: number;
-}
+};
-export interface AllianceExpiredUpdate {
+export type AllianceExpiredUpdate = {
type: GameUpdateType.AllianceExpired;
player1ID: number;
player2ID: number;
-}
+};
-export interface AllianceExtensionUpdate {
+export type AllianceExtensionUpdate = {
type: GameUpdateType.AllianceExtension;
playerID: number;
allianceID: number;
-}
+};
-export interface TargetPlayerUpdate {
+export type TargetPlayerUpdate = {
type: GameUpdateType.TargetPlayer;
playerID: number;
targetID: number;
-}
+};
-export interface EmojiUpdate {
+export type EmojiUpdate = {
type: GameUpdateType.Emoji;
emoji: EmojiMessage;
-}
+};
-export interface DisplayMessageUpdate {
+export type DisplayMessageUpdate = {
type: GameUpdateType.DisplayEvent;
message: string;
messageType: MessageType;
goldAmount?: bigint;
playerID: number | null;
params?: Record;
-}
+};
export type DisplayChatMessageUpdate = {
type: GameUpdateType.DisplayChatEvent;
@@ -236,22 +236,22 @@ export type DisplayChatMessageUpdate = {
recipient: string;
};
-export interface WinUpdate {
+export type WinUpdate = {
type: GameUpdateType.Win;
allPlayersStats: AllPlayersStats;
winner: Winner;
-}
+};
-export interface HashUpdate {
+export type HashUpdate = {
type: GameUpdateType.Hash;
tick: Tick;
hash: number;
-}
+};
-export interface UnitIncomingUpdate {
+export type UnitIncomingUpdate = {
type: GameUpdateType.UnitIncoming;
unitID: number;
message: string;
messageType: MessageType;
playerID: number;
-}
+};
diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts
index 34dc7e005..2739cda60 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -39,10 +39,10 @@ import { UserSettings } from "./UserSettings";
const userSettings: UserSettings = new UserSettings();
-interface PlayerCosmetics {
+type PlayerCosmetics = {
pattern?: string | undefined;
flag?: string | undefined;
-}
+};
export class UnitView {
public _wasUpdated = true;
@@ -106,7 +106,7 @@ export class UnitView {
return this.data.pos;
}
owner(): PlayerView {
- return this.gameView.playerBySmallID(this.data.ownerID)! as PlayerView;
+ return this.gameView.playerBySmallID(this.data.ownerID) as PlayerView;
}
isActive(): boolean {
return this.data.isActive;
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index 3f83fee22..bf70fb321 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -53,10 +53,10 @@ import {
} from "./TransportShipUtils";
import { UnitImpl } from "./UnitImpl";
-interface Target {
+type Target = {
tick: Tick;
target: Player;
-}
+};
class Donation {
constructor(
@@ -66,7 +66,7 @@ class Donation {
}
export class PlayerImpl implements Player {
- public _lastTileChange: number = 0;
+ public _lastTileChange = 0;
public _pseudo_random: PseudoRandom;
private _gold: bigint;
@@ -278,7 +278,7 @@ export class PlayerImpl implements Player {
}
tiles(): ReadonlySet {
- return new Set(this._tiles.values()) as Set;
+ return new Set(this._tiles.values());
}
borderTiles(): ReadonlySet {
@@ -572,7 +572,7 @@ export class PlayerImpl implements Player {
return true;
}
- canDonate(recipient: Player): boolean {
+ canDonateGold(recipient: Player): boolean {
if (!this.isFriendly(recipient)) {
return false;
}
@@ -583,6 +583,36 @@ export class PlayerImpl implements Player {
) {
return false;
}
+ if (this.mg.config().donateGold() === false) {
+ return false;
+ }
+ for (const donation of this.sentDonations) {
+ if (donation.recipient === recipient) {
+ if (
+ this.mg.ticks() - donation.tick <
+ this.mg.config().donateCooldown()
+ ) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ canDonateTroops(recipient: Player): boolean {
+ if (!this.isFriendly(recipient)) {
+ return false;
+ }
+ if (
+ recipient.type() === PlayerType.Human &&
+ this.mg.config().gameConfig().gameMode === GameMode.FFA &&
+ this.mg.config().gameConfig().gameType === GameType.Public
+ ) {
+ return false;
+ }
+ if (this.mg.config().donateTroops() === false) {
+ return false;
+ }
for (const donation of this.sentDonations) {
if (donation.recipient === recipient) {
if (
@@ -1148,7 +1178,7 @@ export class PlayerImpl implements Player {
const weightedPorts: Unit[] = [];
for (const [i, otherPort] of ports.entries()) {
- const expanded = new Array(otherPort.level()).fill(otherPort);
+ const expanded = new Array(otherPort.level()).fill(otherPort);
weightedPorts.push(...expanded);
if (i < this.mg.config().proximityBonusPortsNb(ports.length)) {
weightedPorts.push(...expanded);
diff --git a/src/core/game/RailNetwork.ts b/src/core/game/RailNetwork.ts
index 404c062be..00c2e6f5e 100644
--- a/src/core/game/RailNetwork.ts
+++ b/src/core/game/RailNetwork.ts
@@ -1,8 +1,8 @@
import { Unit } from "./Game";
import { TrainStation } from "./TrainStation";
-export interface RailNetwork {
+export type RailNetwork = {
connectStation(station: TrainStation): void;
removeStation(unit: Unit): void;
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
-}
+};
diff --git a/src/core/game/RailNetworkImpl.ts b/src/core/game/RailNetworkImpl.ts
index 4d054f911..6232ba06d 100644
--- a/src/core/game/RailNetworkImpl.ts
+++ b/src/core/game/RailNetworkImpl.ts
@@ -13,12 +13,12 @@ import { Cluster, TrainStation, TrainStationMapAdapter } from "./TrainStation";
* but it would be expensive to look through the graph to find a station.
* This class stores the existing stations for quick access
*/
-export interface StationManager {
+export type StationManager = {
addStation(station: TrainStation): void;
removeStation(station: TrainStation): void;
findStation(unit: Unit): TrainStation | null;
getAll(): Set;
-}
+};
export class StationManagerImpl implements StationManager {
private stations: Set = new Set();
@@ -43,10 +43,10 @@ export class StationManagerImpl implements StationManager {
}
}
-export interface RailPathFinderService {
+export type RailPathFinderService = {
findTilePath(from: TileRef, to: TileRef): TileRef[];
findStationsPath(from: TrainStation, to: TrainStation): TrainStation[];
-}
+};
class RailPathFinderServiceImpl implements RailPathFinderService {
constructor(private game: Game) {}
@@ -88,7 +88,7 @@ export function createRailNetwork(game: Game): RailNetwork {
}
export class RailNetworkImpl implements RailNetwork {
- private maxConnectionDistance: number = 4;
+ private maxConnectionDistance = 4;
constructor(
private game: Game,
diff --git a/src/core/game/Stats.ts b/src/core/game/Stats.ts
index d328c6d71..63a8aac79 100644
--- a/src/core/game/Stats.ts
+++ b/src/core/game/Stats.ts
@@ -2,7 +2,7 @@ import { AllPlayersStats } from "../Schemas";
import { NukeType, OtherUnitType, PlayerStats } from "../StatsSchemas";
import { Player, TerraNullius } from "./Game";
-export interface Stats {
+export type Stats = {
getPlayerStats(player: Player): PlayerStats | null;
stats(): AllPlayersStats;
@@ -93,4 +93,4 @@ export interface Stats {
// Player loses a unit of type
unitLose(player: Player, type: OtherUnitType): void;
-}
+};
diff --git a/src/core/game/TerrainMapLoader.ts b/src/core/game/TerrainMapLoader.ts
index f59799b38..588b8b98b 100644
--- a/src/core/game/TerrainMapLoader.ts
+++ b/src/core/game/TerrainMapLoader.ts
@@ -1,3 +1,4 @@
+import { z } from "zod";
import { GameMapType } from "./Game";
import { GameMap, GameMapImpl } from "./GameMap";
import { GameMapLoader } from "./GameMapLoader";
@@ -10,25 +11,28 @@ export type TerrainMapData = {
const loadedMaps = new Map();
-export interface MapMetadata {
- width: number;
- height: number;
- num_land_tiles: number;
-}
+export const MapMetadataSchema = z.object({
+ height: z.number(),
+ num_land_tiles: z.number(),
+ width: z.number(),
+});
+export type MapMetadata = z.infer;
-export interface MapManifest {
- name: string;
- map: MapMetadata;
- mini_map: MapMetadata;
- nations: Nation[];
-}
+export const NationSchema = z.object({
+ coordinates: z.tuple([z.number(), z.number()]),
+ flag: z.string(),
+ name: z.string(),
+ strength: z.number(),
+});
+export type Nation = z.infer;
-export interface Nation {
- coordinates: [number, number];
- flag: string;
- name: string;
- strength: number;
-}
+export const MapManifestSchema = z.object({
+ map: MapMetadataSchema,
+ mini_map: MapMetadataSchema,
+ name: z.string(),
+ nations: NationSchema.array(),
+});
+export type MapManifest = z.infer;
export async function loadTerrainMap(
map: GameMapType,
diff --git a/src/core/game/TrainStation.ts b/src/core/game/TrainStation.ts
index 458ba2785..55e74ff49 100644
--- a/src/core/game/TrainStation.ts
+++ b/src/core/game/TrainStation.ts
@@ -9,16 +9,16 @@ import { Railroad } from "./Railroad";
/**
* Handle train stops at various station types
*/
-interface TrainStopHandler {
+type TrainStopHandler = {
onStop(mg: Game, station: TrainStation, trainExecution: TrainExecution): void;
-}
+};
/**
* All stop handlers share the same logic for the time being
* Behavior to be defined
*/
class CityStopHandler implements TrainStopHandler {
- private factor: bigint = BigInt(2);
+ private factor = BigInt(2);
onStop(
mg: Game,
station: TrainStation,
@@ -38,7 +38,7 @@ class CityStopHandler implements TrainStopHandler {
}
class PortStopHandler implements TrainStopHandler {
- private factor: bigint = BigInt(2);
+ private factor = BigInt(2);
constructor(private random: PseudoRandom) {}
onStop(
mg: Game,
@@ -59,7 +59,7 @@ class PortStopHandler implements TrainStopHandler {
}
class FactoryStopHandler implements TrainStopHandler {
- private factor: bigint = BigInt(2);
+ private factor = BigInt(2);
onStop(
mg: Game,
station: TrainStation,
diff --git a/src/core/game/TransportShipUtils.ts b/src/core/game/TransportShipUtils.ts
index 0e22eafdd..df335b714 100644
--- a/src/core/game/TransportShipUtils.ts
+++ b/src/core/game/TransportShipUtils.ts
@@ -102,7 +102,7 @@ export function sourceDstOceanShore(
const srcTile = closestShoreFromPlayer(gm, src, tile);
let dstTile: TileRef | null = null;
if (dst.isPlayer()) {
- dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
+ dstTile = closestShoreFromPlayer(gm, dst, tile);
} else {
dstTile = closestShoreTN(gm, tile, 50);
}
@@ -113,7 +113,7 @@ export function targetTransportTile(gm: Game, tile: TileRef): TileRef | null {
const dst = gm.playerBySmallID(gm.ownerID(tile));
let dstTile: TileRef | null = null;
if (dst.isPlayer()) {
- dstTile = closestShoreFromPlayer(gm, dst as Player, tile);
+ dstTile = closestShoreFromPlayer(gm, dst, tile);
} else {
dstTile = closestShoreTN(gm, tile, 50);
}
@@ -235,7 +235,7 @@ export function candidateShoreTiles(
extremumTiles.maxX,
extremumTiles.maxY,
...sampledTiles,
- ].filter(Boolean) as number[];
+ ].filter(Boolean);
return candidates;
}
diff --git a/src/core/game/UnitGrid.ts b/src/core/game/UnitGrid.ts
index a60fadf53..f17ce3e1e 100644
--- a/src/core/game/UnitGrid.ts
+++ b/src/core/game/UnitGrid.ts
@@ -144,7 +144,12 @@ export class UnitGrid {
searchRange,
);
const rangeSquared = searchRange * searchRange;
- const typeSet = Array.isArray(types) ? new Set(types) : new Set([types]);
+ const typeSet = new Set(
+ // Using typeof check instead of Array.isArray due to a typescript
+ // narrowing limitation. For more information, see the full issue
+ // discussion at https://github.com/mattpocock/ts-reset/issues/48
+ typeof types === "object" ? types : [types],
+ );
for (let cy = startGridY; cy <= endGridY; cy++) {
for (let cx = startGridX; cx <= endGridX; cx++) {
for (const type of typeSet) {
diff --git a/src/core/game/UnitImpl.ts b/src/core/game/UnitImpl.ts
index c9c61b562..c30403108 100644
--- a/src/core/game/UnitImpl.ts
+++ b/src/core/game/UnitImpl.ts
@@ -21,7 +21,7 @@ export class UnitImpl implements Unit {
private _targetUnit: Unit | undefined;
private _health: bigint;
private _lastTile: TileRef;
- private _retreating: boolean = false;
+ private _retreating = false;
private _targetedBySAM = false;
private _reachedTarget = false;
private _lastSetSafeFromPirates: number; // Only for trade ships
@@ -30,14 +30,14 @@ export class UnitImpl implements Unit {
private _troops: number;
// Number of missiles in cooldown, if empty all missiles are ready.
private _missileTimerQueue: number[] = [];
- private _hasTrainStation: boolean = false;
+ private _hasTrainStation = false;
private _patrolTile: TileRef | undefined;
- private _level: number = 1;
- private _targetable: boolean = true;
+ private _level = 1;
+ private _targetable = true;
private _loaded: boolean | undefined;
private _trainType: TrainType | undefined;
// Nuke only
- private _trajectoryIndex: number = 0;
+ private _trajectoryIndex = 0;
private _trajectory: TrajectoryTile[];
constructor(
diff --git a/src/core/pathfinding/AStar.ts b/src/core/pathfinding/AStar.ts
index f16accf4e..63fbf907f 100644
--- a/src/core/pathfinding/AStar.ts
+++ b/src/core/pathfinding/AStar.ts
@@ -1,7 +1,7 @@
-export interface AStar {
+export type AStar = {
compute(): PathFindResultType;
reconstructPath(): NodeType[];
-}
+};
export enum PathFindResultType {
NextTile,
@@ -11,21 +11,21 @@ export enum PathFindResultType {
}
export type AStarResult =
| {
- type: PathFindResultType.NextTile;
- node: NodeType;
- }
+ type: PathFindResultType.NextTile;
+ node: NodeType;
+ }
| {
- type: PathFindResultType.Pending;
- }
+ type: PathFindResultType.Pending;
+ }
| {
- type: PathFindResultType.Completed;
- node: NodeType;
- }
+ type: PathFindResultType.Completed;
+ node: NodeType;
+ }
| {
- type: PathFindResultType.PathNotFound;
- };
+ type: PathFindResultType.PathNotFound;
+ };
-export interface Point {
+export type Point = {
x: number;
y: number;
-}
+};
diff --git a/src/core/pathfinding/MiniAStar.ts b/src/core/pathfinding/MiniAStar.ts
index a355afd41..2c43950cb 100644
--- a/src/core/pathfinding/MiniAStar.ts
+++ b/src/core/pathfinding/MiniAStar.ts
@@ -36,8 +36,8 @@ export class MiniAStar implements AStar {
private dst: TileRef,
iterations: number,
maxTries: number,
- waterPath: boolean = true,
- directionChangePenalty: number = 0,
+ waterPath = true,
+ directionChangePenalty = 0,
) {
const srcArray: TileRef[] = Array.isArray(src) ? src : [src];
const miniSrc = srcArray.map((srcPoint) =>
@@ -113,7 +113,7 @@ function fixExtremes(upscaled: Cell[], cellDst: Cell, cellSrc?: Cell): Cell[] {
return upscaled;
}
-function upscalePath(path: Cell[], scaleFactor: number = 2): Cell[] {
+function upscalePath(path: Cell[], scaleFactor = 2): Cell[] {
// Scale up each point
const scaledPath = path.map(
(point) => new Cell(point.x * scaleFactor, point.y * scaleFactor),
diff --git a/src/core/pathfinding/PathFinding.ts b/src/core/pathfinding/PathFinding.ts
index 9a34e3316..d5723abf1 100644
--- a/src/core/pathfinding/PathFinding.ts
+++ b/src/core/pathfinding/PathFinding.ts
@@ -14,7 +14,7 @@ export class ParabolaPathFinder {
computeControlPoints(
orig: TileRef,
dst: TileRef,
- increment: number = 3,
+ increment = 3,
distanceBasedHeight = true,
) {
const p0 = { x: this.mg.x(orig), y: this.mg.y(orig) };
@@ -117,8 +117,8 @@ export class PathFinder {
public static Mini(
game: Game,
iterations: number,
- waterPath: boolean = true,
- maxTries: number = 20,
+ waterPath = true,
+ maxTries = 20,
) {
return new PathFinder(game, (curr: TileRef, dst: TileRef) => {
return new MiniAStar(
@@ -136,7 +136,7 @@ export class PathFinder {
nextTile(
curr: TileRef | null,
dst: TileRef | null,
- dist: number = 1,
+ dist = 1,
): AStarResult {
if (curr === null) {
console.error("curr is null");
diff --git a/src/core/pathfinding/SerialAStar.ts b/src/core/pathfinding/SerialAStar.ts
index fdb138c82..eef9306c4 100644
--- a/src/core/pathfinding/SerialAStar.ts
+++ b/src/core/pathfinding/SerialAStar.ts
@@ -4,12 +4,12 @@ import { AStar, PathFindResultType } from "./AStar";
/**
* Implement this interface with your graph to find paths with A*
*/
-export interface GraphAdapter {
+export type GraphAdapter = {
neighbors(node: NodeType): NodeType[];
cost(node: NodeType): number;
position(node: NodeType): { x: number; y: number };
isTraversable(from: NodeType, to: NodeType): boolean;
-}
+};
export class SerialAStar implements AStar {
private fwdOpenSet: FastPriorityQueue<{
@@ -37,7 +37,7 @@ export class SerialAStar implements AStar {
private iterations: number,
private maxTries: number,
private graph: GraphAdapter,
- private directionChangePenalty: number = 0,
+ private directionChangePenalty = 0,
) {
this.fwdOpenSet = new FastPriorityQueue((a, b) => a.fScore < b.fScore);
this.bwdOpenSet = new FastPriorityQueue((a, b) => a.fScore < b.fScore);
diff --git a/src/core/utilities/Line.ts b/src/core/utilities/Line.ts
index 67024e9c6..57aea1e6c 100644
--- a/src/core/utilities/Line.ts
+++ b/src/core/utilities/Line.ts
@@ -77,9 +77,9 @@ export class CubicBezierCurve {
* Useful to compute regular steps based on the curve rather than a t
*/
export class DistanceBasedBezierCurve extends CubicBezierCurve {
- private totalDistance: number = 0;
+ private totalDistance = 0;
private cachedPoints: Point[] = [];
- private currentIndex: number = 0;
+ private currentIndex = 0;
constructor(
p0: Point,
diff --git a/src/core/worker/Worker.worker.ts b/src/core/worker/Worker.worker.ts
index 866b70834..bafda59bb 100644
--- a/src/core/worker/Worker.worker.ts
+++ b/src/core/worker/Worker.worker.ts
@@ -13,7 +13,7 @@ import {
WorkerMessage,
} from "./WorkerMessages";
-const ctx: Worker = self as any;
+const ctx: Worker = self as unknown as Worker;
let gameRunner: Promise | null = null;
const mapLoader = new FetchGameMapLoader(`/maps`, version);
diff --git a/src/core/worker/WorkerMessages.ts b/src/core/worker/WorkerMessages.ts
index 65234e2f6..059fbe3f7 100644
--- a/src/core/worker/WorkerMessages.ts
+++ b/src/core/worker/WorkerMessages.ts
@@ -26,91 +26,91 @@ export type WorkerMessageType =
| "transport_ship_spawn_result";
// Base interface for all messages
-interface BaseWorkerMessage {
+type BaseWorkerMessage = {
type: WorkerMessageType;
id?: string;
-}
+};
-export interface HeartbeatMessage extends BaseWorkerMessage {
+export type HeartbeatMessage = {
type: "heartbeat";
-}
+} & BaseWorkerMessage;
// Messages from main thread to worker
-export interface InitMessage extends BaseWorkerMessage {
+export type InitMessage = {
type: "init";
gameStartInfo: GameStartInfo;
clientID: ClientID;
-}
+} & BaseWorkerMessage;
-export interface TurnMessage extends BaseWorkerMessage {
+export type TurnMessage = {
type: "turn";
turn: Turn;
-}
+} & BaseWorkerMessage;
// Messages from worker to main thread
-export interface InitializedMessage extends BaseWorkerMessage {
+export type InitializedMessage = {
type: "initialized";
-}
+} & BaseWorkerMessage;
-export interface GameUpdateMessage extends BaseWorkerMessage {
+export type GameUpdateMessage = {
type: "game_update";
gameUpdate: GameUpdateViewData;
-}
+} & BaseWorkerMessage;
-export interface PlayerActionsMessage extends BaseWorkerMessage {
+export type PlayerActionsMessage = {
type: "player_actions";
playerID: PlayerID;
x: number;
y: number;
-}
+} & BaseWorkerMessage;
-export interface PlayerActionsResultMessage extends BaseWorkerMessage {
+export type PlayerActionsResultMessage = {
type: "player_actions_result";
result: PlayerActions;
-}
+} & BaseWorkerMessage;
-export interface PlayerProfileMessage extends BaseWorkerMessage {
+export type PlayerProfileMessage = {
type: "player_profile";
playerID: number;
-}
+} & BaseWorkerMessage;
-export interface PlayerProfileResultMessage extends BaseWorkerMessage {
+export type PlayerProfileResultMessage = {
type: "player_profile_result";
result: PlayerProfile;
-}
+} & BaseWorkerMessage;
-export interface PlayerBorderTilesMessage extends BaseWorkerMessage {
+export type PlayerBorderTilesMessage = {
type: "player_border_tiles";
playerID: PlayerID;
-}
+} & BaseWorkerMessage;
-export interface PlayerBorderTilesResultMessage extends BaseWorkerMessage {
+export type PlayerBorderTilesResultMessage = {
type: "player_border_tiles_result";
result: PlayerBorderTiles;
-}
+} & BaseWorkerMessage;
-export interface AttackAveragePositionMessage extends BaseWorkerMessage {
+export type AttackAveragePositionMessage = {
type: "attack_average_position";
playerID: number;
attackID: string;
-}
+} & BaseWorkerMessage;
-export interface AttackAveragePositionResultMessage extends BaseWorkerMessage {
+export type AttackAveragePositionResultMessage = {
type: "attack_average_position_result";
x: number | null;
y: number | null;
-}
+} & BaseWorkerMessage;
-export interface TransportShipSpawnMessage extends BaseWorkerMessage {
+export type TransportShipSpawnMessage = {
type: "transport_ship_spawn";
playerID: PlayerID;
targetTile: TileRef;
-}
+} & BaseWorkerMessage;
-export interface TransportShipSpawnResultMessage extends BaseWorkerMessage {
+export type TransportShipSpawnResultMessage = {
type: "transport_ship_spawn_result";
result: TileRef | false;
-}
+} & BaseWorkerMessage;
// Union types for type safety
export type MainThreadMessage =
diff --git a/src/reset.d.ts b/src/reset.d.ts
new file mode 100644
index 000000000..a3d4a031b
--- /dev/null
+++ b/src/reset.d.ts
@@ -0,0 +1 @@
+import "@total-typescript/ts-reset";
diff --git a/src/server/Client.ts b/src/server/Client.ts
index 68f0a2bfb..ecac7f885 100644
--- a/src/server/Client.ts
+++ b/src/server/Client.ts
@@ -1,13 +1,15 @@
import WebSocket from "ws";
import { TokenPayload } from "../core/ApiSchemas";
import { Tick } from "../core/game/Game";
-import { ClientID } from "../core/Schemas";
+import { ClientID, Winner } from "../core/Schemas";
export class Client {
public lastPing: number = Date.now();
public hashes: Map = new Map();
+ public reportedWinner: Winner | null = null;
+
constructor(
public readonly clientID: ClientID,
public readonly persistentID: string,
diff --git a/src/server/Cloudflare.ts b/src/server/Cloudflare.ts
index eef337974..015315b9e 100644
--- a/src/server/Cloudflare.ts
+++ b/src/server/Cloudflare.ts
@@ -1,45 +1,52 @@
import { spawn } from "child_process";
import { promises as fs } from "fs";
import yaml from "js-yaml";
+import { z } from "zod";
import { logger } from "./Logger";
const log = logger.child({
module: "cloudflare",
});
-export interface TunnelConfig {
+export type TunnelConfig = {
domain: string;
subdomain: string;
subdomainToService: Map;
-}
+};
-interface TunnelResponse {
+type TunnelResponse = {
result: {
id: string;
token: string;
};
-}
+};
-interface ZoneResponse {
+type ZoneResponse = {
result: Array<{
id: string;
}>;
-}
+};
-interface DNSRecordResponse {
+type DNSRecordResponse = {
result: Array<{
id: string;
}>;
-}
+};
-interface CloudflaredConfig {
+type CloudflaredConfig = {
tunnel: string;
"credentials-file": string;
ingress: Array<{
hostname?: string;
service: string;
}>;
-}
+};
+
+const CloudflareTunnelConfigSchema = z.object({
+ a: z.string(),
+ s: z.string(),
+ t: z.string(),
+});
export class Cloudflare {
private baseUrl = "https://api.cloudflare.com/client/v4";
@@ -56,13 +63,14 @@ export class Cloudflare {
private async makeRequest(
url: string,
- method: string = "GET",
+ method = "GET",
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any,
): Promise {
const response = await fetch(url, {
body: data ? JSON.stringify(data) : undefined,
headers: {
- Authorization: `Bearer ${this.apiToken}`,
+ "Authorization": `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
method,
@@ -157,14 +165,12 @@ export class Cloudflare {
tunnelName: string,
): Promise {
log.info(`Creating local config for tunnel ${subdomain}.${domain}...`);
- const tokenData = JSON.parse(
- Buffer.from(tunnelToken, "base64").toString("utf8"),
+ const tokenData = CloudflareTunnelConfigSchema.parse(
+ JSON.parse(Buffer.from(tunnelToken, "base64").toString("utf8")),
);
const credentials = {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
AccountTag: tokenData.a || this.accountId,
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
TunnelID: tokenData.t || tunnelId,
TunnelName: tunnelName,
TunnelSecret: tokenData.s,
@@ -179,7 +185,7 @@ export class Cloudflare {
const tunnelConfig: CloudflaredConfig = {
"credentials-file": this.credsPath,
- ingress: [
+ "ingress": [
...Array.from(subdomainToService.entries()).map(
([subdomain, service]) => ({
hostname: `${subdomain}.${domain}`,
@@ -190,7 +196,7 @@ export class Cloudflare {
service: "http_status:404",
},
],
- tunnel: tunnelId,
+ "tunnel": tunnelId,
};
// Write config file
@@ -250,9 +256,11 @@ export class Cloudflare {
);
cloudflared.stdout?.on("data", (data) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
log.info(data.toString().trim());
});
cloudflared.stderr?.on("data", (data) => {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
log.error(data.toString().trim());
});
diff --git a/src/server/GameManager.ts b/src/server/GameManager.ts
index 8870f00db..2a3d2b87a 100644
--- a/src/server/GameManager.ts
+++ b/src/server/GameManager.ts
@@ -43,6 +43,8 @@ export class GameManager {
difficulty: Difficulty.Medium,
disableNPCs: false,
disabledUnits: [],
+ donateGold: false,
+ donateTroops: false,
gameMap: GameMapType.World,
gameMode: GameMode.FFA,
gameType: GameType.Private,
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index 50fefd683..1298eef1b 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -65,6 +65,11 @@ export class GameServer {
private websockets: Set = new Set();
+ winnerVotes: Map<
+ string,
+ { winner: ClientSendWinnerMessage; ips: Set }
+ > = new Map();
+
constructor(
public readonly id: string,
readonly log_: Logger,
@@ -93,9 +98,15 @@ export class GameServer {
if (gameConfig.infiniteGold !== undefined) {
this.gameConfig.infiniteGold = gameConfig.infiniteGold;
}
+ if (gameConfig.donateGold !== undefined) {
+ this.gameConfig.donateGold = gameConfig.donateGold;
+ }
if (gameConfig.infiniteTroops !== undefined) {
this.gameConfig.infiniteTroops = gameConfig.infiniteTroops;
}
+ if (gameConfig.donateTroops !== undefined) {
+ this.gameConfig.donateTroops = gameConfig.donateTroops;
+ }
if (gameConfig.instantBuild !== undefined) {
this.gameConfig.instantBuild = gameConfig.instantBuild;
}
@@ -185,6 +196,7 @@ export class GameServer {
}
client.lastPing = existing.lastPing;
+ client.reportedWinner = existing.reportedWinner;
this.activeClients = this.activeClients.filter((c) => c !== existing);
}
@@ -214,6 +226,7 @@ export class GameServer {
);
});
client.ws.on("error", (error: Error) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") {
client.ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1");
}
diff --git a/src/server/Gatekeeper.ts b/src/server/Gatekeeper.ts
index ee47cbf50..e06f02877 100644
--- a/src/server/Gatekeeper.ts
+++ b/src/server/Gatekeeper.ts
@@ -12,7 +12,7 @@ export enum LimiterType {
WebSocket = "websocket",
}
-export interface Gatekeeper {
+export type Gatekeeper = {
// The wrapper for request handlers with optional rate limiting
httpHandler: (
limiterType: LimiterType,
@@ -24,7 +24,7 @@ export interface Gatekeeper {
req: http.IncomingMessage | string,
fn: (message: string) => Promise,
) => (message: string) => Promise;
-}
+};
let gk: Gatekeeper | null = null;
@@ -66,10 +66,12 @@ async function getGatekeeper(): Promise {
// Use dynamic import for ES modules
// Using a type assertion to avoid TypeScript errors for optional modules
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const module = await import(
"./gatekeeper/RealGatekeeper.js" as string
).catch(() => import("./gatekeeper/RealGatekeeper.js" as string));
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!module || !module.RealGatekeeper) {
console.log(
"RealGatekeeper class not found in module, using NoOpGatekeeper",
@@ -78,6 +80,7 @@ async function getGatekeeper(): Promise {
}
console.log("Successfully loaded real gatekeeper");
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
return new module.RealGatekeeper();
} catch (error) {
console.log("Failed to load real gatekeeper:", error);
diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts
index ca3d0e310..6ff4a2d89 100644
--- a/src/server/MapPlaylist.ts
+++ b/src/server/MapPlaylist.ts
@@ -20,39 +20,39 @@ const config = getServerConfigFromServer();
// How many times each map should appear in the playlist.
// Note: The Partial should eventually be removed for better type safety.
const frequency: Partial> = {
- Africa: 2,
- Asia: 1,
- Australia: 1,
- Baikal: 2,
- BetweenTwoSeas: 1,
- BlackSea: 1,
- Britannia: 1,
- DeglaciatedAntarctica: 1,
- EastAsia: 1,
- Europe: 2,
- EuropeClassic: 1,
- FalklandIslands: 1,
- FaroeIslands: 1,
- GatewayToTheAtlantic: 1,
- Halkidiki: 1,
- Iceland: 1,
- Italia: 1,
- Mars: 1,
- MarsRevised: 1,
- Mena: 1,
- NorthAmerica: 1,
- Pangaea: 1,
- Pluto: 1,
- SouthAmerica: 1,
- StraitOfGibraltar: 1,
- World: 3,
- Yenisei: 1,
+ Africa: 7,
+ Asia: 6,
+ Australia: 4,
+ Baikal: 5,
+ BetweenTwoSeas: 5,
+ BlackSea: 6,
+ Britannia: 5,
+ DeglaciatedAntarctica: 4,
+ EastAsia: 5,
+ Europe: 3,
+ EuropeClassic: 3,
+ FalklandIslands: 4,
+ FaroeIslands: 4,
+ GatewayToTheAtlantic: 5,
+ Halkidiki: 4,
+ Iceland: 4,
+ Italia: 6,
+ Mars: 3,
+ MarsRevised: 3,
+ Mena: 6,
+ NorthAmerica: 5,
+ Pangaea: 5,
+ Pluto: 6,
+ SouthAmerica: 5,
+ StraitOfGibraltar: 5,
+ World: 8,
+ Yenisei: 6,
};
-interface MapWithMode {
+type MapWithMode = {
map: GameMapType;
mode: GameMode;
-}
+};
const TEAM_COUNTS = [
2,
@@ -81,6 +81,8 @@ export class MapPlaylist {
difficulty: Difficulty.Medium,
disableNPCs: mode === GameMode.Team,
disabledUnits: [],
+ donateGold: true,
+ donateTroops: true,
gameMap: map,
gameMode: mode,
gameType: GameType.Public,
@@ -126,6 +128,7 @@ export class MapPlaylist {
const team: GameMapType[] = rand.shuffleArray([...maps]);
this.mapsPlaylist = [];
+ // eslint-disable-next-line @typescript-eslint/prefer-for-of
for (let i = 0; i < maps.length; i++) {
if (!this.addNextMap(this.mapsPlaylist, ffa1, GameMode.FFA)) {
return false;
diff --git a/src/server/Master.ts b/src/server/Master.ts
index ff693c323..ee6f9aa05 100644
--- a/src/server/Master.ts
+++ b/src/server/Master.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import cluster from "cluster";
import express from "express";
import rateLimit from "express-rate-limit";
@@ -5,6 +6,10 @@ import http from "http";
import path from "path";
import { fileURLToPath } from "url";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
+import {
+ ApiEnvResponse,
+ ApiPublicLobbiesResponse,
+} from "../core/ExpressSchemas";
import { GameInfo, ID } from "../core/Schemas";
import { generateID } from "../core/Util";
import { gatekeeper, LimiterType } from "./Gatekeeper";
@@ -59,7 +64,9 @@ app.use(
}),
);
-let publicLobbiesJsonStr = "";
+let publicLobbiesJsonStr = JSON.stringify({
+ lobbies: [],
+} satisfies ApiPublicLobbiesResponse);
const publicLobbyIDs: Set = new Set();
@@ -85,6 +92,7 @@ export async function startMaster() {
cluster.on("message", (worker, message) => {
if (message.type === "WORKER_READY") {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const workerId = message.workerId;
readyWorkers.add(workerId);
log.info(
@@ -115,6 +123,7 @@ export async function startMaster() {
// Handle worker crashes
cluster.on("exit", (worker, code, signal) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const workerId = (worker as any).process?.env?.WORKER_ID;
if (!workerId) {
log.error(`worker crashed could not find id`);
@@ -128,6 +137,7 @@ export async function startMaster() {
// Restart the worker with the same ID
const newWorker = cluster.fork({
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
WORKER_ID: workerId,
});
@@ -145,8 +155,8 @@ export async function startMaster() {
app.get(
"/api/env",
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
- const envConfig = {
- game_env: process.env.GAME_ENV,
+ const envConfig: ApiEnvResponse = {
+ game_env: process.env.GAME_ENV ?? "",
};
if (!envConfig.game_env) return res.sendStatus(500);
res.json(envConfig);
@@ -266,7 +276,7 @@ async function fetchLobbies(): Promise {
// Update the JSON string
publicLobbiesJsonStr = JSON.stringify({
lobbies: lobbyInfos,
- });
+ } satisfies ApiPublicLobbiesResponse);
return publicLobbyIDs.size;
}
diff --git a/src/server/Privilege.ts b/src/server/Privilege.ts
index 88908215f..6d7722020 100644
--- a/src/server/Privilege.ts
+++ b/src/server/Privilege.ts
@@ -1,7 +1,7 @@
import { Cosmetics, Pattern } from "../core/CosmeticSchemas";
import { PatternDecoder } from "../core/PatternDecoder";
-export interface PrivilegeChecker {
+export type PrivilegeChecker = {
isPatternAllowed(
base64: string,
flares: readonly string[] | undefined,
@@ -10,7 +10,7 @@ export interface PrivilegeChecker {
flag: string,
flares: readonly string[] | undefined,
): true | "restricted" | "invalid";
-}
+};
export class PrivilegeCheckerImpl implements PrivilegeChecker {
private b64ToPattern: Record = {};
diff --git a/src/server/Worker.ts b/src/server/Worker.ts
index 9f4bccfbc..0613175dd 100644
--- a/src/server/Worker.ts
+++ b/src/server/Worker.ts
@@ -10,7 +10,11 @@ import { GameEnv } from "../core/configuration/Config";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
import { GameRecord, GameRecordSchema, ID } from "../core/Schemas";
-import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
+import {
+ CreateGameInputSchema,
+ GameInputSchema,
+ WorkerApiGameIdExists,
+} from "../core/WorkerSchemas";
import { archive, readGameRecord } from "./Archive";
import { GameManager } from "./GameManager";
import { gatekeeper, LimiterType } from "./Gatekeeper";
@@ -201,7 +205,7 @@ export async function startWorker() {
const lobbyId = req.params.id;
res.json({
exists: gm.game(lobbyId) !== null,
- });
+ } satisfies WorkerApiGameIdExists);
}),
);
@@ -305,6 +309,7 @@ export async function startWorker() {
);
ws.on("error", (error: Error) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") {
ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1");
}
diff --git a/src/server/jwt.ts b/src/server/jwt.ts
index 98ebb518f..72c5d36da 100644
--- a/src/server/jwt.ts
+++ b/src/server/jwt.ts
@@ -11,9 +11,9 @@ import { PersistentIdSchema } from "../core/Schemas";
type TokenVerificationResult =
| {
- persistentId: string;
- claims: TokenPayload | null;
- }
+ persistentId: string;
+ claims: TokenPayload | null;
+ }
| false;
export async function verifyClientToken(
diff --git a/src/server/worker/websocket/handler/message/PostJoinHandler.ts b/src/server/worker/websocket/handler/message/PostJoinHandler.ts
index 83d2b242a..ada06af0b 100644
--- a/src/server/worker/websocket/handler/message/PostJoinHandler.ts
+++ b/src/server/worker/websocket/handler/message/PostJoinHandler.ts
@@ -2,6 +2,7 @@ import { Logger } from "winston";
import { z } from "zod";
import {
ClientMessageSchema,
+ ClientSendWinnerMessage,
ServerErrorMessage,
} from "../../../../../core/Schemas";
import { Client } from "../../../../Client";
@@ -79,6 +80,7 @@ export async function postJoinMessageHandler(
gs.kickClient(clientMsg.intent.target);
return;
}
+
default: {
gs.addIntent(clientMsg.intent);
break;
@@ -96,19 +98,11 @@ export async function postJoinMessageHandler(
break;
}
case "winner": {
- if (
- gs.outOfSyncClients.has(client.clientID) ||
- gs.kickedClients.has(client.clientID) ||
- gs.winner !== null
- ) {
- return;
- }
- gs.winner = clientMsg;
- gs.archiveGame();
+ handleWinner(gs, log, client, clientMsg);
break;
}
default: {
- log.warn(`Unknown message type: ${(clientMsg as any).type}`, {
+ log.warn(`Unknown message type: ${clientMsg.type}`, {
clientID: client.clientID,
});
break;
@@ -120,3 +114,49 @@ export async function postJoinMessageHandler(
});
}
}
+
+function handleWinner(
+ gs: GameServer,
+ log: Logger,
+ client: Client, clientMsg: ClientSendWinnerMessage) {
+ if (
+ gs.outOfSyncClients.has(client.clientID) ||
+ gs.kickedClients.has(client.clientID) ||
+ gs.winner !== null ||
+ client.reportedWinner !== null
+ ) {
+ return;
+ }
+ client.reportedWinner = clientMsg.winner;
+
+ // Add client vote
+ const winnerKey = JSON.stringify(clientMsg.winner);
+ if (!gs.winnerVotes.has(winnerKey)) {
+ gs.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg });
+ }
+ const potentialWinner = gs.winnerVotes.get(winnerKey)!;
+ potentialWinner.ips.add(client.ip);
+
+ const activeUniqueIPs = new Set(gs.activeClients.map((c) => c.ip));
+
+ // Require at least two unique IPs to agree
+ if (activeUniqueIPs.size < 2) {
+ return;
+ }
+
+ // Check if winner has majority
+ if (potentialWinner.ips.size * 2 < activeUniqueIPs.size) {
+ return;
+ }
+
+ // Vote succeeded
+ gs.winner = potentialWinner.winner;
+ log.info(
+ `Winner determined by ${potentialWinner.ips.size}/${activeUniqueIPs.size} active IPs`,
+ {
+ gameID: gs.id,
+ winnerKey: winnerKey,
+ },
+ );
+ gs.archiveGame();
+}
diff --git a/src/server/worker/websocket/handler/message/PreJoinHandler.ts b/src/server/worker/websocket/handler/message/PreJoinHandler.ts
index b35c74a4a..36b152969 100644
--- a/src/server/worker/websocket/handler/message/PreJoinHandler.ts
+++ b/src/server/worker/websocket/handler/message/PreJoinHandler.ts
@@ -62,13 +62,13 @@ async function handleJoinMessage(
): Promise<
| undefined
| {
- success: true;
- }
+ success: true;
+ }
| {
- success: false;
- code: 1002;
- error: string;
- reason:
+ success: false;
+ code: 1002;
+ error: string;
+ reason:
| "ClientJoinMessageSchema"
| "Flag invalid"
| "Flag restricted"
@@ -78,19 +78,19 @@ async function handleJoinMessage(
| "Pattern restricted"
| "Pattern unlisted"
| "Unauthorized";
- }
+ }
| {
- success: false;
- code: 1011;
- reason: "Internal server error";
- error: string;
- }
+ success: false;
+ code: 1011;
+ reason: "Internal server error";
+ error: string;
+ }
> {
const forwarded = req.headers["x-forwarded-for"];
const ip = Array.isArray(forwarded)
? forwarded[0]
: // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- forwarded || req.socket.remoteAddress || "unknown";
+ forwarded || req.socket.remoteAddress || "unknown";
try {
// Parse and handle client messages
diff --git a/tests/AutoUpgrade.test.ts b/tests/AutoUpgrade.test.ts
index 126929098..2d14933eb 100644
--- a/tests/AutoUpgrade.test.ts
+++ b/tests/AutoUpgrade.test.ts
@@ -147,9 +147,7 @@ describe("AutoUpgrade Feature", () => {
const event = new AutoUpgradeEvent(100, 200);
const eventString = JSON.stringify(event);
const parsedEvent = JSON.parse(eventString);
-
- expect(parsedEvent.x).toBe(100);
- expect(parsedEvent.y).toBe(200);
+ expect(parsedEvent).toStrictEqual({ x: 100, y: 200 });
});
});
});
diff --git a/tests/Donate.test.ts b/tests/Donate.test.ts
new file mode 100644
index 000000000..bbbccd4f9
--- /dev/null
+++ b/tests/Donate.test.ts
@@ -0,0 +1,252 @@
+import { DonateGoldExecution } from "../src/core/execution/DonateGoldExecution";
+import { DonateTroopsExecution } from "../src/core/execution/DonateTroopExecution";
+import { SpawnExecution } from "../src/core/execution/SpawnExecution";
+import { PlayerInfo, PlayerType } from "../src/core/game/Game";
+import { setup } from "./util/Setup";
+
+describe("Donate troops to an ally", () => {
+ it("Troops should be successfully donated", async () => {
+ const game = await setup("ocean_and_land", {
+ infiniteTroops: false,
+ donateTroops: true,
+ });
+
+ const donorInfo = new PlayerInfo(
+ "donor",
+ PlayerType.Human,
+ null,
+ "donor_id",
+ );
+ const recipientInfo = new PlayerInfo(
+ "recipient",
+ PlayerType.Human,
+ null,
+ "recipient_id",
+ );
+
+ game.addPlayer(donorInfo);
+ game.addPlayer(recipientInfo);
+
+ const donor = game.player(donorInfo.id);
+ const recipient = game.player(recipientInfo.id);
+
+ // Spawn both players
+ const spawnA = game.ref(0, 10);
+ const spawnB = game.ref(0, 15);
+
+ game.addExecution(
+ new SpawnExecution(donorInfo, spawnA),
+ new SpawnExecution(recipientInfo, spawnB),
+ );
+
+ while (game.inSpawnPhase()) {
+ game.executeNextTick();
+ }
+
+ // donor sends alliance request to recipient
+ const allianceRequest = donor.createAllianceRequest(recipient);
+ expect(allianceRequest).not.toBeNull();
+
+ // recipient accepts the alliance request
+ if (allianceRequest) {
+ allianceRequest.accept();
+ }
+
+ // Ensure donor can actually donate the requested amount
+ donor.addTroops(6000);
+ const donorTroopsBefore = donor.troops();
+ const recipientTroopsBefore = recipient.troops();
+ game.addExecution(new DonateTroopsExecution(donor, recipientInfo.id, 5000));
+
+ for (let i = 0; i < 5; i++) {
+ game.executeNextTick();
+ }
+
+ expect(donor.troops() < donorTroopsBefore).toBe(true);
+ expect(recipient.troops() > recipientTroopsBefore).toBe(true);
+ });
+});
+
+describe("Donate gold to an ally", () => {
+ it("Gold should be successfully donated", async () => {
+ const game = await setup("ocean_and_land", {
+ infiniteGold: false,
+ donateGold: true,
+ });
+
+ const donorInfo = new PlayerInfo(
+ "donor",
+ PlayerType.Human,
+ null,
+ "donor_id",
+ );
+ const recipientInfo = new PlayerInfo(
+ "recipient",
+ PlayerType.Human,
+ null,
+ "recipient_id",
+ );
+
+ game.addPlayer(donorInfo);
+ game.addPlayer(recipientInfo);
+
+ const donor = game.player(donorInfo.id);
+ const recipient = game.player(recipientInfo.id);
+
+ // Spawn both players
+ const spawnA = game.ref(0, 10);
+ const spawnB = game.ref(0, 15);
+
+ game.addExecution(
+ new SpawnExecution(donorInfo, spawnA),
+ new SpawnExecution(recipientInfo, spawnB),
+ );
+
+ while (game.inSpawnPhase()) {
+ game.executeNextTick();
+ }
+
+ // donor sends alliance request to recipient
+ const allianceRequest = donor.createAllianceRequest(recipient);
+ expect(allianceRequest).not.toBeNull();
+
+ // recipient accepts the alliance request
+ if (allianceRequest) {
+ allianceRequest.accept();
+ }
+ game.executeNextTick();
+
+ // Ensure donor can actually donate the requested amount
+ donor.addGold(6000n);
+ const donorGoldBefore = donor.gold();
+ const recipientGoldBefore = recipient.gold();
+ game.addExecution(new DonateGoldExecution(donor, recipientInfo.id, 5000n));
+
+ for (let i = 0; i < 5; i++) {
+ game.executeNextTick();
+ }
+
+ expect(donor.gold() < donorGoldBefore).toBe(true);
+ expect(recipient.gold() > recipientGoldBefore).toBe(true);
+ });
+});
+
+describe("Donate troops to a non ally", () => {
+ it("Troops should not be donated", async () => {
+ const game = await setup("ocean_and_land", {
+ infiniteTroops: false,
+ donateTroops: true,
+ });
+
+ const donorInfo = new PlayerInfo(
+ "donor",
+ PlayerType.Human,
+ null,
+ "donor_id",
+ );
+ const recipientInfo = new PlayerInfo(
+ "recipient",
+ PlayerType.Human,
+ null,
+ "recipient_id",
+ );
+
+ game.addPlayer(donorInfo);
+ game.addPlayer(recipientInfo);
+
+ const donor = game.player(donorInfo.id);
+ const recipient = game.player(recipientInfo.id);
+
+ // Spawn both players
+ const spawnA = game.ref(0, 10);
+ const spawnB = game.ref(0, 15);
+
+ game.addExecution(
+ new SpawnExecution(donorInfo, spawnA),
+ new SpawnExecution(recipientInfo, spawnB),
+ );
+
+ while (game.inSpawnPhase()) {
+ game.executeNextTick();
+ }
+
+ // Donor sends alliance request to Recipient
+ const allianceRequest = donor.createAllianceRequest(recipient);
+ expect(allianceRequest).not.toBeNull();
+
+ // Donor rejects the Recipient
+ if (allianceRequest) {
+ allianceRequest.reject();
+ }
+
+ const donorTroopsBefore = donor.troops();
+ const recipientTroopsBefore = recipient.troops();
+
+ game.addExecution(new DonateTroopsExecution(donor, recipientInfo.id, 5000));
+ game.executeNextTick();
+
+ // Troops should not be donated since they are not allies
+ expect(donor.troops() >= donorTroopsBefore).toBe(true);
+ expect(recipient.troops() >= recipientTroopsBefore).toBe(true);
+ });
+});
+
+describe("Donate Gold to a non ally", () => {
+ it("Gold should not be donated", async () => {
+ const game = await setup("ocean_and_land", {
+ infiniteGold: false,
+ donateGold: true,
+ });
+
+ const donorInfo = new PlayerInfo(
+ "donor",
+ PlayerType.Human,
+ null,
+ "donor_id",
+ );
+ const recipientInfo = new PlayerInfo(
+ "recipient",
+ PlayerType.Human,
+ null,
+ "recipient_id",
+ );
+
+ game.addPlayer(donorInfo);
+ game.addPlayer(recipientInfo);
+
+ const donor = game.player(donorInfo.id);
+ const recipient = game.player(recipientInfo.id);
+
+ // Spawn both players
+ const spawnA = game.ref(0, 10);
+ const spawnB = game.ref(0, 15);
+
+ game.addExecution(
+ new SpawnExecution(donorInfo, spawnA),
+ new SpawnExecution(recipientInfo, spawnB),
+ );
+
+ while (game.inSpawnPhase()) {
+ game.executeNextTick();
+ }
+
+ // Donor sends alliance request to Recipient
+ const allianceRequest = donor.createAllianceRequest(recipient);
+ expect(allianceRequest).not.toBeNull();
+
+ // Donor rejects the Recipient
+ if (allianceRequest) {
+ allianceRequest.reject();
+ }
+
+ const donorGoldBefore = donor.gold();
+ const recipientGoldBefore = donor.gold();
+
+ game.addExecution(new DonateGoldExecution(donor, recipientInfo.id, 5000n));
+ game.executeNextTick();
+
+ // Gold should not be donated since they are not allies
+ expect(donor.gold() >= donorGoldBefore).toBe(true);
+ expect(recipient.gold() >= recipientGoldBefore).toBe(true);
+ });
+});
diff --git a/tests/LangCode.test.ts b/tests/LangCode.test.ts
index fc4d4520b..d21695000 100644
--- a/tests/LangCode.test.ts
+++ b/tests/LangCode.test.ts
@@ -1,5 +1,6 @@
import fs from "fs";
import path from "path";
+import { z } from "zod";
describe("LangCode Filename Check", () => {
const langDir = path.join(__dirname, "../resources/lang");
@@ -14,9 +15,17 @@ describe("LangCode Filename Check", () => {
return;
}
+ const schema = z.object({
+ lang: z.object({
+ lang_code: z.string(),
+ }),
+ });
+
for (const file of files) {
const filePath = path.join(langDir, file);
- const jsonData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
+ const jsonData = schema.parse(
+ JSON.parse(fs.readFileSync(filePath, "utf-8")),
+ );
const fileNameWithoutExt = path.basename(file, ".json");
const langCode = jsonData.lang?.lang_code;
diff --git a/tests/LangSvg.test.ts b/tests/LangSvg.test.ts
index 3680e8441..07b54b763 100644
--- a/tests/LangSvg.test.ts
+++ b/tests/LangSvg.test.ts
@@ -21,7 +21,14 @@ describe("Lang SVG Field and File Existence Check", () => {
try {
const filePath = path.join(langDir, file);
const jsonData = JSON.parse(fs.readFileSync(filePath, "utf-8"));
- const langSvg = jsonData.lang?.svg;
+ const langSvg =
+ jsonData &&
+ typeof jsonData === "object" &&
+ "lang" in jsonData &&
+ jsonData.lang &&
+ typeof jsonData.lang === "object" &&
+ "svg" in jsonData.lang &&
+ jsonData.lang.svg;
if (typeof langSvg !== "string" || langSvg.length === 0) {
errors.push(
`[${file}]: lang.svg is missing or not a non-empty string`,
diff --git a/tests/client/graphics/RadialMenuElements.test.ts b/tests/client/graphics/RadialMenuElements.test.ts
index 64821581a..df8c796d4 100644
--- a/tests/client/graphics/RadialMenuElements.test.ts
+++ b/tests/client/graphics/RadialMenuElements.test.ts
@@ -129,7 +129,8 @@ describe("RadialMenuElements", () => {
interaction: {
canSendAllianceRequest: true,
canBreakAlliance: false,
- canDonate: true,
+ canDonateTroops: true,
+ canDonateGold: true,
},
};
diff --git a/tests/testdata/maps/big_plains/manifest.json b/tests/testdata/maps/big_plains/manifest.json
index 3d3f9f3e2..8b2908a4a 100644
--- a/tests/testdata/maps/big_plains/manifest.json
+++ b/tests/testdata/maps/big_plains/manifest.json
@@ -9,5 +9,6 @@
"num_land_tiles": 10000,
"width": 100
},
+ "nations": [],
"name": "Big Plains"
}
diff --git a/tests/testdata/maps/half_land_half_ocean/manifest.json b/tests/testdata/maps/half_land_half_ocean/manifest.json
index d28f3fdda..ec13acbe7 100644
--- a/tests/testdata/maps/half_land_half_ocean/manifest.json
+++ b/tests/testdata/maps/half_land_half_ocean/manifest.json
@@ -9,5 +9,6 @@
"num_land_tiles": 48,
"width": 8
},
+ "nations": [],
"name": "Half Land Half Ocean"
}
diff --git a/tests/testdata/maps/ocean_and_land/manifest.json b/tests/testdata/maps/ocean_and_land/manifest.json
index c8bacdc39..7d94ba50e 100644
--- a/tests/testdata/maps/ocean_and_land/manifest.json
+++ b/tests/testdata/maps/ocean_and_land/manifest.json
@@ -9,5 +9,6 @@
"num_land_tiles": 50,
"width": 8
},
+ "nations": [],
"name": "Ocean and Land"
}
diff --git a/tests/testdata/maps/plains/manifest.json b/tests/testdata/maps/plains/manifest.json
index 3b013ee24..852de2478 100644
--- a/tests/testdata/maps/plains/manifest.json
+++ b/tests/testdata/maps/plains/manifest.json
@@ -9,5 +9,6 @@
"num_land_tiles": 2500,
"width": 50
},
+ "nations": [],
"name": "Plains"
}
diff --git a/tests/util/Setup.ts b/tests/util/Setup.ts
index 3253eb9a8..c170a2dd3 100644
--- a/tests/util/Setup.ts
+++ b/tests/util/Setup.ts
@@ -1,5 +1,6 @@
import fs from "fs";
import path from "path";
+import { z } from "zod";
import {
Difficulty,
Game,
@@ -12,7 +13,7 @@ import {
import { createGame } from "../../src/core/game/GameImpl";
import {
genTerrainFromBin,
- MapManifest,
+ MapManifestSchema,
} from "../../src/core/game/TerrainMapLoader";
import { UserSettings } from "../../src/core/game/UserSettings";
import { GameConfig } from "../../src/core/Schemas";
@@ -44,9 +45,14 @@ export async function setup(
const mapBinBuffer = fs.readFileSync(mapBinPath);
const miniMapBinBuffer = fs.readFileSync(miniMapBinPath);
- const manifest = JSON.parse(
- fs.readFileSync(manifestPath, "utf8"),
- ) satisfies MapManifest;
+ const str = fs.readFileSync(manifestPath, "utf8");
+ const raw = JSON.parse(str);
+ const parsed = MapManifestSchema.safeParse(raw);
+ if (!parsed.success) {
+ const error = z.prettifyError(parsed.error);
+ throw new Error(`Error parsing ${manifestPath}: ${error}`);
+ }
+ const manifest = parsed.data;
const gameMap = await genTerrainFromBin(manifest.map, mapBinBuffer);
const miniGameMap = await genTerrainFromBin(
@@ -60,6 +66,8 @@ export async function setup(
bots: 0,
difficulty: Difficulty.Medium,
disableNPCs: false,
+ donateGold: false,
+ donateTroops: false,
gameMap: GameMapType.Asia,
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
diff --git a/tests/util/TestConfig.ts b/tests/util/TestConfig.ts
index f0b10ac37..69b23f3cd 100644
--- a/tests/util/TestConfig.ts
+++ b/tests/util/TestConfig.ts
@@ -10,8 +10,8 @@ import {
import { TileRef } from "../../src/core/game/GameMap";
export class TestConfig extends DefaultConfig {
- private _proximityBonusPortsNb: number = 0;
- private _defaultNukeSpeed: number = 4;
+ private _proximityBonusPortsNb = 0;
+ private _defaultNukeSpeed = 4;
samHittingChance(): number {
return 1;
diff --git a/tsconfig.json b/tsconfig.json
index 7c73290f3..6de45daab 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,7 +21,8 @@
"resolveJsonModule": true,
"strictNullChecks": true,
"useDefineForClassFields": false,
- "strictPropertyInitialization": false
+ "strictPropertyInitialization": false,
+ "strict": true
},
"include": [
"src/**/*",
diff --git a/webpack.config.js b/webpack.config.js
index 0820e8cd0..ca0708e92 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -111,13 +111,13 @@ export default async (env, argv) => {
// Add optimization for HTML
minify: isProduction
? {
- collapseWhitespace: true,
- removeComments: true,
- removeRedundantAttributes: true,
- removeScriptTypeAttributes: true,
- removeStyleLinkTypeAttributes: true,
- useShortDoctype: true,
- }
+ collapseWhitespace: true,
+ removeComments: true,
+ removeRedundantAttributes: true,
+ removeScriptTypeAttributes: true,
+ removeStyleLinkTypeAttributes: true,
+ useShortDoctype: true,
+ }
: false,
}),
new webpack.DefinePlugin({
@@ -160,91 +160,91 @@ export default async (env, argv) => {
devServer: isProduction
? {}
: {
- devMiddleware: { writeToDisk: true },
- static: {
- directory: path.join(__dirname, "static"),
- },
- historyApiFallback: true,
- compress: true,
- port: 9000,
- proxy: [
- // WebSocket proxies
- {
- context: ["/socket"],
- target: "ws://localhost:3000",
- ws: true,
- changeOrigin: true,
- logLevel: "debug",
- },
- // Worker WebSocket proxies - using direct paths without /socket suffix
- {
- context: ["/w0"],
- target: "ws://localhost:3001",
- ws: true,
- secure: false,
- changeOrigin: true,
- logLevel: "debug",
- },
- {
- context: ["/w1"],
- target: "ws://localhost:3002",
- ws: true,
- secure: false,
- changeOrigin: true,
- logLevel: "debug",
- },
- {
- context: ["/w2"],
- target: "ws://localhost:3003",
- ws: true,
- secure: false,
- changeOrigin: true,
- logLevel: "debug",
- },
- // Worker proxies for HTTP requests
- {
- context: ["/w0"],
- target: "http://localhost:3001",
- pathRewrite: { "^/w0": "" },
- secure: false,
- changeOrigin: true,
- logLevel: "debug",
- },
- {
- context: ["/w1"],
- target: "http://localhost:3002",
- pathRewrite: { "^/w1": "" },
- secure: false,
- changeOrigin: true,
- logLevel: "debug",
- },
- {
- context: ["/w2"],
- target: "http://localhost:3003",
- pathRewrite: { "^/w2": "" },
- secure: false,
- changeOrigin: true,
- logLevel: "debug",
- },
- // Original API endpoints
- {
- context: [
- "/api/env",
- "/api/game",
- "/api/public_lobbies",
- "/api/join_game",
- "/api/start_game",
- "/api/create_game",
- "/api/archive_singleplayer_game",
- "/api/auth/callback",
- "/api/auth/discord",
- "/api/kick_player",
- ],
- target: "http://localhost:3000",
- secure: false,
- changeOrigin: true,
- },
- ],
+ devMiddleware: { writeToDisk: true },
+ static: {
+ directory: path.join(__dirname, "static"),
},
+ historyApiFallback: true,
+ compress: true,
+ port: 9000,
+ proxy: [
+ // WebSocket proxies
+ {
+ context: ["/socket"],
+ target: "ws://localhost:3000",
+ ws: true,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ // Worker WebSocket proxies - using direct paths without /socket suffix
+ {
+ context: ["/w0"],
+ target: "ws://localhost:3001",
+ ws: true,
+ secure: false,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ {
+ context: ["/w1"],
+ target: "ws://localhost:3002",
+ ws: true,
+ secure: false,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ {
+ context: ["/w2"],
+ target: "ws://localhost:3003",
+ ws: true,
+ secure: false,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ // Worker proxies for HTTP requests
+ {
+ context: ["/w0"],
+ target: "http://localhost:3001",
+ pathRewrite: { "^/w0": "" },
+ secure: false,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ {
+ context: ["/w1"],
+ target: "http://localhost:3002",
+ pathRewrite: { "^/w1": "" },
+ secure: false,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ {
+ context: ["/w2"],
+ target: "http://localhost:3003",
+ pathRewrite: { "^/w2": "" },
+ secure: false,
+ changeOrigin: true,
+ logLevel: "debug",
+ },
+ // Original API endpoints
+ {
+ context: [
+ "/api/env",
+ "/api/game",
+ "/api/public_lobbies",
+ "/api/join_game",
+ "/api/start_game",
+ "/api/create_game",
+ "/api/archive_singleplayer_game",
+ "/api/auth/callback",
+ "/api/auth/discord",
+ "/api/kick_player",
+ ],
+ target: "http://localhost:3000",
+ secure: false,
+ changeOrigin: true,
+ },
+ ],
+ },
};
};