From 526cb723d6ad8b147e2891648fbc53bd181ec08b Mon Sep 17 00:00:00 2001
From: FloPinguin <25036848+FloPinguin@users.noreply.github.com>
Date: Sat, 7 Mar 2026 21:57:16 +0100
Subject: [PATCH] =?UTF-8?q?Fix=202=20HvN=20UI=20bugs=20=F0=9F=94=A7=20(#33?=
=?UTF-8?q?78)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Description:
I noticed two HvN bugs.
1. Private lobbies don't set `maxPlayers` in `GameConfig`, causing
`getGameModeLabel()` to render "0 Humans vs 0 Nations". Fall back to the
simple "Humans vs Nations" label when `maxPlayers` is unavailable.
2. In public HumansVsNations games, the server matches the nation count
to the human player count at game start. The lobby team size preview
wasn't reflecting this - it displayed the raw config value instead.
Added `isPublicGame` prop to `LobbyPlayerView` and an
`effectiveNationCount` getter that overrides the displayed nation count
to match `clients.length` only for public HvN games. Private lobby hosts
retain full slider control. (This bug got introduced with my
"Configurable nation count" PR)
## 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
---
src/client/JoinLobbyModal.ts | 2 ++
src/client/Utils.ts | 9 ++++---
src/client/components/LobbyPlayerView.ts | 32 ++++++++++++++++++------
3 files changed, 32 insertions(+), 11 deletions(-)
diff --git a/src/client/JoinLobbyModal.ts b/src/client/JoinLobbyModal.ts
index 7834045b7..b6771e5f0 100644
--- a/src/client/JoinLobbyModal.ts
+++ b/src/client/JoinLobbyModal.ts
@@ -130,6 +130,8 @@ export class JoinLobbyModal extends BaseModal {
.lobbyCreatorClientID=${hostClientID}
.currentClientID=${this.currentClientID}
.teamCount=${this.gameConfig?.playerTeams ?? 2}
+ .isPublicGame=${this.gameConfig?.gameType ===
+ GameType.Public}
.nationCount=${nationsConfigToSlider(
this.gameConfig?.nations ?? "default",
this.nationCount,
diff --git a/src/client/Utils.ts b/src/client/Utils.ts
index 7f394b564..159e8cfb3 100644
--- a/src/client/Utils.ts
+++ b/src/client/Utils.ts
@@ -36,9 +36,12 @@ export function getGameModeLabel(gameConfig: GameConfig): string {
// Humans vs Nations
if (playerTeams === HumansVsNations) {
- return translateText("public_lobby.teams_hvn_detailed", {
- num: maxPlayers ?? 0,
- });
+ if (maxPlayers) {
+ return translateText("public_lobby.teams_hvn_detailed", {
+ num: maxPlayers,
+ });
+ }
+ return translateText("public_lobby.teams_hvn");
}
// Named team types (Duos, Trios, Quads)
diff --git a/src/client/components/LobbyPlayerView.ts b/src/client/components/LobbyPlayerView.ts
index 9c0366f9e..7ea87bab1 100644
--- a/src/client/components/LobbyPlayerView.ts
+++ b/src/client/components/LobbyPlayerView.ts
@@ -35,11 +35,24 @@ 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 }) isPublicGame: boolean = false;
private theme: PastelTheme = new PastelTheme();
@state() private showTeamColors: boolean = false;
private userSettings: UserSettings = new UserSettings();
+ /**
+ * For public HumansVsNations games, nation count always matches human count
+ * (server enforces this in NationCreation). For private games, the host
+ * controls the nation count via the slider.
+ */
+ private get effectiveNationCount(): number {
+ if (this.isPublicGame && this.teamCount === HumansVsNations) {
+ return this.clients.length;
+ }
+ return this.nationCount;
+ }
+
willUpdate(changedProperties: Map) {
// Recompute team preview when relevant properties change
// clients is updated from WebSocket lobby_info events
@@ -47,7 +60,8 @@ export class LobbyTeamView extends LitElement {
changedProperties.has("gameMode") ||
changedProperties.has("clients") ||
changedProperties.has("teamCount") ||
- changedProperties.has("nationCount")
+ changedProperties.has("nationCount") ||
+ changedProperties.has("isPublicGame")
) {
const teamsList = this.getTeamList();
this.computeTeamPreview(teamsList);
@@ -67,8 +81,8 @@ export class LobbyTeamView extends LitElement {
? translateText("host_modal.player")
: translateText("host_modal.players")}
•
- ${this.nationCount}
- ${this.nationCount === 1
+ ${this.effectiveNationCount}
+ ${this.effectiveNationCount === 1
? translateText("host_modal.nation_player")
: translateText("host_modal.nation_players")}
@@ -179,12 +193,12 @@ export class LobbyTeamView extends LitElement {
private renderTeamCard(preview: TeamPreviewData, isEmpty: boolean = false) {
const displayCount =
preview.team === ColoredTeams.Nations
- ? this.nationCount
+ ? this.effectiveNationCount
: preview.players.length;
const maxTeamSize =
preview.team === ColoredTeams.Nations
- ? this.nationCount
+ ? this.effectiveNationCount
: this.teamMaxSize;
const teamLabel = getTranslatedPlayerTeamLabel(preview.team);
@@ -245,7 +259,7 @@ export class LobbyTeamView extends LitElement {
private getTeamList(): Team[] {
if (this.gameMode !== GameMode.Team) return [];
- const playerCount = this.clients.length + this.nationCount;
+ const playerCount = this.clients.length + this.effectiveNationCount;
const config = this.teamCount;
if (config === HumansVsNations) {
@@ -309,7 +323,7 @@ export class LobbyTeamView extends LitElement {
const assignment = assignTeamsLobbyPreview(
players,
teams,
- this.nationCount,
+ this.effectiveNationCount,
);
const buckets = new Map();
for (const t of teams) buckets.set(t, []);
@@ -333,7 +347,9 @@ export class LobbyTeamView extends LitElement {
// Fallback: divide players across teams; guard against 0 and empty lobbies
this.teamMaxSize = Math.max(
1,
- Math.ceil((this.clients.length + this.nationCount) / teams.length),
+ Math.ceil(
+ (this.clients.length + this.effectiveNationCount) / teams.length,
+ ),
);
}
this.teamPreview = teams.map((t) => ({