diff --git a/resources/lang/debug.json b/resources/lang/debug.json index 924720ce31..ea4e1a5a32 100644 --- a/resources/lang/debug.json +++ b/resources/lang/debug.json @@ -96,6 +96,8 @@ "infinite_gold": "single_modal.infinite_gold", "random_spawn": "single_modal.random_spawn", "infinite_troops": "single_modal.infinite_troops", + "starting_gold": "single_modal.starting_gold", + "gold_multiplier": "single_modal.gold_multiplier", "disable_nukes": "single_modal.disable_nukes", "start": "single_modal.start" }, @@ -145,6 +147,9 @@ "options_title": "host_modal.options_title", "bots": "host_modal.bots", "bots_disabled": "host_modal.bots_disabled", + "spawn_immunity_duration": "host_modal.spawn_immunity_duration", + "starting_gold": "host_modal.starting_gold", + "gold_multiplier": "host_modal.gold_multiplier", "disable_nations": "host_modal.disable_nations", "instant_build": "host_modal.instant_build", "random_spawn": "host_modal.random_spawn", diff --git a/resources/lang/en.json b/resources/lang/en.json index 77e6b24b53..09a7a8b7a5 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -145,6 +145,8 @@ "instant_build": "Instant build", "infinite_gold": "Infinite gold", "infinite_troops": "Infinite troops", + "starting_gold": "Starting gold", + "gold_multiplier": "Gold multiplier", "compact_map": "Compact Map", "max_timer": "Game length (minutes)", "disable_nukes": "Disable Nukes", @@ -261,6 +263,9 @@ "options_title": "Options", "bots": "Bots: ", "bots_disabled": "Disabled", + "spawn_immunity_duration": "Spawn PVP Immunity", + "starting_gold": "Starting gold", + "gold_multiplier": "Gold multiplier", "nations": "Nations: ", "disable_nations": "Disable Nations", "max_timer": "Game length (minutes)", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index ccb01843c8..a5603accaa 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -1,7 +1,7 @@ import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import randomMap from "../../resources/images/RandomMap.webp"; -import { translateText } from "../client/Utils"; +import { renderNumber, translateText } from "../client/Utils"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { Difficulty, @@ -23,11 +23,17 @@ import { TeamCountConfig, } from "../core/Schemas"; import { generateID } from "../core/Util"; +import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import "./components/Difficulties"; import "./components/Maps"; import { JoinLobbyEvent } from "./Main"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; +import { + STARTING_GOLD_PRESETS, + startingGoldIndexFromValue, + startingGoldValueFromIndex, +} from "./utilities/StartingGoldPresets"; @customElement("host-lobby-modal") export class HostLobbyModal extends LitElement { @@ -41,6 +47,7 @@ export class HostLobbyModal extends LitElement { @state() private gameMode: GameMode = GameMode.FFA; @state() private teamCount: TeamCountConfig = 2; @state() private bots: number = 400; + @state() private spawnImmunityDurationSeconds: number = 5; @state() private infiniteGold: boolean = false; @state() private donateGold: boolean = false; @state() private infiniteTroops: boolean = false; @@ -49,6 +56,10 @@ export class HostLobbyModal extends LitElement { @state() private maxTimerValue: number | undefined = undefined; @state() private instantBuild: boolean = false; @state() private randomSpawn: boolean = false; + @state() private startingGold: number = 0; + @state() private goldMultiplier: number = 1; + @state() private startingGoldEnabled = false; + @state() private goldMultiplierEnabled = false; @state() private compactMap: boolean = false; @state() private lobbyId = ""; @state() private copySuccess = false; @@ -335,9 +346,11 @@ export class HostLobbyModal extends LitElement { min="0" max="400" step="1" + class="option-slider" + style=${this.sliderStyle(this.bots, 0, 400)} @input=${this.handleBotsChange} @change=${this.handleBotsChange} - .value="${String(this.bots)}" + .value=${String(this.bots)} />
${translateText("host_modal.bots")}${ @@ -522,6 +535,108 @@ export class HostLobbyModal extends LitElement { ${translateText("host_modal.max_timer")}
+ + + + +
@@ -610,6 +725,26 @@ export class HostLobbyModal extends LitElement { createLobby(this.lobbyCreatorClientID) .then((lobby) => { this.lobbyId = lobby.gameID; + if (lobby.gameConfig) { + const startingGoldFromServer = + lobby.gameConfig.startingGold ?? STARTING_GOLD_PRESETS[0]; + this.startingGold = this.snapStartingGoldValue( + startingGoldFromServer, + ); + this.startingGoldEnabled = this.startingGold > 0; + + const goldMultiplierFromServer = lobby.gameConfig.goldMultiplier ?? 1; + this.goldMultiplier = this.normalizeGoldMultiplier( + goldMultiplierFromServer, + ); + this.goldMultiplierEnabled = this.goldMultiplier !== 1; + + if (typeof lobby.gameConfig.spawnImmunityDuration === "number") { + this.spawnImmunityDurationSeconds = Math.floor( + lobby.gameConfig.spawnImmunityDuration / 10, + ); + } + } // join lobby }) .then(() => { @@ -660,7 +795,9 @@ export class HostLobbyModal extends LitElement { // Modified to include debouncing private handleBotsChange(e: Event) { - const value = parseInt((e.target as HTMLInputElement).value); + const slider = e.target as HTMLInputElement; + this.updateSliderProgressElement(slider); + const value = parseInt(slider.value, 10); if (isNaN(value) || value < 0 || value > 400) { return; } @@ -685,11 +822,78 @@ export class HostLobbyModal extends LitElement { this.putGameConfig(); } + private handleSpawnImmunityDurationSlider(e: Event) { + const slider = e.target as HTMLInputElement; + const value = parseInt(slider.value, 10); + if (Number.isNaN(value)) { + return; + } + const clamped = Math.min(300, Math.max(0, value)); + this.spawnImmunityDurationSeconds = Math.round(clamped / 5) * 5; + slider.value = String(this.spawnImmunityDurationSeconds); + this.updateSliderProgressElement(slider); + this.putGameConfig(); + } + + private formatSecondsAsClock(seconds: number): string { + const minutes = Math.floor(seconds / 60) + .toString() + .padStart(2, "0"); + const remainder = (seconds % 60).toString().padStart(2, "0"); + return `${minutes}:${remainder}`; + } + private handleRandomSpawnChange(e: Event) { this.randomSpawn = Boolean((e.target as HTMLInputElement).checked); this.putGameConfig(); } + private handleStartingGoldToggle(e: Event) { + const enabled = (e.target as HTMLInputElement).checked; + this.startingGoldEnabled = enabled; + if (!enabled) { + this.startingGold = STARTING_GOLD_PRESETS[0]; + } + this.putGameConfig(); + } + + private handleStartingGoldSliderChange(e: Event) { + const slider = e.target as HTMLInputElement; + this.updateSliderProgressElement(slider); + const index = parseInt(slider.value, 10); + if (Number.isNaN(index)) { + return; + } + this.startingGold = startingGoldValueFromIndex(index); + this.startingGoldEnabled = true; + this.putGameConfig(); + } + + private getStartingGoldSliderIndex(): number { + return startingGoldIndexFromValue(this.startingGold); + } + + private handleGoldMultiplierToggle(e: Event) { + const enabled = (e.target as HTMLInputElement).checked; + this.goldMultiplierEnabled = enabled; + if (!enabled) { + this.goldMultiplier = 1; + } + this.putGameConfig(); + } + + private handleGoldMultiplierSliderChange(e: Event) { + const slider = e.target as HTMLInputElement; + this.updateSliderProgressElement(slider); + const value = parseFloat(slider.value); + if (Number.isNaN(value)) { + return; + } + this.goldMultiplier = this.normalizeGoldMultiplier(value); + this.goldMultiplierEnabled = true; + this.putGameConfig(); + } + private handleInfiniteGoldChange(e: Event) { this.infiniteGold = Boolean((e.target as HTMLInputElement).checked); this.putGameConfig(); @@ -774,6 +978,9 @@ export class HostLobbyModal extends LitElement { randomSpawn: this.randomSpawn, gameMode: this.gameMode, disabledUnits: this.disabledUnits, + spawnImmunityDuration: this.spawnImmunityDurationSeconds * 10, + startingGold: this.startingGold, + goldMultiplier: this.goldMultiplier, playerTeams: this.teamCount, ...(this.gameMode === GameMode.Team && this.teamCount === HumansVsNations @@ -800,6 +1007,36 @@ export class HostLobbyModal extends LitElement { this.putGameConfig(); } + private snapStartingGoldValue(value: number): number { + return startingGoldValueFromIndex(startingGoldIndexFromValue(value)); + } + + private normalizeGoldMultiplier(value: number): number { + const clamped = Math.min(10, Math.max(0, value)); + return Math.round(clamped * 10) / 10; + } + + private sliderStyle(value: number, min: number, max: number): string { + if (max === min) return "--progress:0%"; + const percent = ((value - min) / (max - min)) * 100; + return `--progress:${Math.max(0, Math.min(100, percent))}%`; + } + + private updateSliderProgressElement(slider: HTMLInputElement): void { + const min = Number(slider.min); + const max = Number(slider.max); + const value = Number(slider.value); + if (Number.isNaN(min) || Number.isNaN(max) || max === min) { + slider.style.setProperty("--progress", "0%"); + return; + } + const percent = ((value - min) / (max - min)) * 100; + slider.style.setProperty( + "--progress", + `${Math.max(0, Math.min(100, percent))}%`, + ); + } + private getRandomMap(): GameMapType { const maps = Object.values(GameMapType); const randIdx = Math.floor(Math.random() * maps.length); @@ -857,6 +1094,33 @@ export class HostLobbyModal extends LitElement { console.log(`got game info response: ${JSON.stringify(data)}`); this.clients = data.clients ?? []; + if (data.gameConfig) { + if (typeof data.gameConfig.startingGold === "number") { + const snapped = this.snapStartingGoldValue( + data.gameConfig.startingGold, + ); + const startingGoldChanged = this.startingGold !== snapped; + this.startingGold = snapped; + if (startingGoldChanged) { + this.startingGoldEnabled = this.startingGold !== 0; + } + } + if (typeof data.gameConfig.goldMultiplier === "number") { + const normalized = this.normalizeGoldMultiplier( + data.gameConfig.goldMultiplier, + ); + const goldMultiplierChanged = this.goldMultiplier !== normalized; + this.goldMultiplier = normalized; + if (goldMultiplierChanged) { + this.goldMultiplierEnabled = this.goldMultiplier !== 1; + } + } + if (typeof data.gameConfig.spawnImmunityDuration === "number") { + this.spawnImmunityDurationSeconds = Math.floor( + data.gameConfig.spawnImmunityDuration / 10, + ); + } + } }); } diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 9c51bf9050..ffa873cdfb 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -1,7 +1,7 @@ import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import randomMap from "../../resources/images/RandomMap.webp"; -import { translateText } from "../client/Utils"; +import { renderNumber, translateText } from "../client/Utils"; import { Difficulty, Duos, @@ -27,6 +27,13 @@ import { FlagInput } from "./FlagInput"; import { JoinLobbyEvent } from "./Main"; import { UsernameInput } from "./UsernameInput"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; +import { + STARTING_GOLD_PRESETS, + startingGoldIndexFromValue, + startingGoldValueFromIndex, +} from "./utilities/StartingGoldPresets"; + +const DEFAULT_SPAWN_IMMUNITY_DURATION_SECONDS = 5; @customElement("single-player-modal") export class SinglePlayerModal extends LitElement { @@ -45,6 +52,10 @@ export class SinglePlayerModal extends LitElement { @state() private maxTimerValue: number | undefined = undefined; @state() private instantBuild: boolean = false; @state() private randomSpawn: boolean = false; + @state() private startingGold: number = 0; + @state() private goldMultiplier: number = 1; + @state() private startingGoldEnabled = false; + @state() private goldMultiplierEnabled = false; @state() private useRandomMap: boolean = false; @state() private gameMode: GameMode = GameMode.FFA; @state() private teamCount: TeamCountConfig = 2; @@ -243,9 +254,11 @@ export class SinglePlayerModal extends LitElement { min="0" max="400" step="1" + class="option-slider" + style=${this.sliderStyle(this.bots, 0, 400)} @input=${this.handleBotsChange} @change=${this.handleBotsChange} - .value="${String(this.bots)}" + .value=${String(this.bots)} />
${translateText("single_modal.bots")}${this @@ -309,6 +322,77 @@ export class SinglePlayerModal extends LitElement { ${translateText("single_modal.random_spawn")}
+ + +