diff --git a/resources/lang/en.json b/resources/lang/en.json index 28b1c2caa..cd7c69a01 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -275,6 +275,8 @@ "enables_title": "Enable Settings", "player": "Player", "players": "Players", + "nation_players": "Nations", + "nation_player": "Nation", "waiting": "Waiting for players...", "random_spawn": "Random spawn", "start": "Start Game", @@ -282,7 +284,7 @@ "assigned_teams": "Assigned Teams", "empty_teams": "Empty Teams", "empty_team": "Empty", - "remove_player": "Remove {{username}}" + "remove_player": "Remove {username}" }, "team_colors": { "red": "Red", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 9ace197cf..5d94324e9 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -28,8 +28,8 @@ import "./components/Difficulties"; import "./components/LobbyTeamView"; import "./components/Maps"; import { JoinLobbyEvent } from "./Main"; +import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; - @customElement("host-lobby-modal") export class HostLobbyModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { @@ -58,11 +58,13 @@ export class HostLobbyModal extends LitElement { @state() private disabledUnits: UnitType[] = []; @state() private lobbyCreatorClientID: string = ""; @state() private lobbyIdVisible: boolean = true; + @state() private nationCount: number = 0; private playersInterval: NodeJS.Timeout | null = null; // Add a new timer for debouncing bot changes private botsUpdateTimer: number | null = null; private userSettings: UserSettings = new UserSettings(); + private mapLoader = terrainMapFileLoader; connectedCallback() { super.connectedCallback(); @@ -553,6 +555,13 @@ export class HostLobbyModal extends LitElement { ? translateText("host_modal.player") : translateText("host_modal.players") } + + ${this.nationCount} + ${ + this.nationCount === 1 + ? translateText("host_modal.nation_player") + : translateText("host_modal.nation_players") + } this.kickPlayer(clientID)} > @@ -613,6 +623,7 @@ export class HostLobbyModal extends LitElement { }); this.modalEl?.open(); this.playersInterval = setInterval(() => this.pollPlayers(), 1000); + this.loadNationCount(); } public close() { @@ -631,12 +642,15 @@ export class HostLobbyModal extends LitElement { private async handleRandomMapToggle() { this.useRandomMap = true; + this.selectedMap = this.getRandomMap(); + await this.loadNationCount(); this.putGameConfig(); } private async handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; + await this.loadNationCount(); this.putGameConfig(); } @@ -794,10 +808,6 @@ export class HostLobbyModal extends LitElement { } private async startGame() { - if (this.useRandomMap) { - this.selectedMap = this.getRandomMap(); - } - await this.putGameConfig(); console.log( `Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`, @@ -857,6 +867,17 @@ export class HostLobbyModal extends LitElement { }), ); } + + private async loadNationCount() { + try { + const mapData = this.mapLoader.getMapData(this.selectedMap); + const manifest = await mapData.manifest(); + this.nationCount = manifest.nations.length; + } catch (error) { + console.warn("Failed to load nation count", error); + this.nationCount = 0; + } + } } async function createLobby(creatorClientID: string): Promise { diff --git a/src/client/components/LobbyTeamView.ts b/src/client/components/LobbyTeamView.ts index 0679108f8..711d9790c 100644 --- a/src/client/components/LobbyTeamView.ts +++ b/src/client/components/LobbyTeamView.ts @@ -13,7 +13,7 @@ import { Team, Trios, } from "../../core/game/Game"; -import { assignTeams } from "../../core/game/TeamAssignment"; +import { assignTeamsLobbyPreview } from "../../core/game/TeamAssignment"; import { ClientInfo, TeamCountConfig } from "../../core/Schemas"; import { translateText } from "../Utils"; @@ -31,19 +31,23 @@ export class LobbyTeamView extends LitElement { @property({ type: String }) lobbyCreatorClientID: string = ""; @property({ attribute: "team-count" }) teamCount: TeamCountConfig = 2; @property({ type: Function }) onKickPlayer?: (clientID: string) => void; + @property({ type: Number }) nationCount: number = 0; private theme: PastelTheme = new PastelTheme(); @state() private showTeamColors: boolean = false; willUpdate(changedProperties: Map) { // Recompute team preview when relevant properties change + // clients is 'changed' every 1s from pollPlayers, chose to not compare for actual change if ( changedProperties.has("gameMode") || changedProperties.has("clients") || - changedProperties.has("teamCount") + changedProperties.has("teamCount") || + changedProperties.has("nationCount") ) { - this.computeTeamPreview(); - this.showTeamColors = this.getTeamList().length <= 7; + const teamsList = this.getTeamList(); + this.computeTeamPreview(teamsList); + this.showTeamColors = teamsList.length <= 7; } } @@ -60,8 +64,12 @@ export class LobbyTeamView extends LitElement { } private renderTeamMode() { - const active = this.teamPreview.filter((t) => t.players.length > 0); - const empty = this.teamPreview.filter((t) => t.players.length === 0); + const active = this.teamPreview.filter( + (t) => t.players.length > 0 || t.team === ColoredTeams.Nations, + ); + const empty = this.teamPreview.filter( + (t) => t.players.length === 0 && t.team !== ColoredTeams.Nations, + ); return html`
@@ -96,9 +104,11 @@ export class LobbyTeamView extends LitElement {
-
- ${translateText("host_modal.empty_teams")} -
+ ${empty.length > 0 + ? html`
+ ${translateText("host_modal.empty_teams")} +
` + : ""}
${repeat( empty, @@ -136,6 +146,16 @@ export class LobbyTeamView extends LitElement { } private renderTeamCard(preview: TeamPreviewData, isEmpty: boolean = false) { + const displayCount = + preview.team === ColoredTeams.Nations + ? this.nationCount + : preview.players.length; + + const maxTeamSize = + preview.team === ColoredTeams.Nations + ? this.nationCount + : this.teamMaxSize; + return html`
` : null} ${preview.team} - ${preview.players.length}/${this.teamMaxSize} + ${displayCount}/${maxTeamSize}
${isEmpty @@ -190,7 +208,7 @@ export class LobbyTeamView extends LitElement { private getTeamList(): Team[] { if (this.gameMode !== GameMode.Team) return []; - const playerCount = this.clients.length; + const playerCount = this.clients.length + this.nationCount; const config = this.teamCount; if (config === HumansVsNations) { @@ -230,13 +248,12 @@ export class LobbyTeamView extends LitElement { } } - private computeTeamPreview() { + private computeTeamPreview(teams: Team[] = []) { if (this.gameMode !== GameMode.Team) { this.teamPreview = []; this.teamMaxSize = 0; return; } - const teams = this.getTeamList(); // HumansVsNations: show all clients under Humans initially if (this.teamCount === HumansVsNations) { @@ -252,7 +269,11 @@ export class LobbyTeamView extends LitElement { (c) => new PlayerInfo(c.username, PlayerType.Human, c.clientID, c.clientID), ); - const assignment = assignTeams(players, teams); + const assignment = assignTeamsLobbyPreview( + players, + teams, + this.nationCount, + ); const buckets = new Map(); for (const t of teams) buckets.set(t, []); @@ -260,9 +281,7 @@ export class LobbyTeamView extends LitElement { if (team === "kicked") continue; const bucket = buckets.get(team); if (!bucket) continue; - const client = - this.clients.find((c) => c.clientID === p.clientID) ?? - this.clients.find((c) => c.username === p.name); + const client = this.clients.find((c) => c.clientID === p.clientID); if (client) bucket.push(client); } @@ -277,7 +296,7 @@ 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 / teams.length), + Math.ceil((this.clients.length + this.nationCount) / teams.length), ); } this.teamPreview = teams.map((t) => ({ diff --git a/src/core/game/TeamAssignment.ts b/src/core/game/TeamAssignment.ts index 535626d7d..13e2f6c64 100644 --- a/src/core/game/TeamAssignment.ts +++ b/src/core/game/TeamAssignment.ts @@ -5,6 +5,7 @@ import { PlayerInfo, PlayerType, Team } from "./Game"; export function assignTeams( players: PlayerInfo[], teams: Team[], + maxTeamSize: number = getMaxTeamSize(players.length, teams.length), ): Map { const result = new Map(); const teamPlayerCount = new Map(); @@ -25,8 +26,6 @@ export function assignTeams( } } - const maxTeamSize = Math.ceil(players.length / teams.length); - // Sort clans by size (largest first) const sortedClans = Array.from(clanGroups.entries()).sort( (a, b) => b[1].length - a[1].length, @@ -87,3 +86,19 @@ export function assignTeams( return result; } + +export function assignTeamsLobbyPreview( + players: PlayerInfo[], + teams: Team[], + nationCount: number, +): Map { + const maxTeamSize = getMaxTeamSize( + players.length + nationCount, + teams.length, + ); + return assignTeams(players, teams, maxTeamSize); +} + +export function getMaxTeamSize(numPlayers: number, numTeams: number): number { + return Math.ceil(numPlayers / numTeams); +}