From 42b909cada7f1bdae884e1d8ac00dda8d80b0694 Mon Sep 17 00:00:00 2001 From: DeLoWaN Date: Tue, 16 Dec 2025 15:59:28 +0100 Subject: [PATCH 1/4] feat: show next map and mode info in public lobby - Add next map/mode fields to GameConfig schema - Update MapPlaylist to pre-calculate next map details - Show 'Up Next' section in PublicLobby UI --- resources/lang/en.json | 3 ++- src/client/PublicLobby.ts | 57 ++++++++++++++++++++++++++++++++++++++- src/core/Schemas.ts | 4 +++ src/server/MapPlaylist.ts | 37 ++++++++++++++++++++++--- 4 files changed, 95 insertions(+), 6 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index ad8f421aa0..c239e6ac11 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -273,7 +273,8 @@ "teams_Quads": "of 4 (Quads)", "teams_hvn": "Humans Vs Nations", "teams": "{num} teams", - "players_per_team": "of {num}" + "players_per_team": "of {num}", + "following_game": "Following Game" }, "matchmaking_modal": { "title": "Matchmaking", diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 2e79119cb7..51ce4d0f4c 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -149,12 +149,42 @@ export class PublicLobby extends LitElement { : modeLabel; const mapImageSrc = this.mapImages.get(lobby.gameID); + const nextMapName = lobby.gameConfig.nextMap + ? translateText( + `map.${lobby.gameConfig.nextMap.toLowerCase().replace(/[\s.]+/g, "")}`, + ) + : ""; + + // Calculate details for Next Game + const nextGameMode = lobby.gameConfig.nextGameMode; + const nextTeamCount = + nextGameMode === GameMode.Team + ? (lobby.gameConfig.nextPlayerTeams ?? 0) + : null; + const nextMaxPlayers = lobby.gameConfig.nextMaxPlayers ?? 0; + const nextTeamSize = this.getTeamSize(nextTeamCount, nextMaxPlayers); + const nextTeamTotal = this.getTeamTotal( + nextTeamCount, + nextTeamSize, + nextMaxPlayers, + ); + const nextModeLabel = nextGameMode + ? this.getModeLabel(nextGameMode, nextTeamCount, nextTeamTotal) + : ""; + const nextTeamDetailLabel = nextGameMode + ? this.getTeamDetailLabel( + nextGameMode, + nextTeamCount, + nextTeamTotal, + nextTeamSize, + ) + : null; return html` `; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 9370d6a477..76c3aa67fa 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -174,6 +174,10 @@ export const GameConfigSchema = z.object({ maxTimerValue: z.number().int().min(1).max(120).optional(), disabledUnits: z.enum(UnitType).array().optional(), playerTeams: TeamCountConfigSchema.optional(), + nextMap: z.enum(GameMapType).optional(), + nextGameMode: z.enum(GameMode).optional(), + nextPlayerTeams: TeamCountConfigSchema.optional(), + nextMaxPlayers: z.number().optional(), }); export const TeamSchema = z.string(); diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts index dacc005b6f..1fe4b02451 100644 --- a/src/server/MapPlaylist.ts +++ b/src/server/MapPlaylist.ts @@ -60,6 +60,7 @@ const frequency: Partial> = { interface MapWithMode { map: GameMapType; mode: GameMode; + playerTeams?: TeamCountConfig; } const TEAM_COUNTS = [ @@ -80,10 +81,24 @@ export class MapPlaylist { constructor(private disableTeams: boolean = false) {} public gameConfig(): GameConfig { - const { map, mode } = this.getNextMap(); + const nextItem = this.getNextMap(); + const { map, mode } = nextItem; + + this.ensurePlaylistPopulated(); + const upNextItem = this.mapsPlaylist[0]; + let nextPlayerTeams: TeamCountConfig | undefined; + + if (upNextItem) { + if (upNextItem.mode === GameMode.Team && !upNextItem.playerTeams) { + upNextItem.playerTeams = this.getTeamCount(); + } + nextPlayerTeams = upNextItem.playerTeams; + } const playerTeams = - mode === GameMode.Team ? this.getTeamCount() : undefined; + mode === GameMode.Team + ? (nextItem.playerTeams ?? this.getTeamCount()) + : undefined; // Create the default public game config (from your GameManager) return { @@ -104,6 +119,16 @@ export class MapPlaylist { playerTeams, bots: 400, disabledUnits: [], + nextMap: upNextItem?.map, + nextGameMode: upNextItem?.mode, + nextPlayerTeams, + nextMaxPlayers: upNextItem + ? config.lobbyMaxPlayers( + upNextItem.map, + upNextItem.mode, + nextPlayerTeams, + ) + : undefined, } satisfies GameConfig; } @@ -111,17 +136,21 @@ export class MapPlaylist { return TEAM_COUNTS[Math.floor(Math.random() * TEAM_COUNTS.length)]; } - private getNextMap(): MapWithMode { + private ensurePlaylistPopulated() { if (this.mapsPlaylist.length === 0) { const numAttempts = 10000; for (let i = 0; i < numAttempts; i++) { if (this.shuffleMapsPlaylist()) { log.info(`Generated map playlist in ${i} attempts`); - return this.mapsPlaylist.shift()!; + return; } } log.error("Failed to generate a valid map playlist"); } + } + + private getNextMap(): MapWithMode { + this.ensurePlaylistPopulated(); // Even if it failed, playlist will be partially populated. return this.mapsPlaylist.shift()!; } From 43bfc2c2e28feadf62e6bbb28ba2f20b6f3885a7 Mon Sep 17 00:00:00 2001 From: DeLoWaN Date: Thu, 18 Dec 2025 12:25:40 +0100 Subject: [PATCH 2/4] Refactor game details --- src/client/PublicLobby.ts | 114 +++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 58 deletions(-) diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts index 51ce4d0f4c..cf1d1f96ce 100644 --- a/src/client/PublicLobby.ts +++ b/src/client/PublicLobby.ts @@ -9,7 +9,7 @@ import { Quads, Trios, } from "../core/game/Game"; -import { GameID, GameInfo } from "../core/Schemas"; +import { GameID, GameInfo, TeamCountConfig } from "../core/Schemas"; import { generateID } from "../core/Util"; import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; @@ -124,62 +124,39 @@ export class PublicLobby extends LitElement { // Format time to show minutes and seconds const timeDisplay = renderDuration(timeRemaining); - const teamCount = - lobby.gameConfig.gameMode === GameMode.Team - ? (lobby.gameConfig.playerTeams ?? 0) - : null; - - const maxPlayers = lobby.gameConfig.maxPlayers ?? 0; - const teamSize = this.getTeamSize(teamCount, maxPlayers); - const teamTotal = this.getTeamTotal(teamCount, teamSize, maxPlayers); - const modeLabel = this.getModeLabel( - lobby.gameConfig.gameMode, - teamCount, - teamTotal, - ); - const teamDetailLabel = this.getTeamDetailLabel( + const { + modeLabel, + teamDetailLabel, + mapName: currentMapName, + } = this.getGameDisplayDetails( + lobby.gameConfig.gameMap, lobby.gameConfig.gameMode, - teamCount, - teamTotal, - teamSize, + lobby.gameConfig.playerTeams, + lobby.gameConfig.maxPlayers, ); + + const fullModeLabel = teamDetailLabel ? `${modeLabel} ${teamDetailLabel}` : modeLabel; - const mapImageSrc = this.mapImages.get(lobby.gameID); - const nextMapName = lobby.gameConfig.nextMap - ? translateText( - `map.${lobby.gameConfig.nextMap.toLowerCase().replace(/[\s.]+/g, "")}`, - ) - : ""; // Calculate details for Next Game - const nextGameMode = lobby.gameConfig.nextGameMode; - const nextTeamCount = - nextGameMode === GameMode.Team - ? (lobby.gameConfig.nextPlayerTeams ?? 0) - : null; - const nextMaxPlayers = lobby.gameConfig.nextMaxPlayers ?? 0; - const nextTeamSize = this.getTeamSize(nextTeamCount, nextMaxPlayers); - const nextTeamTotal = this.getTeamTotal( - nextTeamCount, - nextTeamSize, - nextMaxPlayers, + const { + modeLabel: nextModeLabel, + teamDetailLabel: nextTeamDetailLabel, + mapName: nextMapName, + } = this.getGameDisplayDetails( + lobby.gameConfig.nextMap, + lobby.gameConfig.nextGameMode, + lobby.gameConfig.nextPlayerTeams, + lobby.gameConfig.nextMaxPlayers, ); - const nextModeLabel = nextGameMode - ? this.getModeLabel(nextGameMode, nextTeamCount, nextTeamTotal) - : ""; - const nextTeamDetailLabel = nextGameMode - ? this.getTeamDetailLabel( - nextGameMode, - nextTeamCount, - nextTeamTotal, - nextTeamSize, - ) - : null; + const fullNextModeLabel = nextTeamDetailLabel + ? `${nextModeLabel} ${nextTeamDetailLabel}` + : nextModeLabel; return html`