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 { GameInfo } from "../core/Schemas"; @customElement("public-lobby") export class PublicLobby extends LitElement { private lobbyOutOfDate = true; @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; 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 { // Due to cache we only update every 2s, // but reduce time by 1s so countdown looks nice. if (this.lobbyOutOfDate) { this.lobbies = await this.fetchLobbies(); } else { this.lobbies.forEach((l) => { l.msUntilStart -= 1000; }); this.requestUpdate(); } this.lobbyOutOfDate = !this.lobbyOutOfDate; } 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(lobby.msUntilStart / 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`; 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.numClients === 1 ? "Player" : "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, gameType: GameType.Public, map: GameMapType.World, difficulty: Difficulty.Medium, }, bubbles: true, composed: true, }), ); } else { this.dispatchEvent( new CustomEvent("leave-lobby", { detail: { lobby: this.currLobby }, bubbles: true, composed: true, }), ); this.leaveLobby(); } } }