import { html, LitElement, nothing, type TemplateResult } from "lit"; import { customElement, state } from "lit/decorators.js"; import { Duos, GameMapType, GameMode, HumansVsNations, Quads, Trios, } from "../core/game/Game"; import { PublicGameInfo, PublicGames } from "../core/Schemas"; import { HostLobbyModal } from "./HostLobbyModal"; import { JoinLobbyModal } from "./JoinLobbyModal"; import { PublicLobbySocket } from "./LobbySocket"; import { JoinLobbyEvent } from "./Main"; import { SinglePlayerModal } from "./SinglePlayerModal"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { getMapName, getModifierLabels, renderDuration, translateText, } from "./Utils"; const CARD_BG = "bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)]"; @customElement("game-mode-selector") export class GameModeSelector extends LitElement { @state() private lobbies: PublicGames | null = null; private serverTimeOffset: number = 0; private lobbySocket = new PublicLobbySocket((lobbies) => this.handleLobbiesUpdate(lobbies), ); createRenderRoot() { return this; } /** * Validates username input and shows error message if invalid. * Returns true if valid, false otherwise. */ private validateUsername(): boolean { const usernameInput = document.querySelector("username-input") as any; if (usernameInput?.isValid?.() === false) { window.dispatchEvent( new CustomEvent("show-message", { detail: { message: usernameInput.validationError, color: "red", duration: 3000, }, }), ); return false; } return true; } connectedCallback() { super.connectedCallback(); this.lobbySocket.start(); } disconnectedCallback() { this.stop(); super.disconnectedCallback(); } public stop() { this.lobbySocket.stop(); } private handleLobbiesUpdate(lobbies: PublicGames) { this.lobbies = lobbies; this.serverTimeOffset = lobbies.serverTime - Date.now(); document.dispatchEvent( new CustomEvent("public-lobbies-update", { detail: { payload: lobbies }, }), ); this.requestUpdate(); } render() { const ffa = this.lobbies?.games?.["ffa"]?.[0]; const teams = this.lobbies?.games?.["team"]?.[0]; const special = this.lobbies?.games?.["special"]?.[0]; return html`
${ffa ? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa)) : nothing} ${teams ? this.renderLobbyCard(teams, this.getLobbyTitle(teams)) : nothing} ${special ? this.renderSpecialLobbyCard(special) : nothing} ${this.renderQuickActionsSection()}
`; } private renderSpecialLobbyCard(lobby: PublicGameInfo) { const subtitle = this.getLobbyTitle(lobby); const mainTitle = translateText("mode_selector.special_title"); const titleContent = subtitle ? html` ${mainTitle} ${subtitle} ` : mainTitle; return this.renderLobbyCard(lobby, titleContent); } private renderQuickActionsSection() { return html`
${this.renderSmallActionCard( translateText("main.solo"), this.openSinglePlayerModal, )} ${this.renderSmallActionCard( translateText("mode_selector.ranked_title"), this.openRankedMenu, )}
${this.renderSmallActionCard( translateText("main.create"), this.openHostLobby, )} ${this.renderSmallActionCard( translateText("main.join"), this.openJoinLobby, )}
`; } private openRankedMenu = () => { if (!this.validateUsername()) return; window.showPage?.("page-ranked"); }; private openSinglePlayerModal = () => { if (!this.validateUsername()) return; ( document.querySelector("single-player-modal") as SinglePlayerModal )?.open(); }; private openHostLobby = () => { if (!this.validateUsername()) return; (document.querySelector("host-lobby-modal") as HostLobbyModal)?.open(); }; private openJoinLobby = () => { if (!this.validateUsername()) return; (document.querySelector("join-lobby-modal") as JoinLobbyModal)?.open(); }; private renderSmallActionCard(title: string, onClick: () => void) { return html` `; } private renderLobbyCard( lobby: PublicGameInfo, titleContent: string | TemplateResult, ) { const mapType = lobby.gameConfig!.gameMap as GameMapType; const mapImageSrc = terrainMapFileLoader.getMapData(mapType).webpPath; const timeRemaining = lobby.startsAt ? Math.max( 0, Math.floor( (lobby.startsAt - this.serverTimeOffset - Date.now()) / 1000, ), ) : undefined; let timeDisplay: string = ""; if (timeRemaining === undefined) { timeDisplay = "-s"; } else if (timeRemaining > 0) { timeDisplay = renderDuration(timeRemaining); } else { timeDisplay = translateText("public_lobby.starting_game"); } const mapName = getMapName(lobby.gameConfig?.gameMap); const modifierLabels = getModifierLabels( lobby.gameConfig?.publicGameModifiers, ); // Sort by length for visual consistency (shorter labels first) if (modifierLabels.length > 1) { modifierLabels.sort((a, b) => a.length - b.length); } return html` `; } private validateAndJoin(lobby: PublicGameInfo) { if (!this.validateUsername()) return; this.dispatchEvent( new CustomEvent("join-lobby", { detail: { gameID: lobby.gameID, source: "public", publicLobbyInfo: lobby, } as JoinLobbyEvent, bubbles: true, composed: true, }), ); } private getLobbyTitle(lobby: PublicGameInfo): string { const config = lobby.gameConfig!; if (config.gameMode === GameMode.FFA) { return translateText("game_mode.ffa"); } if (config?.gameMode === GameMode.Team) { const totalPlayers = config.maxPlayers ?? lobby.numClients ?? undefined; const formatTeamsOf = ( teamCount: number | undefined, playersPerTeam: number | undefined, label?: string, ) => { if (!teamCount) return label ?? translateText("mode_selector.teams_title"); const baseTitle = playersPerTeam ? translateText("mode_selector.teams_of", { teamCount: String(teamCount), playersPerTeam: String(playersPerTeam), }) : translateText("mode_selector.teams_count", { teamCount: String(teamCount), }); return `${baseTitle}${label ? ` (${label})` : ""}`; }; switch (config.playerTeams) { case Duos: { const teamCount = totalPlayers ? Math.floor(totalPlayers / 2) : undefined; return teamCount ? translateText("public_lobby.teams_Duos", { team_count: String(teamCount), }) : formatTeamsOf(undefined, 2); } case Trios: { const teamCount = totalPlayers ? Math.floor(totalPlayers / 3) : undefined; return teamCount ? translateText("public_lobby.teams_Trios", { team_count: String(teamCount), }) : formatTeamsOf(undefined, 3); } case Quads: { const teamCount = totalPlayers ? Math.floor(totalPlayers / 4) : undefined; return teamCount ? translateText("public_lobby.teams_Quads", { team_count: String(teamCount), }) : formatTeamsOf(undefined, 4); } case HumansVsNations: { const humanSlots = config.maxPlayers ?? lobby.numClients; return humanSlots ? translateText("public_lobby.teams_hvn_detailed", { num: String(humanSlots), }) : translateText("public_lobby.teams_hvn"); } default: if (typeof config.playerTeams === "number") { const teamCount = config.playerTeams; const playersPerTeam = totalPlayers && teamCount > 0 ? Math.floor(totalPlayers / teamCount) : undefined; return formatTeamsOf(teamCount, playersPerTeam); } } } return ""; } }