From 8ff3f4496cc232a0ca1c967eb6db7893095661d0 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 8 Jan 2026 19:18:04 -0800 Subject: [PATCH] Update & improve 1v1 Ranked Matchmaking (#2831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit references #2001 ## Description: Improve the ranked matchmaking modal. Better messages, and show 1v1 elo Screenshot 2026-01-08 at 7 11 20 PM Screenshot 2026-01-08 at 7 11 14 PM ## Please complete the following: - [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 - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: evan --- resources/lang/en.json | 3 +- src/client/Matchmaking.ts | 52 ++++++++++++++++--------- src/core/ApiSchemas.ts | 9 +++++ src/core/Schemas.ts | 1 + src/core/configuration/Config.ts | 1 - src/core/configuration/DefaultConfig.ts | 3 -- src/server/Worker.ts | 25 +++++------- tests/util/TestServerConfig.ts | 3 -- 8 files changed, 56 insertions(+), 41 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 4ec742af3..b20999535 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -292,7 +292,8 @@ "players_per_team": "of {num}" }, "matchmaking_modal": { - "title": "Matchmaking", + "title": "1v1 Ranked Matchmaking (ALPHA)", + "elo": "ELO: {elo}", "connecting": "Connecting to matchmaking server...", "searching": "Searching for game...", "waiting_for_game": "Waiting for game to start..." diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts index e36ce18c4..c7c26a631 100644 --- a/src/client/Matchmaking.ts +++ b/src/client/Matchmaking.ts @@ -1,5 +1,6 @@ import { html, LitElement } from "lit"; import { customElement, query, state } from "lit/decorators.js"; +import { UserMeResponse } from "src/core/ApiSchemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { generateID } from "../core/Util"; import { getPlayToken } from "./Auth"; @@ -12,16 +13,29 @@ import { translateText } from "./Utils"; export class MatchmakingModal extends LitElement { private gameCheckInterval: ReturnType | null = null; private connected = false; + private elo = "unknown"; @state() private socket: WebSocket | null = null; @state() private gameID: string | null = null; @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; + onClose?: () => void; + isModalOpen: boolean; }; constructor() { super(); + document.addEventListener("userMeResponse", (event: Event) => { + const customEvent = event as CustomEvent; + if (customEvent.detail) { + const userMeResponse = customEvent.detail as UserMeResponse; + this.elo = + userMeResponse.player?.leaderboard?.oneVone?.elo?.toString() ?? + "unknown"; + this.requestUpdate(); + } + }); } createRenderRoot() { @@ -34,6 +48,9 @@ export class MatchmakingModal extends LitElement { id="matchmaking-modal" title="${translateText("matchmaking_modal.title")}" > +

+ ${translateText("matchmaking_modal.elo", { elo: this.elo })} +

${this.renderInner()} `; @@ -41,12 +58,18 @@ export class MatchmakingModal extends LitElement { private renderInner() { if (!this.connected) { - return html`${translateText("matchmaking_modal.connecting")}`; + return html`

+ ${translateText("matchmaking_modal.connecting")} +

`; } if (this.gameID === null) { - return html`${translateText("matchmaking_modal.searching")}`; + return html`

+ ${translateText("matchmaking_modal.searching")} +

`; } else { - return html`${translateText("matchmaking_modal.waiting_for_game")}`; + return html`

+ ${translateText("matchmaking_modal.waiting_for_game")} +

`; } } @@ -64,8 +87,8 @@ export class MatchmakingModal extends LitElement { }, 1000); this.socket?.send( JSON.stringify({ - type: "auth", - playToken: await getPlayToken(), + type: "join", + jwt: await getPlayToken(), }), ); }; @@ -97,6 +120,7 @@ export class MatchmakingModal extends LitElement { } public async open() { + this.modalEl!.onClose = () => this.close(); this.modalEl?.open(); this.requestUpdate(); this.connect(); @@ -148,7 +172,6 @@ export class MatchmakingModal extends LitElement { @customElement("matchmaking-button") export class MatchmakingButton extends LitElement { @query("matchmaking-modal") private matchmakingModal: MatchmakingModal; - @state() private matchmakingEnabled = false; constructor() { super(); @@ -156,8 +179,6 @@ export class MatchmakingButton extends LitElement { async connectedCallback() { super.connectedCallback(); - const config = await getServerConfigFromClient(); - this.matchmakingEnabled = config.enableMatchmaking(); } createRenderRoot() { @@ -165,19 +186,14 @@ export class MatchmakingButton extends LitElement { } render() { - if (!this.matchmakingEnabled) { - return html``; - } - return html`
- + translationKey="matchmaking_modal.title" + block + secondary + >
`; diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 6461027ed..c8056464e 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -64,6 +64,15 @@ export const UserMeResponseSchema = z.object({ }), ) .optional(), + leaderboard: z + .object({ + oneVone: z + .object({ + elo: z.number().optional(), + }) + .optional(), + }) + .optional(), }), }); export type UserMeResponse = z.infer; diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 4a9878786..15927fa56 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -189,6 +189,7 @@ export const GameConfigSchema = z.object({ spawnImmunityDuration: z.number().int().min(0).optional(), // In ticks disabledUnits: z.enum(UnitType).array().optional(), playerTeams: TeamCountConfigSchema.optional(), + isOneVOne: z.boolean().optional(), }); export const TeamSchema = z.string(); diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts index 3d3920c1a..58a83f68b 100644 --- a/src/core/configuration/Config.ts +++ b/src/core/configuration/Config.ts @@ -58,7 +58,6 @@ export interface ServerConfig { subdomain(): string; stripePublishableKey(): string; allowedFlares(): string[] | undefined; - enableMatchmaking(): boolean; getRandomPublicGameModifiers(): PublicGameModifiers; supportsCompactMapForTeams(map: GameMapType): boolean; } diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts index 6dd76f5cd..e9e2a33c9 100644 --- a/src/core/configuration/DefaultConfig.ts +++ b/src/core/configuration/DefaultConfig.ts @@ -222,9 +222,6 @@ export abstract class DefaultServerConfig implements ServerConfig { workerPortByIndex(index: number): number { return 3001 + index; } - enableMatchmaking(): boolean { - return false; - } getRandomPublicGameModifiers(): PublicGameModifiers { return { diff --git a/src/server/Worker.ts b/src/server/Worker.ts index ae3a6870f..1dff7aaaf 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -8,7 +8,7 @@ import { fileURLToPath } from "url"; import { WebSocket, WebSocketServer } from "ws"; import { z } from "zod"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; -import { GameType } from "../core/game/Game"; +import { GameMapSize, GameType } from "../core/game/Game"; import { ClientMessageSchema, GameID, @@ -40,17 +40,12 @@ const playlist = new MapPlaylist(true); export async function startWorker() { log.info(`Worker starting...`); - if (config.enableMatchmaking()) { - log.info("Starting matchmaking"); - setTimeout( - () => { - pollLobby(gm); - }, - 1000 + Math.random() * 2000, - ); - } else { - log.info("Matchmaking disabled"); - } + setTimeout( + () => { + pollLobby(gm); + }, + 1000 + Math.random() * 2000, + ); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -501,9 +496,9 @@ async function pollLobby(gm: GameManager) { log.info(`Lobby poll successful:`, data); if (data.assignment) { - // TODO: Only allow specified players to join the game. - console.log(`Creating game ${gameId}`); - const game = gm.createGame(gameId, playlist.gameConfig()); + const gameConfig = playlist.gameConfig(); + gameConfig.gameMapSize = GameMapSize.Compact; + const game = gm.createGame(gameId, gameConfig); setTimeout(() => { // Wait a few seconds to allow clients to connect. console.log(`Starting game ${gameId}`); diff --git a/tests/util/TestServerConfig.ts b/tests/util/TestServerConfig.ts index 3199e5b81..28b058726 100644 --- a/tests/util/TestServerConfig.ts +++ b/tests/util/TestServerConfig.ts @@ -10,9 +10,6 @@ export class TestServerConfig implements ServerConfig { turnstileSecretKey(): string { throw new Error("Method not implemented."); } - enableMatchmaking(): boolean { - throw new Error("Method not implemented."); - } apiKey(): string { throw new Error("Method not implemented."); }