From 1c1bb47040445710e641d753236a6a8c13f6f553 Mon Sep 17 00:00:00 2001 From: Noah Schmalenberger Date: Thu, 4 Jun 2026 21:31:30 +0000 Subject: [PATCH 1/7] Hide the map of "Random" lobbies until the game starts Selecting "Random" still resolves a concrete map client-side (so nation count and map loading work as today), but the choice now rides a `randomMap` flag on GameConfig. While the flag is set, all pre-start surfaces hide the concrete map so an invite link doesn't spoil it: - Invite embed (GamePreviewBuilder): generic RandomMap.webp image and a "Random Map" title instead of the real thumbnail/name. - Join screen + lobby player view: map shows "Random", and the nation count (which is per-map and would reveal the map) shows "Map default". - Host's nation slider: FluentSlider gains `hideDefaultValue` so the default position reads "Map default" without the revealing number. The server clears the flag at prestart, which reveals the (already resolved) map everywhere with no downstream special-casing. Applies to random lobbies only; concrete-map lobbies are unchanged. Tests cover the schema round-trip, prestart clearing the flag, updateGameConfig persisting it, and the embed hiding/showing the map. --- resources/lang/en.json | 1 + src/client/HostLobbyModal.ts | 5 ++ src/client/JoinLobbyModal.ts | 13 +++-- src/client/components/FluentSlider.ts | 16 ++++-- src/client/components/GameConfigSettings.ts | 3 ++ src/client/components/LobbyPlayerView.ts | 13 +++-- src/core/Schemas.ts | 5 ++ src/server/GamePreviewBuilder.ts | 19 +++++-- src/server/GameServer.ts | 9 ++++ tests/RandomMapConfig.test.ts | 20 +++++++ tests/server/GameLifecycle.test.ts | 26 +++++++++ tests/server/GamePreviewRandomMap.test.ts | 58 +++++++++++++++++++++ 12 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 tests/RandomMapConfig.test.ts create mode 100644 tests/server/GamePreviewRandomMap.test.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 0c46493a3c..c47613d8ae 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -519,6 +519,7 @@ }, "map": { "map": "Map", + "random": "Random", "featured": "Featured", "all": "All", "world": "World", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 322cc68add..512745a019 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -323,6 +323,9 @@ export class HostLobbyModal extends BaseModal { defaultValue: this.defaultNationCount, labelKey: "host_modal.nations", disabledKey: "host_modal.nations_disabled", + // Random lobby: show "Map default" without the count, which + // would otherwise reveal the (hidden) map. + hideDefaultValue: this.useRandomMap, }, toggles: [ { @@ -409,6 +412,7 @@ export class HostLobbyModal extends BaseModal { .currentClientID=${this.lobbyCreatorClientID} .teamCount=${this.teamCount} .nationCount=${this.nations} + .randomMap=${this.useRandomMap} .onKickPlayer=${(clientID: string) => this.kickPlayer(clientID)} > @@ -945,6 +949,7 @@ export class HostLobbyModal extends BaseModal { detail: { config: { gameMap: this.selectedMap, + randomMap: this.useRandomMap, gameMapSize: this.compactMap ? GameMapSize.Compact : GameMapSize.Normal, diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts index 115bff0fa4..d8dbb38ab2 100644 --- a/src/client/JoinLobbyModal.ts +++ b/src/client/JoinLobbyModal.ts @@ -156,6 +156,7 @@ export class JoinLobbyModal extends BaseModal { this.gameConfig?.nations ?? "default", this.nationCount, )} + .randomMap=${this.gameConfig?.randomMap === true} > ` : ""} @@ -410,11 +411,15 @@ export class JoinLobbyModal extends BaseModal { if (!this.gameConfig) return html``; const c = this.gameConfig; - const mapName = getMapName(c.gameMap); + // A "Random" lobby keeps the concrete map hidden until the game starts. + const isRandomMap = c.randomMap === true; + const mapName = isRandomMap + ? translateText("map.random") + : getMapName(c.gameMap); const normalizedMap = normaliseMapKey(c.gameMap); - const thumbnailUrl = assetUrl( - `maps/${encodeURIComponent(normalizedMap)}/thumbnail.webp`, - ); + const thumbnailUrl = isRandomMap + ? assetUrl("images/RandomMap.webp") + : assetUrl(`maps/${encodeURIComponent(normalizedMap)}/thumbnail.webp`); const isTeam = c.gameMode === GameMode.Team; let modeSubtitle: string; diff --git a/src/client/components/FluentSlider.ts b/src/client/components/FluentSlider.ts index 1a026135ed..1323f5c219 100644 --- a/src/client/components/FluentSlider.ts +++ b/src/client/components/FluentSlider.ts @@ -16,6 +16,12 @@ export class FluentSlider extends LitElement { @property({ type: String }) disabledKey = ""; @property({ type: Number }) defaultValue: number | undefined = undefined; @property({ type: String }) defaultLabelKey = ""; + /** + * When true, render only the default label (not the numeric value) while the + * slider sits at its default position. Used to hide the map's nation count + * for "Random" lobbies, where the number would reveal the map. + */ + @property({ type: Boolean }) hideDefaultValue = false; @state() private isEditing = false; @@ -136,10 +142,14 @@ export class FluentSlider extends LitElement { : this.defaultValue !== undefined && this.value === this.defaultValue && this.defaultLabelKey - ? html`${this.value} - (${translateText(this.defaultLabelKey)})${translateText(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 39184d267d..4731e9a59a 100644 --- a/src/client/components/GameConfigSettings.ts +++ b/src/client/components/GameConfigSettings.ts @@ -198,6 +198,7 @@ export interface GameConfigSettingsData { labelKey: string; disabledKey: string; hidden?: boolean; + hideDefaultValue?: boolean; }; toggles: ToggleOptionConfig[]; inputCards: TemplateResult[]; @@ -468,6 +469,8 @@ export class GameConfigSettings extends LitElement { .value=${settings.options.nations.value} .defaultValue=${settings.options.nations.defaultValue} defaultLabelKey="common.map_default" + .hideDefaultValue=${settings.options.nations + .hideDefaultValue ?? false} labelKey=${settings.options.nations.labelKey} disabledKey=${settings.options.nations.disabledKey} @value-changed=${this.handleNationsChanged} diff --git a/src/client/components/LobbyPlayerView.ts b/src/client/components/LobbyPlayerView.ts index d4f4510e11..b58c1ee6f3 100644 --- a/src/client/components/LobbyPlayerView.ts +++ b/src/client/components/LobbyPlayerView.ts @@ -37,6 +37,8 @@ export class LobbyTeamView extends LitElement { @property({ type: Function }) onKickPlayer?: (clientID: string) => void; @property({ type: Number }) nationCount: number = 0; @property({ type: Boolean }) isPublicGame: boolean = false; + /** When true (a "Random" lobby), hide the nation count so it can't reveal the map. */ + @property({ type: Boolean }) randomMap: boolean = false; private get theme(): Theme { return themeProvider.current(); @@ -84,10 +86,13 @@ export class LobbyTeamView extends LitElement { ? translateText("host_modal.player") : translateText("host_modal.players")} - ${this.effectiveNationCount} - ${this.effectiveNationCount === 1 - ? translateText("host_modal.nation_player") - : translateText("host_modal.nation_players")} + ${this.randomMap + ? html`${translateText("common.map_default")} + ${translateText("host_modal.nation_players")}` + : html`${this.effectiveNationCount} + ${this.effectiveNationCount === 1 + ? translateText("host_modal.nation_player") + : translateText("host_modal.nation_players")}`}
; export const GameConfigSchema = z.object({ gameMap: z.enum(GameMapType), + // When true, the host chose "Random": the concrete gameMap above is still + // resolved client-side (so nation count etc. work), but it must be hidden + // from all pre-start visuals (embed, lobby map name, nation count). The + // server clears this flag at prestart, which reveals the map. + randomMap: z.boolean().optional(), difficulty: z.enum(Difficulty), donateGold: z.boolean(), // Configures donations to humans only donateTroops: z.boolean(), // Configures donations to humans only diff --git a/src/server/GamePreviewBuilder.ts b/src/server/GamePreviewBuilder.ts index 592fafc491..102904c803 100644 --- a/src/server/GamePreviewBuilder.ts +++ b/src/server/GamePreviewBuilder.ts @@ -162,6 +162,10 @@ export async function buildPreview( countActivePlayers(players) || (lobby?.clients?.length ?? 0); } const map = lobby?.gameConfig?.gameMap ?? config.gameMap; + // While a "Random" lobby hasn't started, hide the concrete map so the embed + // doesn't spoil it. The server clears randomMap at prestart, so an + // in-progress/finished game reveals its real map normally. + const isRandomMap = lobby?.gameConfig?.randomMap === true && !isFinished; let mode = lobby?.gameConfig?.gameMode ?? config.gameMode ?? GameMode.FFA; const playerTeams = lobby?.gameConfig?.playerTeams ?? config.playerTeams; const numericTeamCount = @@ -192,23 +196,28 @@ export async function buildPreview( const duration = publicInfo?.info?.duration; // Normalize map name to match filesystem (lowercase, no spaces or special chars) - const normalizedMap = map ? map.toLowerCase().replace(/[\s.()]+/g, "") : null; + const normalizedMap = + map && !isRandomMap ? map.toLowerCase().replace(/[\s.()]+/g, "") : null; const mapThumbnail = normalizedMap ? buildAbsoluteAssetUrl( `maps/${encodeURIComponent(normalizedMap)}/thumbnail.webp`, ) : null; - const image = - mapThumbnail ?? buildAbsoluteAssetUrl("images/GameplayScreenshot.png"); + const image = isRandomMap + ? buildAbsoluteAssetUrl("images/RandomMap.webp") + : (mapThumbnail ?? buildAbsoluteAssetUrl("images/GameplayScreenshot.png")); + + // The name shown in the title: "Random Map" until the lobby starts. + const displayMap = isRandomMap ? "Random Map" : map; const gameType = lobby?.gameConfig?.gameType ?? config.gameType; const gameTypeLabel = gameType ? ` (${gameType})` : ""; const title = isFinished ? `${mode ?? "Game"} on ${map ?? "Unknown Map"}${gameTypeLabel}` - : mode && map - ? `${mode} on ${map}${gameTypeLabel}` + : mode && displayMap + ? `${mode} on ${displayMap}${gameTypeLabel}` : "OpenFront Game"; let description: string; diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 21279f856d..71f8b6db65 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -122,6 +122,9 @@ export class GameServer { if (gameConfig.gameMap !== undefined) { this.gameConfig.gameMap = gameConfig.gameMap; } + if (gameConfig.randomMap !== undefined) { + this.gameConfig.randomMap = gameConfig.randomMap; + } if (gameConfig.gameMapSize !== undefined) { this.gameConfig.gameMapSize = gameConfig.gameMapSize; } @@ -654,6 +657,12 @@ export class GameServer { } this._hasPrestarted = true; + // The game is starting: reveal the map. Clearing the flag means every + // downstream consumer (embed, started-game info) sees a normal concrete + // map with no special-casing. The concrete gameMap was already resolved + // client-side at lobby creation, so nothing else needs to change here. + this.gameConfig.randomMap = false; + const prestartMsg = ServerPrestartMessageSchema.safeParse({ type: "prestart", gameMap: this.gameConfig.gameMap, diff --git a/tests/RandomMapConfig.test.ts b/tests/RandomMapConfig.test.ts new file mode 100644 index 0000000000..b156d4a1d1 --- /dev/null +++ b/tests/RandomMapConfig.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, test } from "vitest"; +import { + GameConfigSchema, + UpdateGameConfigIntentSchema, +} from "../src/core/Schemas"; + +describe("randomMap in GameConfig", () => { + test("is carried by the update_game_config intent", () => { + const parsed = UpdateGameConfigIntentSchema.parse({ + type: "update_game_config", + config: { randomMap: true }, + }); + expect(parsed.config.randomMap).toBe(true); + }); + + test("is optional", () => { + const parsed = GameConfigSchema.partial().parse({}); + expect(parsed.randomMap).toBeUndefined(); + }); +}); diff --git a/tests/server/GameLifecycle.test.ts b/tests/server/GameLifecycle.test.ts index 19661a7648..0c4f714310 100644 --- a/tests/server/GameLifecycle.test.ts +++ b/tests/server/GameLifecycle.test.ts @@ -85,4 +85,30 @@ describe("GameLifecycle", () => { await expect(game.end()).resolves.toBeUndefined(); expect((game as any)._hasEnded).toBe(true); }); + + it("clears the randomMap flag at prestart (reveals the map at start)", () => { + const game = new GameServer("test-game", mockLogger, Date.now(), { + gameType: GameType.Private, + gameMap: "plains", + gameMapSize: 100, + randomMap: true, + } as any); + + expect((game as any).gameConfig.randomMap).toBe(true); + game.prestart(); + // The concrete map is untouched; only the "hide it" flag is cleared. + expect((game as any).gameConfig.randomMap).toBe(false); + expect((game as any).gameConfig.gameMap).toBe("plains"); + }); + + it("persists the randomMap flag through updateGameConfig", () => { + const game = new GameServer("test-game", mockLogger, Date.now(), { + gameType: GameType.Private, + gameMap: "plains", + randomMap: false, + } as any); + + game.updateGameConfig({ randomMap: true } as any); + expect((game as any).gameConfig.randomMap).toBe(true); + }); }); diff --git a/tests/server/GamePreviewRandomMap.test.ts b/tests/server/GamePreviewRandomMap.test.ts new file mode 100644 index 0000000000..2cce0a2ce0 --- /dev/null +++ b/tests/server/GamePreviewRandomMap.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test, vi } from "vitest"; + +// buildPreview reads the runtime asset manifest and CDN base; stub both so the +// pure title/image logic can be tested without filesystem/env. +vi.mock("../../src/server/RuntimeAssetManifest", () => ({ + getRuntimeAssetManifest: vi.fn().mockResolvedValue({}), +})); +vi.mock("../../src/server/ServerEnv", () => ({ + ServerEnv: { cdnBase: () => "" }, +})); + +import { buildPreview } from "../../src/server/GamePreviewBuilder"; + +const origin = "https://example.com"; + +function lobby(gameConfig: any) { + return { gameConfig, clients: [{ username: "host" }] } as any; +} + +describe("GamePreview — random map", () => { + test("hides the concrete map for an unstarted random lobby", async () => { + const meta = await buildPreview( + "game1", + origin, + "w0", + lobby({ + gameMap: "Europe", + randomMap: true, + gameType: "Private", + gameMode: "FFA", + }), + null, + ); + + expect(meta.image).toContain("RandomMap.webp"); + expect(meta.image.toLowerCase()).not.toContain("europe"); + expect(meta.title).toContain("Random Map"); + expect(meta.title).not.toContain("Europe"); + }); + + test("shows the concrete map for a normal lobby", async () => { + const meta = await buildPreview( + "game2", + origin, + "w0", + lobby({ + gameMap: "Europe", + randomMap: false, + gameType: "Private", + gameMode: "FFA", + }), + null, + ); + + expect(meta.image.toLowerCase()).toContain("europe"); + expect(meta.title).toContain("Europe"); + }); +}); From 38b5c98b843a35c329add7ef6d0a97da2f90ec59 Mon Sep 17 00:00:00 2001 From: Noah Schmalenberger Date: Thu, 4 Jun 2026 21:33:29 +0000 Subject: [PATCH 2/7] Hide nation count for single-player random maps too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-player uses the same game-config-settings nation slider, so pass hideDefaultValue when Random is selected — otherwise the default position shows the map's real nation count, revealing the randomly chosen map. No embed/lobby surfaces exist for single-player, so this is the only change. --- src/client/SinglePlayerModal.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 994996ddd6..5ca71dc1df 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -290,6 +290,9 @@ export class SinglePlayerModal extends BaseModal { defaultValue: this.defaultNationCount, labelKey: "single_modal.nations", disabledKey: "single_modal.nations_disabled", + // Random map: show "Map default" without the count, which + // would otherwise reveal the hidden map. + hideDefaultValue: this.useRandomMap, }, toggles: [ { From cd4d7e36ce9a052d3b56f2fa80434d090bc1e7b7 Mon Sep 17 00:00:00 2001 From: Noah Schmalenberger Date: Thu, 4 Jun 2026 21:39:22 +0000 Subject: [PATCH 3/7] Park nation slider thumb at center for hidden-default random maps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hiding the nation count number wasn't enough: the slider thumb still sat at value/max (the map's real nation count / 400), so the map could be inferred from the thumb's position. When a slider is at a hidden default (a Random lobby), render the thumb — and the fill bar and number editor — at the track center instead. Dragging or typing still reports the real input value, so the host can still override it. --- src/client/components/FluentSlider.ts | 17 +++++-- tests/client/components/FluentSlider.test.ts | 53 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/client/components/FluentSlider.ts b/src/client/components/FluentSlider.ts index 1323f5c219..f13f58e832 100644 --- a/src/client/components/FluentSlider.ts +++ b/src/client/components/FluentSlider.ts @@ -78,10 +78,21 @@ export class FluentSlider extends LitElement { } render() { + // For a hidden default (a "Random" lobby), the real value would reveal the + // map's nation count via the thumb's *position*, not just the number. While + // untouched at the default, park the thumb at the track's center so it + // gives nothing away. Dragging/typing still reports the real input value. + const atHiddenDefault = + this.hideDefaultValue && + this.defaultValue !== undefined && + this.value === this.defaultValue; + const displayValue = atHiddenDefault + ? Math.round((this.min + this.max) / 2) + : this.value; const percentage = this.max === this.min ? 0 - : ((this.value - this.min) / (this.max - this.min)) * 100; + : ((displayValue - this.min) / (this.max - this.min)) * 100; return html`
{ diff --git a/tests/client/components/FluentSlider.test.ts b/tests/client/components/FluentSlider.test.ts index 8b633df09e..3f3518b566 100644 --- a/tests/client/components/FluentSlider.test.ts +++ b/tests/client/components/FluentSlider.test.ts @@ -278,6 +278,59 @@ describe("FluentSlider", () => { }); }); + describe("Hidden default (Random lobby)", () => { + it("parks the thumb at the track center when at a hidden default", async () => { + slider.min = 0; + slider.max = 400; + slider.defaultValue = 247; + slider.value = 247; + slider.hideDefaultValue = true; + await slider.updateComplete; + + const rangeInput = slider.querySelector( + 'input[type="range"]', + ) as HTMLInputElement; + + // Thumb shows the center (200), NOT the real count (247) that would + // otherwise reveal the hidden map. + expect(rangeInput.valueAsNumber).toBe(200); + // The underlying value is untouched. + expect(slider.value).toBe(247); + }); + + it("tracks the real position once moved off the default", async () => { + slider.min = 0; + slider.max = 400; + slider.defaultValue = 247; + slider.value = 300; + slider.hideDefaultValue = true; + await slider.updateComplete; + + const rangeInput = slider.querySelector( + 'input[type="range"]', + ) as HTMLInputElement; + expect(rangeInput.valueAsNumber).toBe(300); + }); + + it("still reports the dragged value from the parked position", async () => { + slider.min = 0; + slider.max = 400; + slider.defaultValue = 247; + slider.value = 247; + slider.hideDefaultValue = true; + await slider.updateComplete; + + const rangeInput = slider.querySelector( + 'input[type="range"]', + ) as HTMLInputElement; + rangeInput.valueAsNumber = 180; + rangeInput.dispatchEvent(new Event("change", { bubbles: true })); + await slider.updateComplete; + + expect(slider.value).toBe(180); + }); + }); + describe("Edge Cases", () => { it("should handle min equal to max without NaN in style", async () => { slider.min = 100; From 50f8fbd623f6ee819fc9031881591563cd64cac2 Mon Sep 17 00:00:00 2001 From: Noah Schmalenberger Date: Thu, 4 Jun 2026 21:42:31 +0000 Subject: [PATCH 4/7] Add reset-to-default control for hidden-default random sliders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once you move a Random lobby's nation slider off the default, the real default count is hidden, so there's no way to drag back to it. Add a small "↺ Map default" reset button that appears only in that state (hideDefaultValue + moved off default); clicking it restores the default value, which re-parks the thumb at center and hides the button again. --- src/client/components/FluentSlider.ts | 19 +++++++++ tests/client/components/FluentSlider.test.ts | 44 ++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/client/components/FluentSlider.ts b/src/client/components/FluentSlider.ts index f13f58e832..522b146896 100644 --- a/src/client/components/FluentSlider.ts +++ b/src/client/components/FluentSlider.ts @@ -37,6 +37,13 @@ export class FluentSlider extends LitElement { ); } + private resetToDefault() { + if (this.defaultValue === undefined) return; + this.value = this.defaultValue; + this.isEditing = false; + this.dispatchValueChange(); + } + private handleSliderInput(e: Event) { const target = e.target as HTMLInputElement; this.value = target.valueAsNumber; @@ -163,6 +170,18 @@ export class FluentSlider extends LitElement { >` : this.value} `} + ${this.hideDefaultValue && + this.defaultValue !== undefined && + this.value !== this.defaultValue && + this.defaultLabelKey + ? html`` + : ""}
`; diff --git a/tests/client/components/FluentSlider.test.ts b/tests/client/components/FluentSlider.test.ts index 3f3518b566..4b5f965772 100644 --- a/tests/client/components/FluentSlider.test.ts +++ b/tests/client/components/FluentSlider.test.ts @@ -329,6 +329,50 @@ describe("FluentSlider", () => { expect(slider.value).toBe(180); }); + + it("offers a reset button only once moved off the hidden default", async () => { + slider.min = 0; + slider.max = 400; + slider.defaultValue = 247; + slider.hideDefaultValue = true; + slider.defaultLabelKey = "common.map_default"; + + // At default: no reset button. + slider.value = 247; + await slider.updateComplete; + expect(slider.querySelector("button")).toBeNull(); + + // Moved off default: reset button appears. + slider.value = 180; + await slider.updateComplete; + expect(slider.querySelector("button")).toBeTruthy(); + }); + + it("resets to the default value (and re-parks) when reset is clicked", async () => { + const eventSpy = vi.fn(); + slider.addEventListener("value-changed", eventSpy); + + slider.min = 0; + slider.max = 400; + slider.defaultValue = 247; + slider.value = 180; + slider.hideDefaultValue = true; + slider.defaultLabelKey = "common.map_default"; + await slider.updateComplete; + + const resetButton = slider.querySelector("button") as HTMLButtonElement; + expect(resetButton).toBeTruthy(); + resetButton.click(); + await slider.updateComplete; + + // Back to the (hidden) default, re-parked at center, change dispatched. + expect(slider.value).toBe(247); + expect(eventSpy).toHaveBeenCalledTimes(1); + const rangeInput = slider.querySelector( + 'input[type="range"]', + ) as HTMLInputElement; + expect(rangeInput.valueAsNumber).toBe(200); + }); }); describe("Edge Cases", () => { From b5d848fc905923e43a4592d68f5c83a572e98419 Mon Sep 17 00:00:00 2001 From: Noah Schmalenberger Date: Thu, 4 Jun 2026 22:25:36 +0000 Subject: [PATCH 5/7] Address review: dup i18n key, team-preview leak, docstrings, test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the duplicate "map.random" en.json key (one already existed); reuse the existing entry for translateText("map.random"). - Close the nation-count leak in team mode: the masked header wasn't enough — team-card totals and team sizing also derive from the count. Make effectiveNationCount return 0 for random lobbies so nothing nation-derived reveals the map, and recompute the team preview when the randomMap flag flips. - Add a buildPreview test for randomMap omitted entirely (the common normal-lobby case, since the field is optional). - Add docstrings to the functions touched by this change to satisfy the docstring-coverage threshold. --- resources/lang/en.json | 1 - src/client/HostLobbyModal.ts | 2 ++ src/client/JoinLobbyModal.ts | 2 ++ src/client/SinglePlayerModal.ts | 1 + src/client/components/FluentSlider.ts | 2 ++ src/client/components/GameConfigSettings.ts | 1 + src/client/components/LobbyPlayerView.ts | 11 ++++++++++- src/server/GamePreviewBuilder.ts | 5 +++++ src/server/GameServer.ts | 2 ++ tests/server/GamePreviewRandomMap.test.ts | 15 +++++++++++++++ 10 files changed, 40 insertions(+), 2 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index c47613d8ae..0c46493a3c 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -519,7 +519,6 @@ }, "map": { "map": "Map", - "random": "Random", "featured": "Featured", "all": "All", "world": "World", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 512745a019..01dbdc6858 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -179,6 +179,7 @@ export class HostLobbyModal extends BaseModal { }); } + /** Render the host lobby body: game config settings and the player list. */ protected renderBody() { const inputCards = [ html`