mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
Improve public lobby join button UI/UX with animated three-dot indicator (#2670)
## Description: This PR improves the public lobby join button UI by providing clearer, state-aware feedback while a player is waiting to enter a match. The button text now reflects two distinct phases of the join flow: - **Waiting for players** while the lobby is filling - **Starting game…** when the match is about to begin This removes ambiguity caused by relying only on button color changes and makes it immediately clear whether the join action has registered and what stage the lobby is currently in. ### Demo  ## How - Replaces the static **“Join next game”** label with dynamic text based on lobby state - Shows **“Waiting for players”** with an animated three-dot indicator while the lobby fills - Switches to **“Starting game…”** shortly before the match begins - Animation and state reset cleanly when leaving or cancelling - Uses existing lobby timing and state, with no additional network calls ## Notes - No CSS changes - No behavioral changes to matchmaking logic - Fully contained within `PublicLobby.ts` - Added translation keys for the updated indicators to `en.json` (Rest of language files will need to be updated) ## Testing notes During local testing (single-player, local server), the button text transitions as follows: - Initial state (not clicked): **“Join next game”** - After first click, the text briefly shows **“Starting game…”** - It then switches to **“Waiting for players”** with the animated dots - Shortly before the match starts, it switches back to **“Starting game…”** and proceeds to start a solo game Not sure why this flow happens in the testing environment: **“Join next game”** ->**“Starting game…”** (brief) -> **“Waiting for **players...”**** -> **“Starting game…”** (brief) instead of just: **“Join next game”** -> **“Waiting for **players...”**** -> **“Starting game…”** (brief) More testing is needed. ## Checklist - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [ ] I have added relevant tests to the test directory - [ ] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced
This commit is contained in:
committed by
GitHub
parent
86d1ac6c62
commit
28e22c9ca8
@@ -268,6 +268,8 @@
|
||||
"public_lobby": {
|
||||
"join": "Join next Game",
|
||||
"waiting": "players waiting",
|
||||
"waiting_for_players": "Waiting for players",
|
||||
"starting_game": "Starting game…",
|
||||
"teams_Duos": "of 2 (Duos)",
|
||||
"teams_Trios": "of 3 (Trios)",
|
||||
"teams_Quads": "of 4 (Quads)",
|
||||
|
||||
+46
-24
@@ -20,7 +20,10 @@ export class PublicLobby extends LitElement {
|
||||
@state() public isLobbyHighlighted: boolean = false;
|
||||
@state() private isButtonDebounced: boolean = false;
|
||||
@state() private mapImages: Map<GameID, string> = new Map();
|
||||
@state() private joiningDotIndex: number = 0;
|
||||
|
||||
private lobbiesInterval: number | null = null;
|
||||
private joiningInterval: number | null = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
private debounceDelay: number = 750;
|
||||
private lobbyIDToStart = new Map<GameID, number>();
|
||||
@@ -45,20 +48,18 @@ export class PublicLobby extends LitElement {
|
||||
clearInterval(this.lobbiesInterval);
|
||||
this.lobbiesInterval = null;
|
||||
}
|
||||
this.stopJoiningAnimation();
|
||||
}
|
||||
|
||||
private async fetchAndUpdateLobbies(): Promise<void> {
|
||||
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)) {
|
||||
const msUntilStart = l.msUntilStart ?? 0;
|
||||
this.lobbyIDToStart.set(l.gameID, msUntilStart + Date.now());
|
||||
}
|
||||
|
||||
// Load map image if not already loaded
|
||||
if (l.gameConfig && !this.mapImages.has(l.gameID)) {
|
||||
this.loadMapImage(l.gameID, l.gameConfig.gameMap);
|
||||
}
|
||||
@@ -70,7 +71,6 @@ export class PublicLobby extends LitElement {
|
||||
|
||||
private async loadMapImage(gameID: GameID, gameMap: string) {
|
||||
try {
|
||||
// Convert string to GameMapType enum value
|
||||
const mapType = gameMap as GameMapType;
|
||||
const data = terrainMapFileLoader.getMapData(mapType);
|
||||
this.mapImages.set(gameID, await data.webpPath());
|
||||
@@ -106,6 +106,7 @@ export class PublicLobby extends LitElement {
|
||||
public stop() {
|
||||
if (this.lobbiesInterval !== null) {
|
||||
this.isLobbyHighlighted = false;
|
||||
this.stopJoiningAnimation();
|
||||
clearInterval(this.lobbiesInterval);
|
||||
this.lobbiesInterval = null;
|
||||
}
|
||||
@@ -115,13 +116,11 @@ export class PublicLobby extends LitElement {
|
||||
if (this.lobbies.length === 0) return html``;
|
||||
|
||||
const lobby = this.lobbies[0];
|
||||
if (!lobby?.gameConfig) {
|
||||
return;
|
||||
}
|
||||
if (!lobby?.gameConfig) return html``;
|
||||
|
||||
const start = this.lobbyIDToStart.get(lobby.gameID) ?? 0;
|
||||
const timeRemaining = Math.max(0, Math.floor((start - Date.now()) / 1000));
|
||||
|
||||
// Format time to show minutes and seconds
|
||||
const isStarting = timeRemaining <= 2;
|
||||
const timeDisplay = renderDuration(timeRemaining);
|
||||
|
||||
const teamCount =
|
||||
@@ -177,17 +176,26 @@ export class PublicLobby extends LitElement {
|
||||
>
|
||||
<div>
|
||||
<div class="text-lg md:text-2xl font-semibold">
|
||||
${translateText("public_lobby.join")}
|
||||
${this.currLobby
|
||||
? isStarting
|
||||
? html`${translateText("public_lobby.starting_game")}`
|
||||
: html`${translateText("public_lobby.waiting_for_players")}
|
||||
${[0, 1, 2]
|
||||
.map((i) => (i === this.joiningDotIndex ? "•" : "·"))
|
||||
.join("")}`
|
||||
: translateText("public_lobby.join")}
|
||||
</div>
|
||||
<div class="text-md font-medium text-white-400">
|
||||
<span class="text-sm text-red-800 bg-white rounded-sm px-1 mr-1"
|
||||
>${fullModeLabel}</span
|
||||
>
|
||||
<span
|
||||
>${translateText(
|
||||
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/[\s.]+/g, "")}`,
|
||||
)}</span
|
||||
>
|
||||
<span class="text-sm text-red-800 bg-white rounded-sm px-1 mr-1">
|
||||
${fullModeLabel}
|
||||
</span>
|
||||
<span>
|
||||
${translateText(
|
||||
`map.${lobby.gameConfig.gameMap
|
||||
.toLowerCase()
|
||||
.replace(/[\s.]+/g, "")}`,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -205,6 +213,24 @@ export class PublicLobby extends LitElement {
|
||||
leaveLobby() {
|
||||
this.isLobbyHighlighted = false;
|
||||
this.currLobby = null;
|
||||
this.stopJoiningAnimation();
|
||||
}
|
||||
|
||||
private startJoiningAnimation() {
|
||||
if (this.joiningInterval !== null) return;
|
||||
|
||||
this.joiningDotIndex = 0;
|
||||
this.joiningInterval = window.setInterval(() => {
|
||||
this.joiningDotIndex = (this.joiningDotIndex + 1) % 3;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private stopJoiningAnimation() {
|
||||
if (this.joiningInterval !== null) {
|
||||
clearInterval(this.joiningInterval);
|
||||
this.joiningInterval = null;
|
||||
}
|
||||
this.joiningDotIndex = 0;
|
||||
}
|
||||
|
||||
private getTeamSize(
|
||||
@@ -270,14 +296,9 @@ export class PublicLobby extends LitElement {
|
||||
}
|
||||
|
||||
private lobbyClicked(lobby: GameInfo) {
|
||||
if (this.isButtonDebounced) {
|
||||
return;
|
||||
}
|
||||
if (this.isButtonDebounced) return;
|
||||
|
||||
// Set debounce state
|
||||
this.isButtonDebounced = true;
|
||||
|
||||
// Reset debounce after delay
|
||||
setTimeout(() => {
|
||||
this.isButtonDebounced = false;
|
||||
}, this.debounceDelay);
|
||||
@@ -285,6 +306,7 @@ export class PublicLobby extends LitElement {
|
||||
if (this.currLobby === null) {
|
||||
this.isLobbyHighlighted = true;
|
||||
this.currLobby = lobby;
|
||||
this.startJoiningAnimation();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
|
||||
Reference in New Issue
Block a user