import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import randomMap from "../../resources/images/RandomMap.webp"; import { translateText } from "../client/Utils"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { Difficulty, Duos, GameMapType, GameMode, UnitType, mapCategories, } from "../core/game/Game"; import { GameConfig, GameInfo } from "../core/Schemas"; import { generateID } from "../core/Util"; import "./components/baseComponents/Modal"; import "./components/Difficulties"; import { DifficultyDescription } from "./components/Difficulties"; import "./components/Maps"; import { JoinLobbyEvent } from "./Main"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; @customElement("host-lobby-modal") export class HostLobbyModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; }; @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @state() private disableNPCs = false; @state() private gameMode: GameMode = GameMode.FFA; @state() private teamCount: number | typeof Duos = 2; @state() private bots: number = 400; @state() private infiniteGold: boolean = false; @state() private infiniteTroops: boolean = false; @state() private instantBuild: boolean = false; @state() private lobbyId = ""; @state() private copySuccess = false; @state() private players: string[] = []; @state() private useRandomMap: boolean = false; @state() private disabledUnits: UnitType[] = []; private playersInterval: NodeJS.Timeout | null = null; // Add a new timer for debouncing bot changes private botsUpdateTimer: number | null = null; render() { return html` ${this.lobbyId} ${ this.copySuccess ? html`✓` : html` ` } ${translateText("map.map")} ${Object.entries(mapCategories).map( ([categoryKey, maps]) => html` ${translateText(`map_categories.${categoryKey}`)} ${maps.map((mapValue) => { const mapKey = Object.keys(GameMapType).find( (key) => GameMapType[key] === mapValue, ); return html` this.handleMapSelection(mapValue)} > `; })} `, )} ${translateText("map.random")} ${translateText("difficulty.difficulty")} ${Object.entries(Difficulty) .filter(([key]) => isNaN(Number(key))) .map( ([key, value]) => html` this.handleDifficultySelection(value)} > ${translateText( `difficulty.${DifficultyDescription[key]}`, )} `, )} ${translateText("host_modal.mode")} this.handleGameModeSelection(GameMode.FFA)} > ${translateText("game_mode.ffa")} this.handleGameModeSelection(GameMode.Team)} > ${translateText("game_mode.teams")} ${ this.gameMode === GameMode.FFA ? "" : html` ${translateText("host_modal.team_count")} ${[Duos, 2, 3, 4, 5, 6, 7].map( (o) => html` this.handleTeamCountSelection(o)} > ${o} `, )} ` } ${translateText("host_modal.options_title")} ${translateText("host_modal.bots")}${ this.bots === 0 ? translateText("host_modal.bots_disabled") : this.bots } ${translateText("host_modal.disable_nations")} ${translateText("host_modal.instant_build")} ${translateText("host_modal.infinite_gold")} ${translateText("host_modal.infinite_troops")} ${translateText("host_modal.enables_title")} ${renderUnitTypeOptions({ disabledUnits: this.disabledUnits, toggleUnit: this.toggleUnit.bind(this), })} ${this.players.length} ${ this.players.length === 1 ? translateText("host_modal.player") : translateText("host_modal.players") } ${this.players.map( (player) => html`${player}`, )} ${ this.players.length === 1 ? translateText("host_modal.waiting") : translateText("host_modal.start") } `; } createRenderRoot() { return this; } public open() { createLobby() .then((lobby) => { this.lobbyId = lobby.gameID; // join lobby }) .then(() => { this.dispatchEvent( new CustomEvent("join-lobby", { detail: { gameID: this.lobbyId, clientID: generateID(), } as JoinLobbyEvent, bubbles: true, composed: true, }), ); }); this.modalEl?.open(); this.playersInterval = setInterval(() => this.pollPlayers(), 1000); } public close() { this.modalEl?.close(); this.copySuccess = false; if (this.playersInterval) { clearInterval(this.playersInterval); this.playersInterval = null; } // Clear any pending bot updates if (this.botsUpdateTimer !== null) { clearTimeout(this.botsUpdateTimer); this.botsUpdateTimer = null; } } private async handleRandomMapToggle() { this.useRandomMap = true; this.putGameConfig(); } private async handleMapSelection(value: GameMapType) { this.selectedMap = value; this.useRandomMap = false; this.putGameConfig(); } private async handleDifficultySelection(value: Difficulty) { this.selectedDifficulty = value; this.putGameConfig(); } // Modified to include debouncing private handleBotsChange(e: Event) { const value = parseInt((e.target as HTMLInputElement).value); if (isNaN(value) || value < 0 || value > 400) { return; } // Update the display value immediately this.bots = value; // Clear any existing timer if (this.botsUpdateTimer !== null) { clearTimeout(this.botsUpdateTimer); } // Set a new timer to call putGameConfig after 300ms of inactivity this.botsUpdateTimer = window.setTimeout(() => { this.putGameConfig(); this.botsUpdateTimer = null; }, 300); } private handleInstantBuildChange(e: Event) { this.instantBuild = Boolean((e.target as HTMLInputElement).checked); this.putGameConfig(); } private handleInfiniteGoldChange(e: Event) { this.infiniteGold = Boolean((e.target as HTMLInputElement).checked); this.putGameConfig(); } private handleInfiniteTroopsChange(e: Event) { this.infiniteTroops = Boolean((e.target as HTMLInputElement).checked); this.putGameConfig(); } private async handleDisableNPCsChange(e: Event) { this.disableNPCs = Boolean((e.target as HTMLInputElement).checked); console.log(`updating disable npcs to ${this.disableNPCs}`); this.putGameConfig(); } private async handleGameModeSelection(value: GameMode) { this.gameMode = value; this.putGameConfig(); } private async handleTeamCountSelection(value: number | typeof Duos) { this.teamCount = value === Duos ? Duos : Number(value); this.putGameConfig(); } private async putGameConfig() { const url = await buildGameUrl(this.lobbyId, "game"); const response = await fetch(url, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ gameMap: this.selectedMap, difficulty: this.selectedDifficulty, disableNPCs: this.disableNPCs, bots: this.bots, infiniteGold: this.infiniteGold, infiniteTroops: this.infiniteTroops, instantBuild: this.instantBuild, gameMode: this.gameMode, disabledUnits: this.disabledUnits, playerTeams: this.teamCount, } satisfies Partial), }); return response; } private toggleUnit(unit: UnitType, checked: boolean): void { console.log(`Toggling unit type: ${unit} to ${checked}`); this.disabledUnits = checked ? [...this.disabledUnits, unit] : this.disabledUnits.filter((u) => u !== unit); this.putGameConfig(); } private getRandomMap(): GameMapType { const maps = Object.values(GameMapType); const randIdx = Math.floor(Math.random() * maps.length); return maps[randIdx] as GameMapType; } private async startGame() { if (this.useRandomMap) { this.selectedMap = this.getRandomMap(); } await this.putGameConfig(); console.log( `Starting private game with map: ${GameMapType[this.selectedMap]} ${this.useRandomMap ? " (Randomly selected)" : ""}`, ); this.close(); const url = await buildGameUrl(this.lobbyId, "start_game"); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, }); return response; } private async copyToClipboard() { try { //TODO: Convert id to url and copy await navigator.clipboard.writeText( `${location.origin}#join=${this.lobbyId}`, ); this.copySuccess = true; setTimeout(() => { this.copySuccess = false; }, 2000); } catch (err) { console.error(`Failed to copy text: ${err}`); } } private async pollPlayers() { const url = await buildGameUrl(this.lobbyId, "game"); fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }) .then((response) => response.json()) .then((data: GameInfo) => { console.log(`got game info response: ${JSON.stringify(data)}`); this.players = data.clients?.map((p) => p.username) ?? []; }); } } async function createLobby(): Promise { try { const id = generateID(); const url = await buildGameUrl(id, "create_game"); const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", }, // body: JSON.stringify(data), // Include this if you need to send data }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log("Success:", data); return data as GameInfo; } catch (error) { console.error("Error creating lobby:", error); throw error; // Re-throw the error so the caller can handle it } } export async function buildGameUrl( gameID: string, path: string, ): Promise { const config = await getServerConfigFromClient(); const apiPath = `/api/${path}/${gameID}`; const baseUrl = process.env.APP_BASE_URL || ""; return `${baseUrl}/${config.workerPath(gameID)}${apiPath}`; }
${translateText( `difficulty.${DifficultyDescription[key]}`, )}