Lobby Gold Options (Starting Gold, Gold Multiplier) 💰 (#2915)

## Description:

We might want to add this to v29 to have a third possible public game
modifier from the beginning on 😄 Would be fun

- Add starting gold option (0 to 1_000_000_000 allowed, also applies to
nations)
- Add gold multiplier option (0.1 to 1000 allowed, also applies to
nations and bots)
- Add third public game modifier (3% chance of starting with 5M gold)
- Why 5M? It's enough gold to massively change the game start but not
enough to insta-hydro someone (launcher + hydro is 6M)

<img width="357" height="140" alt="image"
src="https://github.com/user-attachments/assets/72acc15c-e788-4e04-8590-ac72dd9657c7"
/>


## Please complete the following:

- [X] I have added screenshots for all UI updates
- [X] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [X] I have added relevant tests to the test directory
- [X] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

FloPinguin
This commit is contained in:
FloPinguin
2026-01-16 10:20:35 -08:00
committed by evanpelle
parent d0fda1d535
commit 4e8454f3cc
11 changed files with 488 additions and 14 deletions
+224
View File
@@ -52,6 +52,10 @@ export class SinglePlayerModal extends BaseModal {
@state() private showAchievements: boolean = false;
@state() private mapWins: Map<GameMapType, Set<Difficulty>> = 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")}
</div>
</div>
<!-- Gold Multiplier -->
<div
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"}"
@click=${(e: Event) => {
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);
}
}}
>
<div class="flex items-center justify-center w-full mt-1">
<div
class="w-5 h-5 rounded border flex items-center justify-center transition-colors ${this
.goldMultiplier
? "bg-blue-500 border-blue-500"
: "border-white/20 bg-white/5"}"
>
${this.goldMultiplier
? html`<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 text-white"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>`
: ""}
</div>
</div>
${this.goldMultiplier
? html`<input
type="number"
id="gold-multiplier-value"
min="0.1"
max="1000"
step="any"
value=${this.goldMultiplierValue ?? ""}
class="w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
aria-label=${translateText(
"single_modal.gold_multiplier",
)}
@change=${this.handleGoldMultiplierValueChanges}
@keydown=${this.handleGoldMultiplierValueKeyDown}
placeholder=${translateText(
"single_modal.gold_multiplier_placeholder",
)}
/>`
: html`<div
class="h-[2px] w-4 bg-white/10 rounded my-3"
></div>`}
<div
class="text-[10px] uppercase font-bold text-white/60 tracking-wider text-center w-full leading-tight break-words hyphens-auto"
>
${translateText("single_modal.gold_multiplier")}
</div>
</div>
<!-- Starting Gold -->
<div
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"}"
@click=${(e: Event) => {
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);
}
}}
>
<div class="flex items-center justify-center w-full mt-1">
<div
class="w-5 h-5 rounded border flex items-center justify-center transition-colors ${this
.startingGold
? "bg-blue-500 border-blue-500"
: "border-white/20 bg-white/5"}"
>
${this.startingGold
? html`<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 text-white"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
/>
</svg>`
: ""}
</div>
</div>
${this.startingGold
? html`<input
type="number"
id="starting-gold-value"
min="0"
max="1000000000"
step="100000"
.value=${String(this.startingGoldValue ?? "")}
class="w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
aria-label=${translateText(
"single_modal.starting_gold",
)}
@input=${this.handleStartingGoldValueChanges}
@keydown=${this.handleStartingGoldValueKeyDown}
placeholder=${translateText(
"single_modal.starting_gold_placeholder",
)}
/>`
: html`<div
class="h-[2px] w-4 bg-white/10 rounded my-3"
></div>`}
<div
class="text-[10px] uppercase font-bold text-white/60 tracking-wider text-center w-full leading-tight break-words hyphens-auto"
>
${translateText("single_modal.starting_gold")}
</div>
</div>
</div>
</div>
@@ -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
},