import { LitElement, html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { Difficulty, GameMapType, GameType } from "../core/game/Game"; import { consolex } from "../core/Consolex"; import { getMapsImage } from "./utilities/Maps"; import { GameID, GameInfo } from "../core/Schemas"; @customElement("public-lobby") export class PublicLobby extends LitElement { @state() private lobbies: GameInfo[] = []; @state() public isLobbyHighlighted: boolean = false; @state() private isButtonDebounced: boolean = false; private lobbiesInterval: number | null = null; private currLobby: GameInfo = null; private debounceDelay: number = 750; private lobbyIDToStart = new Map(); createRenderRoot() { return this; } connectedCallback() { super.connectedCallback(); this.fetchAndUpdateLobbies(); this.lobbiesInterval = window.setInterval( () => this.fetchAndUpdateLobbies(), 1000, ); } disconnectedCallback() { super.disconnectedCallback(); if (this.lobbiesInterval !== null) { clearInterval(this.lobbiesInterval); this.lobbiesInterval = null; } } private async fetchAndUpdateLobbies(): Promise { try { this.lobbies = await this.fetchLobbies(); this.lobbies.forEach((l) => { // Store the start time on first fetch because endpoint is cached, causing // the time to appear irregular. if (!this.lobbyIDToStart.has(l.gameID)) { this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now()); } }); } catch (error) { consolex.error("Error fetching lobbies:", error); } } async fetchLobbies(): Promise { try { const response = await fetch(`/api/public_lobbies`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const data = await response.json(); return data.lobbies; } catch (error) { consolex.error("Error fetching lobbies:", error); throw error; } } public stop() { if (this.lobbiesInterval !== null) { this.isLobbyHighlighted = false; clearInterval(this.lobbiesInterval); this.lobbiesInterval = null; } } render() { if (this.lobbies.length === 0) return html``; const lobby = this.lobbies[0]; if (!lobby?.gameConfig) { return; } const timeRemaining = Math.max( 0, Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000), ); // Format time to show minutes and seconds const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; const playersRemainingBeforeMax = lobby.gameConfig.maxPlayers - lobby.numClients; return html` this.lobbyClicked(lobby)} ?disabled=${this.isButtonDebounced} class="w-full mx-auto p-4 md:p-6 ${this.isLobbyHighlighted ? "bg-gradient-to-r from-green-600 to-green-500" : "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this .isButtonDebounced ? "opacity-70 cursor-not-allowed" : ""}" > Join next Game ${lobby.gameConfig.gameMap} ${lobby.numClients} / ${lobby.gameConfig.maxPlayers} players waiting ${timeDisplay} `; } leaveLobby() { this.isLobbyHighlighted = false; this.currLobby = null; } private lobbyClicked(lobby: GameInfo) { if (this.isButtonDebounced) { return; } // Set debounce state this.isButtonDebounced = true; // Reset debounce after delay setTimeout(() => { this.isButtonDebounced = false; }, this.debounceDelay); if (this.currLobby == null) { this.isLobbyHighlighted = true; this.currLobby = lobby; this.dispatchEvent( new CustomEvent("join-lobby", { detail: lobby, bubbles: true, composed: true, }), ); } else { this.dispatchEvent( new CustomEvent("leave-lobby", { detail: { lobby: this.currLobby }, bubbles: true, composed: true, }), ); this.leaveLobby(); } } }