diff --git a/resources/lang/en.json b/resources/lang/en.json index 8d41f9170..516d9645a 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -23,7 +23,8 @@ "none": "None", "copied": "Copied!", "click_to_copy": "Click to copy", - "enabled": "Enabled" + "enabled": "Enabled", + "map_default": "Map default" }, "main": { "title": "OpenFront (ALPHA)", @@ -186,7 +187,8 @@ "options_title": "Options", "bots": "Tribes: ", "bots_disabled": "Disabled", - "disable_nations": "Disable Nations", + "nations": "Nations: ", + "nations_disabled": "Disabled", "instant_build": "Instant build", "infinite_gold": "Infinite gold", "infinite_troops": "Infinite troops", @@ -401,8 +403,9 @@ "options_title": "Options", "bots": "Tribes: ", "bots_disabled": "Disabled", + "nations": "Nations: ", + "nations_disabled": "Disabled", "player_immunity_duration": "PVP immunity duration (minutes)", - "disable_nations": "Disable Nations", "max_timer": "Game length (minutes)", "mins_placeholder": "Mins", "instant_build": "Instant build", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 978fa12ac..1851b4e56 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -8,7 +8,6 @@ import { GameMapSize, GameMapType, GameMode, - HumansVsNations, UnitType, } from "../core/game/Game"; import { @@ -33,11 +32,13 @@ import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { getBotsForCompactMap, + getNationsForCompactMap, getRandomMapType, getUpdatedDisabledUnits, parseBoundedFloatFromInput, parseBoundedIntegerFromInput, preventDisallowedKeys, + sliderToNationsConfig, toOptionalNumber, } from "./utilities/GameConfigHelpers"; @@ -45,7 +46,8 @@ import { export class HostLobbyModal extends BaseModal { @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Easy; - @state() private disableNations = false; + @state() private nations: number = 0; + @state() private defaultNationCount: number = 0; @state() private gameMode: GameMode = GameMode.FFA; @state() private teamCount: TeamCountConfig = 2; @@ -75,11 +77,11 @@ export class HostLobbyModal extends BaseModal { @state() private useRandomMap: boolean = false; @state() private disabledUnits: UnitType[] = []; @state() private lobbyCreatorClientID: string = ""; - @state() private nationCount: number = 0; @property({ attribute: false }) eventBus: EventBus | null = null; - // Add a new timer for debouncing bot changes + // Timers for debouncing slider changes private botsUpdateTimer: number | null = null; + private nationsUpdateTimer: number | null = null; private mapLoader = terrainMapFileLoader; private leaveLobbyOnClose = true; @@ -242,7 +244,7 @@ export class HostLobbyModal extends BaseModal { }, difficulty: { selected: this.selectedDifficulty, - disabled: this.disableNations, + disabled: this.nations === 0, }, gameMode: { selected: this.gameMode, @@ -257,14 +259,13 @@ export class HostLobbyModal extends BaseModal { labelKey: "host_modal.bots", disabledKey: "host_modal.bots_disabled", }, + nations: { + value: this.nations, + defaultValue: this.defaultNationCount, + labelKey: "host_modal.nations", + disabledKey: "host_modal.nations_disabled", + }, toggles: [ - { - labelKey: "host_modal.disable_nations", - checked: this.disableNations, - hidden: - this.gameMode === GameMode.Team && - this.teamCount === HumansVsNations, - }, { labelKey: "host_modal.instant_build", checked: this.instantBuild, @@ -307,6 +308,7 @@ export class HostLobbyModal extends BaseModal { @game-mode-selected=${this.handleConfigGameModeSelected} @team-count-selected=${this.handleConfigTeamCountSelected} @bots-changed=${this.handleBotsChange} + @nations-changed=${this.handleNationsChange} @option-toggle-changed=${this.handleConfigOptionToggleChanged} @unit-toggle-changed=${this.handleConfigUnitToggleChanged} > @@ -318,9 +320,7 @@ export class HostLobbyModal extends BaseModal { .lobbyCreatorClientID=${this.lobbyCreatorClientID} .currentClientID=${this.lobbyCreatorClientID} .teamCount=${this.teamCount} - .nationCount=${this.nationCount} - .disableNations=${this.disableNations} - .isCompactMap=${this.compactMap} + .nationCount=${this.nations} .onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)} > @@ -420,11 +420,16 @@ export class HostLobbyModal extends BaseModal { clearTimeout(this.botsUpdateTimer); this.botsUpdateTimer = null; } + if (this.nationsUpdateTimer !== null) { + clearTimeout(this.nationsUpdateTimer); + this.nationsUpdateTimer = null; + } // Reset all transient form state to ensure clean slate this.selectedMap = GameMapType.World; this.selectedDifficulty = Difficulty.Easy; - this.disableNations = false; + this.nations = 0; + this.defaultNationCount = 0; this.gameMode = GameMode.FFA; this.teamCount = 2; this.bots = 400; @@ -444,7 +449,6 @@ export class HostLobbyModal extends BaseModal { this.lobbyId = ""; this.clients = []; this.lobbyCreatorClientID = ""; - this.nationCount = 0; this.goldMultiplier = false; this.goldMultiplierValue = undefined; this.startingGold = false; @@ -504,9 +508,6 @@ export class HostLobbyModal extends BaseModal { const { labelKey, checked } = customEvent.detail; switch (labelKey) { - case "host_modal.disable_nations": - void this.handleDisableNationsChange(checked); - break; case "host_modal.instant_build": this.handleInstantBuildChange(checked); break; @@ -677,6 +678,11 @@ export class HostLobbyModal extends BaseModal { private handleCompactMapChange = (val: boolean) => { this.compactMap = val; this.bots = getBotsForCompactMap(this.bots, val); + this.nations = getNationsForCompactMap( + this.nations, + this.defaultNationCount, + val, + ); this.putGameConfig(); }; @@ -704,10 +710,21 @@ export class HostLobbyModal extends BaseModal { this.putGameConfig(); }; - private handleDisableNationsChange = async (val: boolean) => { - this.disableNations = val; - console.log(`updating disable nations to ${this.disableNations}`); - this.putGameConfig(); + private handleNationsChange = (e: Event) => { + const customEvent = e as CustomEvent<{ value: number }>; + const value = customEvent.detail.value; + if (isNaN(value) || value < 0 || value > 400) { + return; + } + this.nations = value; + + if (this.nationsUpdateTimer !== null) { + clearTimeout(this.nationsUpdateTimer); + } + this.nationsUpdateTimer = window.setTimeout(() => { + this.putGameConfig(); + this.nationsUpdateTimer = null; + }, 300); }; private async handleGameModeSelection(value: GameMode) { @@ -755,14 +772,10 @@ export class HostLobbyModal extends BaseModal { ? spawnImmunityTicks : undefined, playerTeams: this.teamCount, - ...(this.gameMode === GameMode.Team && - this.teamCount === HumansVsNations - ? { - disableNations: false, - } - : { - disableNations: this.disableNations, - }), + nations: sliderToNationsConfig( + this.nations, + this.defaultNationCount, + ), maxTimerValue: this.maxTimer === true ? this.maxTimerValue : undefined, goldMultiplier: @@ -823,14 +836,14 @@ export class HostLobbyModal extends BaseModal { const manifest = await mapData.manifest(); // Only update if the map hasn't changed if (this.selectedMap === currentMap) { - this.nationCount = manifest.nations.length; + this.defaultNationCount = manifest.nations.length; + this.nations = this.compactMap + ? Math.max(0, Math.floor(manifest.nations.length * 0.25)) + : manifest.nations.length; } } catch (error) { console.warn("Failed to load nation count", error); - // Only update if the map hasn't changed - if (this.selectedMap === currentMap) { - this.nationCount = 0; - } + // Leave existing values unchanged so the UI stays consistent } } } diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 5ed20369a..892fe7537 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -19,12 +19,7 @@ import { PublicGameInfo, } from "../core/Schemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; -import { - GameMapSize, - GameMode, - GameType, - HumansVsNations, -} from "../core/game/Game"; +import { GameMode, GameType, HumansVsNations } from "../core/game/Game"; import { getApiBase } from "./Api"; import { crazyGamesSDK } from "./CrazyGamesSDK"; import { JoinLobbyEvent } from "./Main"; @@ -34,6 +29,7 @@ import "./components/CopyButton"; import "./components/LobbyConfigItem"; import "./components/LobbyPlayerView"; import { modalHeader } from "./components/ui/ModalHeader"; +import { nationsConfigToSlider } from "./utilities/GameConfigHelpers"; @customElement("join-lobby-modal") export class JoinLobbyModal extends BaseModal { @@ -134,11 +130,10 @@ export class JoinLobbyModal extends BaseModal { .lobbyCreatorClientID=${hostClientID} .currentClientID=${this.currentClientID} .teamCount=${this.gameConfig?.playerTeams ?? 2} - .nationCount=${this.nationCount} - .disableNations=${this.gameConfig?.disableNations ?? - false} - .isCompactMap=${this.gameConfig?.gameMapSize === - GameMapSize.Compact} + .nationCount=${nationsConfigToSlider( + this.gameConfig?.nations ?? "default", + this.nationCount, + )} > ` : ""} diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 0b36f13a0..ba7508107 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -8,7 +8,6 @@ import { GameMapType, GameMode, GameType, - HumansVsNations, UnitType, } from "../core/game/Game"; import { TeamCountConfig } from "../core/Schemas"; @@ -26,18 +25,21 @@ import { JoinLobbyEvent } from "./Main"; import { UsernameInput } from "./UsernameInput"; import { getBotsForCompactMap, + getNationsForCompactMap, getRandomMapType, getUpdatedDisabledUnits, parseBoundedFloatFromInput, parseBoundedIntegerFromInput, preventDisallowedKeys, + sliderToNationsConfig, toOptionalNumber, } from "./utilities/GameConfigHelpers"; +import { terrainMapFileLoader } from "./TerrainMapFileLoader"; + const DEFAULT_OPTIONS = { selectedMap: GameMapType.World, selectedDifficulty: Difficulty.Easy, - disableNations: false, bots: 400, infiniteGold: false, infiniteTroops: false, @@ -61,7 +63,8 @@ export class SinglePlayerModal extends BaseModal { @state() private selectedMap: GameMapType = DEFAULT_OPTIONS.selectedMap; @state() private selectedDifficulty: Difficulty = DEFAULT_OPTIONS.selectedDifficulty; - @state() private disableNations: boolean = DEFAULT_OPTIONS.disableNations; + @state() private nations: number = 0; + @state() private defaultNationCount: number = 0; @state() private bots: number = DEFAULT_OPTIONS.bots; @state() private infiniteGold: boolean = DEFAULT_OPTIONS.infiniteGold; @state() private infiniteTroops: boolean = DEFAULT_OPTIONS.infiniteTroops; @@ -88,12 +91,15 @@ export class SinglePlayerModal extends BaseModal { ...DEFAULT_OPTIONS.disabledUnits, ]; + private mapLoader = terrainMapFileLoader; + connectedCallback() { super.connectedCallback(); document.addEventListener( "userMeResponse", this.handleUserMeResponse as EventListener, ); + void this.loadNationCount(); } disconnectedCallback() { @@ -265,7 +271,7 @@ export class SinglePlayerModal extends BaseModal { }, difficulty: { selected: this.selectedDifficulty, - disabled: this.disableNations, + disabled: this.nations === 0, }, gameMode: { selected: this.gameMode, @@ -280,14 +286,13 @@ export class SinglePlayerModal extends BaseModal { labelKey: "single_modal.bots", disabledKey: "single_modal.bots_disabled", }, + nations: { + value: this.nations, + defaultValue: this.defaultNationCount, + labelKey: "single_modal.nations", + disabledKey: "single_modal.nations_disabled", + }, toggles: [ - { - labelKey: "single_modal.disable_nations", - checked: this.disableNations, - hidden: - this.gameMode === GameMode.Team && - this.teamCount === HumansVsNations, - }, { labelKey: "single_modal.instant_build", checked: this.instantBuild, @@ -322,6 +327,7 @@ export class SinglePlayerModal extends BaseModal { @game-mode-selected=${this.handleConfigGameModeSelected} @team-count-selected=${this.handleConfigTeamCountSelected} @bots-changed=${this.handleBotsChange} + @nations-changed=${this.handleNationsChange} @option-toggle-changed=${this.handleConfigOptionToggleChanged} @unit-toggle-changed=${this.handleConfigUnitToggleChanged} > @@ -366,7 +372,7 @@ export class SinglePlayerModal extends BaseModal { // Check if any options other than map and difficulty have been changed from defaults private hasOptionsChanged(): boolean { return ( - this.disableNations !== DEFAULT_OPTIONS.disableNations || + this.nations !== this.defaultNationCount || this.bots !== DEFAULT_OPTIONS.bots || this.infiniteGold !== DEFAULT_OPTIONS.infiniteGold || this.infiniteTroops !== DEFAULT_OPTIONS.infiniteTroops || @@ -387,8 +393,9 @@ export class SinglePlayerModal extends BaseModal { this.selectedDifficulty = DEFAULT_OPTIONS.selectedDifficulty; this.gameMode = DEFAULT_OPTIONS.gameMode; this.useRandomMap = DEFAULT_OPTIONS.useRandomMap; - this.disableNations = DEFAULT_OPTIONS.disableNations; this.bots = DEFAULT_OPTIONS.bots; + this.nations = 0; + this.defaultNationCount = 0; this.infiniteGold = DEFAULT_OPTIONS.infiniteGold; this.infiniteTroops = DEFAULT_OPTIONS.infiniteTroops; this.compactMap = DEFAULT_OPTIONS.compactMap; @@ -404,8 +411,14 @@ export class SinglePlayerModal extends BaseModal { this.startingGoldValue = DEFAULT_OPTIONS.startingGoldValue; } + protected onOpen(): void { + void this.loadNationCount(); + } + private handleSelectRandomMap() { this.useRandomMap = true; + this.selectedMap = getRandomMapType(); + void this.loadNationCount(); } private handleConfigRandomMapSelected = () => { @@ -415,6 +428,7 @@ export class SinglePlayerModal extends BaseModal { private handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; + void this.loadNationCount(); } private handleConfigMapSelected = (e: Event) => { @@ -444,6 +458,11 @@ export class SinglePlayerModal extends BaseModal { private handleCompactMapChange(val: boolean) { this.compactMap = val; this.bots = getBotsForCompactMap(this.bots, val); + this.nations = getNationsForCompactMap( + this.nations, + this.defaultNationCount, + val, + ); } private handleConfigOptionToggleChanged = (e: Event) => { @@ -454,9 +473,6 @@ export class SinglePlayerModal extends BaseModal { const { labelKey, checked } = customEvent.detail; switch (labelKey) { - case "single_modal.disable_nations": - this.disableNations = checked; - break; case "single_modal.instant_build": this.instantBuild = checked; break; @@ -496,6 +512,15 @@ export class SinglePlayerModal extends BaseModal { this.bots = value; }; + private handleNationsChange = (e: Event) => { + const customEvent = e as CustomEvent<{ value: number }>; + const value = customEvent.detail.value; + if (isNaN(value) || value < 0 || value > 400) { + return; + } + this.nations = value; + }; + private handleMaxTimerToggle = ( checked: boolean, value: number | string | undefined, @@ -604,11 +629,6 @@ export class SinglePlayerModal extends BaseModal { finalMaxTimerValue = Math.max(1, Math.min(120, this.maxTimerValue)); } - // If random map is selected, choose a random map now - if (this.useRandomMap) { - this.selectedMap = getRandomMapType(); - } - console.log( `Starting single player game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]}${this.useRandomMap ? " (Randomly selected)" : ""}`, ); @@ -657,14 +677,10 @@ export class SinglePlayerModal extends BaseModal { disabledUnits: this.disabledUnits .map((u) => Object.values(UnitType).find((ut) => ut === u)) .filter((ut): ut is UnitType => ut !== undefined), - ...(this.gameMode === GameMode.Team && - this.teamCount === HumansVsNations - ? { - disableNations: false, - } - : { - disableNations: this.disableNations, - }), + nations: sliderToNationsConfig( + this.nations, + this.defaultNationCount, + ), ...(this.goldMultiplier && this.goldMultiplierValue ? { goldMultiplier: this.goldMultiplierValue } : {}), @@ -682,4 +698,22 @@ export class SinglePlayerModal extends BaseModal { ); this.close(); } + + private async loadNationCount() { + const currentMap = this.selectedMap; + try { + const mapData = this.mapLoader.getMapData(currentMap); + const manifest = await mapData.manifest(); + // Only update if the map hasn't changed + if (this.selectedMap === currentMap) { + this.defaultNationCount = manifest.nations.length; + this.nations = this.compactMap + ? Math.max(0, Math.floor(manifest.nations.length * 0.25)) + : manifest.nations.length; + } + } catch (error) { + console.warn("Failed to load nation count", error); + // Leave existing values unchanged so the UI stays consistent + } + } } diff --git a/src/client/components/FluentSlider.ts b/src/client/components/FluentSlider.ts index ce2f1e5af..c45dc967f 100644 --- a/src/client/components/FluentSlider.ts +++ b/src/client/components/FluentSlider.ts @@ -14,6 +14,8 @@ export class FluentSlider extends LitElement { @property({ type: Number }) step = 1; @property({ type: String }) labelKey = ""; @property({ type: String }) disabledKey = ""; + @property({ type: Number }) defaultValue: number | undefined = undefined; + @property({ type: String }) defaultLabelKey = ""; @state() private isEditing = false; @@ -131,7 +133,14 @@ export class FluentSlider extends LitElement { > ${this.value === 0 && this.disabledKey ? translateText(this.disabledKey) - : this.value} + : this.defaultValue !== undefined && + this.value === this.defaultValue && + this.defaultLabelKey + ? html`${this.value} + (${translateText(this.defaultLabelKey)})` + : this.value} `} diff --git a/src/client/components/GameConfigSettings.ts b/src/client/components/GameConfigSettings.ts index 8300dd2b1..d0516b620 100644 --- a/src/client/components/GameConfigSettings.ts +++ b/src/client/components/GameConfigSettings.ts @@ -186,6 +186,13 @@ export interface GameConfigSettingsData { labelKey: string; disabledKey: string; }; + nations?: { + value: number; + defaultValue?: number; + labelKey: string; + disabledKey: string; + hidden?: boolean; + }; toggles: ToggleOptionConfig[]; inputCards: TemplateResult[]; }; @@ -246,6 +253,11 @@ export class GameConfigSettings extends LitElement { this.emit("bots-changed", customEvent.detail); }; + private handleNationsChanged = (event: Event) => { + const customEvent = event as CustomEvent<{ value: number }>; + this.emit("nations-changed", customEvent.detail); + }; + private handleUnitToggle = (unit: UnitType, checked: boolean) => { this.emit("unit-toggle-changed", { unit, checked }); }; @@ -423,6 +435,26 @@ export class GameConfigSettings extends LitElement { > + ${settings.options.nations && !settings.options.nations.hidden + ? html`
` + : nothing} ${settings.options.toggles.map((toggle) => this.renderOptionToggle(toggle), )} diff --git a/src/client/components/LobbyPlayerView.ts b/src/client/components/LobbyPlayerView.ts index d5b9c5259..9c0366f9e 100644 --- a/src/client/components/LobbyPlayerView.ts +++ b/src/client/components/LobbyPlayerView.ts @@ -13,7 +13,6 @@ import { Team, Trios, } from "../../core/game/Game"; -import { getCompactMapNationCount } from "../../core/game/NationCreation"; import { assignTeamsLobbyPreview } from "../../core/game/TeamAssignment"; import { UserSettings } from "../../core/game/UserSettings"; import { ClientInfo, TeamCountConfig } from "../../core/Schemas"; @@ -36,8 +35,6 @@ export class LobbyTeamView extends LitElement { @property({ attribute: "team-count" }) teamCount: TeamCountConfig = 2; @property({ type: Function }) onKickPlayer?: (clientID: string) => void; @property({ type: Number }) nationCount: number = 0; - @property({ type: Boolean }) disableNations: boolean = false; - @property({ type: Boolean }) isCompactMap: boolean = false; private theme: PastelTheme = new PastelTheme(); @state() private showTeamColors: boolean = false; @@ -50,9 +47,7 @@ export class LobbyTeamView extends LitElement { changedProperties.has("gameMode") || changedProperties.has("clients") || changedProperties.has("teamCount") || - changedProperties.has("nationCount") || - changedProperties.has("disableNations") || - changedProperties.has("isCompactMap") + changedProperties.has("nationCount") ) { const teamsList = this.getTeamList(); this.computeTeamPreview(teamsList); @@ -72,8 +67,8 @@ export class LobbyTeamView extends LitElement { ? translateText("host_modal.player") : translateText("host_modal.players")} • - ${this.getEffectiveNationCount()} - ${this.getEffectiveNationCount() === 1 + ${this.nationCount} + ${this.nationCount === 1 ? translateText("host_modal.nation_player") : translateText("host_modal.nation_players")} @@ -182,15 +177,14 @@ export class LobbyTeamView extends LitElement { } private renderTeamCard(preview: TeamPreviewData, isEmpty: boolean = false) { - const effectiveNationCount = this.getEffectiveNationCount(); const displayCount = preview.team === ColoredTeams.Nations - ? effectiveNationCount + ? this.nationCount : preview.players.length; const maxTeamSize = preview.team === ColoredTeams.Nations - ? effectiveNationCount + ? this.nationCount : this.teamMaxSize; const teamLabel = getTranslatedPlayerTeamLabel(preview.team); @@ -251,7 +245,7 @@ export class LobbyTeamView extends LitElement { private getTeamList(): Team[] { if (this.gameMode !== GameMode.Team) return []; - const playerCount = this.clients.length + this.getEffectiveNationCount(); + const playerCount = this.clients.length + this.nationCount; const config = this.teamCount; if (config === HumansVsNations) { @@ -315,7 +309,7 @@ export class LobbyTeamView extends LitElement { const assignment = assignTeamsLobbyPreview( players, teams, - this.getEffectiveNationCount(), + this.nationCount, ); const buckets = new Map