diff --git a/resources/lang/en.json b/resources/lang/en.json
index da453ae7b..0422bd517 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -171,7 +171,11 @@
"max_timer_invalid": "Please enter a valid max timer value (1-120 minutes)",
"disable_nukes": "Disable Nukes",
"enables_title": "Enable Settings",
- "start": "Start Game"
+ "start": "Start Game",
+ "gold_multiplier": "Gold multiplier",
+ "gold_multiplier_placeholder": "2.0x",
+ "starting_gold": "Starting gold",
+ "starting_gold_placeholder": "5000000"
},
"token_login_modal": {
"title": "Logging in...",
@@ -379,7 +383,11 @@
"teams_Duos": "Duos (teams of 2)",
"teams_Trios": "Trios (teams of 3)",
"teams_Quads": "Quads (teams of 4)",
- "teams_Humans Vs Nations": "Humans vs Nations"
+ "teams_Humans Vs Nations": "Humans vs Nations",
+ "gold_multiplier": "Gold multiplier",
+ "gold_multiplier_placeholder": "2.0x",
+ "starting_gold": "Starting gold",
+ "starting_gold_placeholder": "5000000"
},
"team_colors": {
"red": "Red",
@@ -409,7 +417,8 @@
},
"public_game_modifier": {
"random_spawn": "Random Spawn",
- "compact_map": "Compact Map"
+ "compact_map": "Compact Map",
+ "starting_gold": "5M Starting Gold"
},
"select_lang": {
"title": "Select Language"
diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts
index 5abdb60a9..e7d709c12 100644
--- a/src/client/HostLobbyModal.ts
+++ b/src/client/HostLobbyModal.ts
@@ -60,6 +60,10 @@ export class HostLobbyModal extends BaseModal {
@state() private instantBuild: boolean = false;
@state() private randomSpawn: boolean = false;
@state() private compactMap: boolean = false;
+ @state() private goldMultiplier: boolean = false;
+ @state() private goldMultiplierValue: number | undefined = undefined;
+ @state() private startingGold: boolean = false;
+ @state() private startingGoldValue: number | undefined = undefined;
@state() private lobbyId = "";
@state() private copySuccess = false;
@state() private lobbyUrlSuffix = "";
@@ -739,6 +743,158 @@ export class HostLobbyModal extends BaseModal {
${translateText("host_modal.player_immunity_duration")}
+
+
+
this.goldMultiplier,
+ (val) => (this.goldMultiplier = val),
+ () => this.goldMultiplierValue,
+ (val) => (this.goldMultiplierValue = val),
+ 2,
+ ).click}
+ @keydown=${this.createToggleHandlers(
+ () => this.goldMultiplier,
+ (val) => (this.goldMultiplier = val),
+ () => this.goldMultiplierValue,
+ (val) => (this.goldMultiplierValue = val),
+ 2,
+ ).keydown}
+ class="relative p-3 rounded-xl border transition-all duration-200 flex flex-col items-center justify-between gap-2 h-full cursor-pointer min-h-[100px] ${this
+ .goldMultiplier
+ ? "bg-blue-500/20 border-blue-500/50"
+ : "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}"
+ >
+
+
+ ${this.goldMultiplier
+ ? html`
`
+ : ""}
+
+
+
+ ${this.goldMultiplier
+ ? html`
`
+ : html`
`}
+
+
+ ${translateText("single_modal.gold_multiplier")}
+
+
+
+
+ this.startingGold,
+ (val) => (this.startingGold = val),
+ () => this.startingGoldValue,
+ (val) => (this.startingGoldValue = val),
+ 5000000,
+ ).click}
+ @keydown=${this.createToggleHandlers(
+ () => this.startingGold,
+ (val) => (this.startingGold = val),
+ () => this.startingGoldValue,
+ (val) => (this.startingGoldValue = val),
+ 5000000,
+ ).keydown}
+ class="relative p-3 rounded-xl border transition-all duration-200 flex flex-col items-center justify-between gap-2 h-full cursor-pointer min-h-[100px] ${this
+ .startingGold
+ ? "bg-blue-500/20 border-blue-500/50"
+ : "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20"}"
+ >
+
+
+ ${this.startingGold
+ ? html`
`
+ : ""}
+
+
+
+ ${this.startingGold
+ ? html`
`
+ : html`
`}
+
+
+ ${translateText("single_modal.starting_gold")}
+
+
@@ -968,6 +1124,10 @@ export class HostLobbyModal extends BaseModal {
this.lobbyCreatorClientID = "";
this.lobbyIdVisible = true;
this.nationCount = 0;
+ this.goldMultiplier = false;
+ this.goldMultiplierValue = undefined;
+ this.startingGold = false;
+ this.startingGoldValue = undefined;
this.leaveLobbyOnClose = true;
}
@@ -1036,6 +1196,44 @@ export class HostLobbyModal extends BaseModal {
this.putGameConfig();
}
+ private handleGoldMultiplierValueKeyDown(e: KeyboardEvent) {
+ if (["+", "-", "e", "E"].includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+
+ private handleGoldMultiplierValueChanges(e: Event) {
+ const input = e.target as HTMLInputElement;
+ const value = parseFloat(input.value);
+
+ if (isNaN(value) || value < 0.1 || value > 1000) {
+ this.goldMultiplierValue = undefined;
+ input.value = "";
+ } else {
+ this.goldMultiplierValue = value;
+ }
+ this.putGameConfig();
+ }
+
+ private handleStartingGoldValueKeyDown(e: KeyboardEvent) {
+ if (["-", "+", "e", "E"].includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+
+ private handleStartingGoldValueChanges(e: Event) {
+ const input = e.target as HTMLInputElement;
+ input.value = input.value.replace(/[eE+-]/g, "");
+ const value = parseInt(input.value);
+
+ if (isNaN(value) || value < 0 || value > 1000000000) {
+ this.startingGoldValue = undefined;
+ } else {
+ this.startingGoldValue = value;
+ }
+ this.putGameConfig();
+ }
+
private handleRandomSpawnChange = (val: boolean) => {
this.randomSpawn = val;
this.putGameConfig();
@@ -1151,6 +1349,12 @@ export class HostLobbyModal extends BaseModal {
}),
maxTimerValue:
this.maxTimer === true ? this.maxTimerValue : undefined,
+ goldMultiplier:
+ this.goldMultiplier === true
+ ? this.goldMultiplierValue
+ : undefined,
+ startingGold:
+ this.startingGold === true ? this.startingGoldValue : undefined,
} satisfies Partial,
},
bubbles: true,
diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts
index c7516804d..4c895ab8f 100644
--- a/src/client/PublicLobby.ts
+++ b/src/client/PublicLobby.ts
@@ -374,6 +374,9 @@ export class PublicLobby extends LitElement {
if (publicGameModifiers.isCompact) {
labels.push(translateText("public_game_modifier.compact_map"));
}
+ if (publicGameModifiers.startingGold) {
+ labels.push(translateText("public_game_modifier.starting_gold"));
+ }
return labels;
}
diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts
index 12c805751..c34dfe268 100644
--- a/src/client/SinglePlayerModal.ts
+++ b/src/client/SinglePlayerModal.ts
@@ -52,6 +52,10 @@ export class SinglePlayerModal extends BaseModal {
@state() private showAchievements: boolean = false;
@state() private mapWins: Map> = new Map();
@state() private userMeResponse: UserMeResponse | false = false;
+ @state() private goldMultiplier: boolean = false;
+ @state() private goldMultiplierValue: number | undefined = undefined;
+ @state() private startingGold: boolean = false;
+ @state() private startingGoldValue: number | undefined = undefined;
@state() private disabledUnits: UnitType[] = [];
@@ -601,6 +605,180 @@ export class SinglePlayerModal extends BaseModal {
${translateText("single_modal.max_timer")}
+
+
+ {
+ if (
+ (e.target as HTMLElement).tagName.toLowerCase() ===
+ "input"
+ )
+ return;
+ this.goldMultiplier = !this.goldMultiplier;
+ if (!this.goldMultiplier) {
+ this.goldMultiplierValue = undefined;
+ } else {
+ if (
+ !this.goldMultiplierValue ||
+ this.goldMultiplierValue <= 0
+ ) {
+ this.goldMultiplierValue = 2;
+ }
+ setTimeout(() => {
+ const input = this.renderRoot.querySelector(
+ "#gold-multiplier-value",
+ ) as HTMLInputElement;
+ if (input) {
+ input.focus();
+ input.select();
+ }
+ }, 0);
+ }
+ }}
+ >
+
+
+ ${this.goldMultiplier
+ ? html`
`
+ : ""}
+
+
+
+ ${this.goldMultiplier
+ ? html`
`
+ : html`
`}
+
+
+ ${translateText("single_modal.gold_multiplier")}
+
+
+
+
+ {
+ if (
+ (e.target as HTMLElement).tagName.toLowerCase() ===
+ "input"
+ )
+ return;
+ this.startingGold = !this.startingGold;
+ if (!this.startingGold) {
+ this.startingGoldValue = undefined;
+ } else {
+ if (
+ !this.startingGoldValue ||
+ this.startingGoldValue < 0
+ ) {
+ this.startingGoldValue = 5000000;
+ }
+ setTimeout(() => {
+ const input = this.renderRoot.querySelector(
+ "#starting-gold-value",
+ ) as HTMLInputElement;
+ if (input) {
+ input.focus();
+ input.select();
+ }
+ }, 0);
+ }
+ }}
+ >
+
+
+ ${this.startingGold
+ ? html`
`
+ : ""}
+
+
+
+ ${this.startingGold
+ ? html`
`
+ : html`
`}
+
+
+ ${translateText("single_modal.starting_gold")}
+
+
@@ -714,6 +892,10 @@ export class SinglePlayerModal extends BaseModal {
this.randomSpawn = false;
this.teamCount = 2;
this.disabledUnits = [];
+ this.goldMultiplier = false;
+ this.goldMultiplierValue = undefined;
+ this.startingGold = false;
+ this.startingGoldValue = undefined;
}
private handleSelectRandomMap() {
@@ -767,6 +949,42 @@ export class SinglePlayerModal extends BaseModal {
}
}
+ private handleGoldMultiplierValueKeyDown(e: KeyboardEvent) {
+ if (["+", "-", "e", "E"].includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+
+ private handleGoldMultiplierValueChanges(e: Event) {
+ const input = e.target as HTMLInputElement;
+ const value = parseFloat(input.value);
+
+ if (isNaN(value) || value < 0.1 || value > 1000) {
+ this.goldMultiplierValue = undefined;
+ input.value = "";
+ } else {
+ this.goldMultiplierValue = value;
+ }
+ }
+
+ private handleStartingGoldValueKeyDown(e: KeyboardEvent) {
+ if (["-", "+", "e", "E"].includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+
+ private handleStartingGoldValueChanges(e: Event) {
+ const input = e.target as HTMLInputElement;
+ input.value = input.value.replace(/[eE+-]/g, "");
+ const value = parseInt(input.value);
+
+ if (isNaN(value) || value < 0 || value > 1000000000) {
+ this.startingGoldValue = undefined;
+ } else {
+ this.startingGoldValue = value;
+ }
+ }
+
private handleGameModeSelection(value: GameMode) {
this.gameMode = value;
}
@@ -888,6 +1106,12 @@ export class SinglePlayerModal extends BaseModal {
: {
disableNations: this.disableNations,
}),
+ ...(this.goldMultiplier && this.goldMultiplierValue
+ ? { goldMultiplier: this.goldMultiplierValue }
+ : {}),
+ ...(this.startingGold && this.startingGoldValue !== undefined
+ ? { startingGold: this.startingGoldValue }
+ : {}),
},
lobbyCreatedAt: Date.now(), // ms; server should be authoritative in MP
},
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index 10a1a84b2..28362063f 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -190,6 +190,7 @@ export const GameConfigSchema = z.object({
.object({
isCompact: z.boolean(),
isRandomSpawn: z.boolean(),
+ startingGold: z.number().int().min(0).optional(),
})
.optional(),
disableNations: z.boolean(),
@@ -204,6 +205,8 @@ export const GameConfigSchema = z.object({
spawnImmunityDuration: z.number().int().min(0).optional(), // In ticks
disabledUnits: z.enum(UnitType).array().optional(),
playerTeams: TeamCountConfigSchema.optional(),
+ goldMultiplier: z.number().min(0.1).max(1000).optional(),
+ startingGold: z.number().int().min(0).max(1000000000).optional(),
});
export const TeamSchema = z.string();
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts
index 4c9fce54d..ac1d9ee4a 100644
--- a/src/core/configuration/Config.ts
+++ b/src/core/configuration/Config.ts
@@ -76,6 +76,8 @@ export interface Config {
numSpawnPhaseTurns(): number;
userSettings(): UserSettings;
playerTeams(): TeamCountConfig;
+ goldMultiplier(): number;
+ startingGold(playerInfo: PlayerInfo): Gold;
startManpower(playerInfo: PlayerInfo): number;
troopIncreaseRate(player: Player | PlayerView): number;
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index 7311cb60c..36057bdad 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -245,6 +245,15 @@ export class DefaultConfig implements Config {
donateTroops(): boolean {
return this._gameConfig.donateTroops;
}
+ goldMultiplier(): number {
+ return this._gameConfig.goldMultiplier ?? 1;
+ }
+ startingGold(playerInfo: PlayerInfo): Gold {
+ if (playerInfo.playerType === PlayerType.Bot) {
+ return 0n;
+ }
+ return BigInt(this._gameConfig.startingGold ?? 0);
+ }
trainSpawnRate(numPlayerFactories: number): number {
// hyperbolic decay, midpoint at 10 factories
@@ -252,15 +261,21 @@ export class DefaultConfig implements Config {
return (numPlayerFactories + 10) * 18;
}
trainGold(rel: "self" | "team" | "ally" | "other"): Gold {
+ const multiplier = this.goldMultiplier();
+ let baseGold: bigint;
switch (rel) {
case "ally":
- return 35_000n;
+ baseGold = 35_000n;
+ break;
case "team":
case "other":
- return 25_000n;
+ baseGold = 25_000n;
+ break;
case "self":
- return 10_000n;
+ baseGold = 10_000n;
+ break;
}
+ return BigInt(Math.floor(Number(baseGold) * multiplier));
}
trainStationMinRange(): number {
@@ -281,7 +296,8 @@ export class DefaultConfig implements Config {
const numPortBonus = numPorts - 1;
// Hyperbolic decay, midpoint at 5 ports, 3x bonus max.
const bonus = 1 + 2 * (numPortBonus / (numPortBonus + 5));
- return BigInt(Math.floor(baseGold * bonus));
+ const multiplier = this.goldMultiplier();
+ return BigInt(Math.floor(baseGold * bonus * multiplier));
}
// Probability of trade ship spawn = 1 / tradeShipSpawnRate
@@ -791,10 +807,14 @@ export class DefaultConfig implements Config {
}
goldAdditionRate(player: Player): Gold {
+ const multiplier = this.goldMultiplier();
+ let baseRate: bigint;
if (player.type() === PlayerType.Bot) {
- return 50n;
+ baseRate = 50n;
+ } else {
+ baseRate = 100n;
}
- return 100n;
+ return BigInt(Math.floor(Number(baseRate) * multiplier));
}
nukeMagnitudes(unitType: UnitType): NukeMagnitude {
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index 4ead6efe5..fdfff12d8 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -211,6 +211,7 @@ export enum GameMapSize {
export interface PublicGameModifiers {
isCompact: boolean;
isRandomSpawn: boolean;
+ startingGold?: number;
}
export interface UnitInfo {
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index 3b773576e..e09360acc 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -112,7 +112,7 @@ export class PlayerImpl implements Player {
) {
this._name = playerInfo.name;
this._troops = toInt(startTroops);
- this._gold = 0n;
+ this._gold = mg.config().startingGold(playerInfo);
this._displayName = this._name;
this._pseudo_random = new PseudoRandom(simpleHash(this.playerInfo.id));
}
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index 068253920..1f685a72e 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -127,14 +127,18 @@ export class GameServer {
if (gameConfig.gameMode !== undefined) {
this.gameConfig.gameMode = gameConfig.gameMode;
}
-
if (gameConfig.disabledUnits !== undefined) {
this.gameConfig.disabledUnits = gameConfig.disabledUnits;
}
-
if (gameConfig.playerTeams !== undefined) {
this.gameConfig.playerTeams = gameConfig.playerTeams;
}
+ if (gameConfig.goldMultiplier !== undefined) {
+ this.gameConfig.goldMultiplier = gameConfig.goldMultiplier;
+ }
+ if (gameConfig.startingGold !== undefined) {
+ this.gameConfig.startingGold = gameConfig.startingGold;
+ }
}
public joinClient(client: Client) {
diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts
index a9ba0e78d..beadd0bec 100644
--- a/src/server/MapPlaylist.ts
+++ b/src/server/MapPlaylist.ts
@@ -94,7 +94,9 @@ export class MapPlaylist {
const playerTeams =
mode === GameMode.Team ? this.getTeamCount() : undefined;
- let { isCompact, isRandomSpawn } = this.getRandomPublicGameModifiers();
+ const modifiers = this.getRandomPublicGameModifiers();
+ const { startingGold } = modifiers;
+ let { isCompact, isRandomSpawn } = modifiers;
// Duos, Trios, and Quads should not get random spawn (as it defeats the purpose)
if (
@@ -122,7 +124,8 @@ export class MapPlaylist {
maxPlayers: await this.lobbyMaxPlayers(map, mode, playerTeams, isCompact),
gameType: GameType.Public,
gameMapSize: isCompact ? GameMapSize.Compact : GameMapSize.Normal,
- publicGameModifiers: { isCompact, isRandomSpawn },
+ publicGameModifiers: { isCompact, isRandomSpawn, startingGold },
+ startingGold,
difficulty:
playerTeams === HumansVsNations
? Difficulty.Impossible
@@ -198,6 +201,7 @@ export class MapPlaylist {
return {
isRandomSpawn: Math.random() < 0.1, // 10% chance
isCompact: Math.random() < 0.05, // 5% chance
+ startingGold: Math.random() < 0.03 ? 5_000_000 : undefined, // 3% chance
};
}