diff --git a/index.html b/index.html index 7bfcfbd24..f33b919c0 100644 --- a/index.html +++ b/index.html @@ -284,9 +284,13 @@
-
- -
+ +
{ + if (this.usernameInput?.isValid()) { + rankedQueueModal?.open(); + } + }); + if (this.userSettings.darkMode()) { document.documentElement.classList.add("dark"); } else { diff --git a/src/client/RankedQueue.ts b/src/client/RankedQueue.ts index 47b593973..b0e1b77e3 100644 --- a/src/client/RankedQueue.ts +++ b/src/client/RankedQueue.ts @@ -1,5 +1,6 @@ import { LitElement, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; +import { customElement, query, state } from "lit/decorators.js"; +import { classMap } from "lit/directives/class-map.js"; import { generateID } from "../core/Util"; import { getApiBase, getUserMe } from "./Api"; import { userAuth } from "./Auth"; @@ -7,7 +8,7 @@ import { JoinLobbyEvent } from "./Main"; import { translateText } from "./Utils"; type QueueType = "ranked" | "unranked"; -type GameMode = "ffa" | "team" | "duel"; +type GameMode = "ffa" | "team" | "duel" | "duos" | "trios" | "quads"; interface QueueStatus { queueSize: number; @@ -26,6 +27,11 @@ interface LeaderboardEntry { @customElement("ranked-queue") export class RankedQueue extends LitElement { + @query("o-modal") private modalEl!: HTMLElement & { + open: () => void; + close: () => void; + }; + @state() private inQueue: boolean = false; @state() private queueType: QueueType = "ranked"; @state() private gameMode: GameMode = "ffa"; @@ -53,17 +59,25 @@ export class RankedQueue extends LitElement { /** * Get the current player's ELO for the selected game mode + * Returns null for unranked modes (duos, trios, quads, unranked ffa) */ private get currentPlayerElo(): number | null { + if (this.queueType === "unranked") { + return null; // No ELO for unranked modes + } return this.gameMode === "duel" ? this.playerEloByMode.duel : this.playerEloByMode.ffa; } - async connectedCallback() { - super.connectedCallback(); - // Fetch player ELO and leaderboard immediately when component loads - await Promise.all([this.fetchPlayerElo(), this.fetchLeaderboard()]); + /** + * Check if the current mode is a ranked mode (has ELO tracking) + */ + private get isRankedMode(): boolean { + return ( + this.queueType === "ranked" && + (this.gameMode === "ffa" || this.gameMode === "duel") + ); } disconnectedCallback() { @@ -125,7 +139,6 @@ export class RankedQueue extends LitElement { } } catch (error) { console.error("Failed to fetch leaderboard:", error); - // Don't show error to user, just silently fail } finally { this.isLoadingLeaderboard = false; } @@ -160,7 +173,6 @@ export class RankedQueue extends LitElement { const token = loginResult.jwt; // Determine WebSocket URL based on environment - // In development, use local matchmaking server; in production, use API const matchmakingBase = process?.env?.MATCHMAKING_WS_URL; const wsUrl = matchmakingBase ? `${matchmakingBase}/matchmaking/join` @@ -240,7 +252,6 @@ export class RankedQueue extends LitElement { case "auth_success": console.log("Authentication successful"); if (message.playerElo !== undefined) { - // Update the ELO for the current game mode from the server response if (this.gameMode === "duel") { this.playerEloByMode = { ...this.playerEloByMode, @@ -306,6 +317,7 @@ export class RankedQueue extends LitElement { this.inQueue = false; this.queueStatus = null; this.cleanup(); + this.close(); } private async joinQueue() { @@ -313,10 +325,8 @@ export class RankedQueue extends LitElement { return; } - // Connect WebSocket if not connected await this.connectWebSocket(); - // Function to send join queue message const sendJoinMessage = () => { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send( @@ -329,12 +339,10 @@ export class RankedQueue extends LitElement { } }; - // Send join queue message immediately if connected, or wait for connection if (this.ws) { if (this.ws.readyState === WebSocket.OPEN) { sendJoinMessage(); } else if (this.ws.readyState === WebSocket.CONNECTING) { - // Wait for connection to open before sending join message this.ws.addEventListener("open", () => sendJoinMessage(), { once: true, }); @@ -347,7 +355,6 @@ export class RankedQueue extends LitElement { return; } - // Send leave queue message if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send( JSON.stringify({ @@ -362,123 +369,243 @@ export class RankedQueue extends LitElement { } private setQueueType(type: QueueType) { - if (this.inQueue) { - return; // Can't change while in queue + if (this.inQueue || this.isConnecting) { + return; + } + if (this.queueType !== type) { + this.queueType = type; + this.gameMode = "ffa"; + if (type === "ranked") { + this.fetchLeaderboard(); + } } - this.queueType = type; } private setGameMode(mode: GameMode) { - if (this.inQueue) { - return; // Can't change while in queue + if (this.inQueue || this.isConnecting) { + return; } if (this.gameMode !== mode) { this.gameMode = mode; - // Refresh leaderboard for the new mode - this.fetchLeaderboard(); + if (this.queueType === "ranked") { + this.fetchLeaderboard(); + } } } + public async open() { + this.modalEl?.open(); + await Promise.all([this.fetchPlayerElo(), this.fetchLeaderboard()]); + } + + public close() { + if (this.inQueue) { + this.leaveQueue(); + } + this.modalEl?.close(); + } + render() { return html` -
- -
-

- ${translateText("ranked_queue.ranked_matchmaking")} -

-
- -
- + +
+
+ + ${this.queueType === "ranked" + ? html` +
+ + +
+ ` + : html` +
+ + + + +
+ `} + + + ${this.isRankedMode + ? html` +
+ ${this.isLoadingElo + ? html`${translateText("ranked_queue.loading_elo")}` + : this.currentPlayerElo !== null + ? html`${translateText("ranked_queue.your_elo")} + ${this.currentPlayerElo}` + : ""} +
+ ` + : ""} + - - + + ${this.error + ? html`
+ ${this.error} +
` + : ""} + + + ${this.inQueue && this.queueStatus + ? html` +
+ ${this.queueStatus.queueSize} + ${translateText("ranked_queue.players_in_queue")} +
+ ` + : ""} + + + ${this.isRankedMode + ? html` + + ` + : ""} - ${this.showLeaderboard + ${this.isRankedMode && this.showLeaderboard ? html`
${this.isLoadingLeaderboard - ? html`
+ ? html`
${translateText("ranked_queue.loading_leaderboard")}
` : this.leaderboard.length === 0 - ? html`
+ ? html`
${translateText("ranked_queue.no_ranked_players")}
` : html` @@ -486,13 +613,13 @@ export class RankedQueue extends LitElement { ${this.leaderboard.slice(0, 10).map( (entry) => html`
#${entry.rank}
@@ -500,15 +627,9 @@ export class RankedQueue extends LitElement {
${entry.username}
-
+
${entry.gamesPlayed} - ${translateText("ranked_queue.games")} • - ${entry.wins}${translateText( - "ranked_queue.wins_short", - )} - ${entry.losses}${translateText( - "ranked_queue.losses_short", - )} + ${translateText("ranked_queue.games")}
@@ -516,7 +637,7 @@ export class RankedQueue extends LitElement {
${entry.currentElo}
-
+
${translateText("ranked_queue.elo")}
@@ -529,7 +650,7 @@ export class RankedQueue extends LitElement { ` : ""}
-
+ `; } } diff --git a/src/server/MapSelection.ts b/src/server/MapSelection.ts index 0a7c40ae2..955b20d54 100644 --- a/src/server/MapSelection.ts +++ b/src/server/MapSelection.ts @@ -7,6 +7,7 @@ export interface MapSelectionCriteria { playerCount: number; gameMode: GameMode; queueType: "ranked" | "unranked"; + matchMode?: "ffa" | "team" | "duel" | "duos" | "trios" | "quads"; } /** @@ -16,7 +17,11 @@ export interface MapSelectionCriteria { export function selectMapForRanked( criteria: MapSelectionCriteria, ): GameMapType { - const { playerCount, gameMode } = criteria; + const { playerCount, gameMode, matchMode } = criteria; + + if (matchMode === "duel") { + return GameMapType.Australia; + } // Get maps that can handle this player count const suitableMaps = getSuitableMaps(playerCount); diff --git a/src/server/RankedGameConfig.ts b/src/server/RankedGameConfig.ts index 9a12e3799..48ce769ef 100644 --- a/src/server/RankedGameConfig.ts +++ b/src/server/RankedGameConfig.ts @@ -1,15 +1,18 @@ import { Difficulty, + Duos, GameMapSize, GameMapType, GameMode, GameType, + Quads, + Trios, } from "../core/game/Game"; import { GameConfig, TeamCountConfig } from "../core/Schemas"; export interface RankedMatchConfig { queueType: "ranked" | "unranked"; - gameMode: "ffa" | "team" | "duel"; + gameMode: "ffa" | "team" | "duel" | "duos" | "trios" | "quads"; playerCount: number; teamConfig?: TeamCountConfig; } @@ -25,11 +28,27 @@ export function buildRankedGameConfig( ): GameConfig { const { gameMode, playerCount } = matchConfig; const isDuel = gameMode === "duel"; - const mode = gameMode === "team" ? GameMode.Team : GameMode.FFA; + const isFFA = gameMode === "ffa"; + const isTeamMode = + gameMode === "team" || + gameMode === "duos" || + gameMode === "trios" || + gameMode === "quads"; + const mode = isTeamMode ? GameMode.Team : GameMode.FFA; + + // Determine team configuration based on game mode + let teamConfig: TeamCountConfig | undefined = matchConfig.teamConfig; + if (gameMode === "duos") { + teamConfig = Duos; + } else if (gameMode === "trios") { + teamConfig = Trios; + } else if (gameMode === "quads") { + teamConfig = Quads; + } return { gameMap: map, - gameMapSize: isDuel ? GameMapSize.Compact : selectMapSize(playerCount), + gameMapSize: isDuel ? GameMapSize.Normal : selectMapSize(playerCount), gameType: GameType.Public, gameMode: mode, maxPlayers: playerCount, @@ -39,21 +58,21 @@ export function buildRankedGameConfig( disableNPCs: isDuel ? true : false, // Donation rules - donateGold: mode === GameMode.Team, - donateTroops: mode === GameMode.Team, + donateGold: isTeamMode, + donateTroops: isTeamMode, // Standard settings infiniteGold: false, infiniteTroops: false, instantBuild: false, - randomSpawn: true, + randomSpawn: isFFA, maxTimerValue: undefined, // No disabled units in ranked disabledUnits: [], // Team configuration - playerTeams: matchConfig.teamConfig, + playerTeams: teamConfig, }; } diff --git a/src/server/Worker.ts b/src/server/Worker.ts index fd7256e51..2059f0c00 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -82,12 +82,13 @@ export async function startWorker() { config, log, async (gameId, assignment) => { - // Select map based on player count + // Select map based on player count and mode const selectedMap = selectMapForRanked({ playerCount: assignment.config.playerCount, gameMode: assignment.config.gameMode === "ffa" ? GameMode.FFA : GameMode.Team, queueType: assignment.config.queueType, + matchMode: assignment.config.gameMode, }); // Build full game config