Update & improve 1v1 Ranked Matchmaking (#2831)

references #2001

## Description:

Improve the ranked matchmaking modal. Better messages, and show 1v1 elo


<img width="450" height="210" alt="Screenshot 2026-01-08 at 7 11 20 PM"
src="https://github.com/user-attachments/assets/e4f8323c-5d98-48de-babe-b51526a6d408"
/>

<img width="622" height="614" alt="Screenshot 2026-01-08 at 7 11 14 PM"
src="https://github.com/user-attachments/assets/73d10f84-b5b5-4ba8-95bb-a181a9fd9dae"
/>



## 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
This commit is contained in:
Evan
2026-01-08 19:18:04 -08:00
committed by GitHub
parent c769e4a99a
commit 8ff3f4496c
8 changed files with 56 additions and 41 deletions
+2 -1
View File
@@ -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..."
+34 -18
View File
@@ -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<typeof setInterval> | 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")}"
>
<p class="text-center mt-4 mb-8">
${translateText("matchmaking_modal.elo", { elo: this.elo })}
</p>
${this.renderInner()}
</o-modal>
`;
@@ -41,12 +58,18 @@ export class MatchmakingModal extends LitElement {
private renderInner() {
if (!this.connected) {
return html`${translateText("matchmaking_modal.connecting")}`;
return html`<p class="text-center">
${translateText("matchmaking_modal.connecting")}
</p>`;
}
if (this.gameID === null) {
return html`${translateText("matchmaking_modal.searching")}`;
return html`<p class="text-center">
${translateText("matchmaking_modal.searching")}
</p>`;
} else {
return html`${translateText("matchmaking_modal.waiting_for_game")}`;
return html`<p class="text-center">
${translateText("matchmaking_modal.waiting_for_game")}
</p>`;
}
}
@@ -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`
<div class="z-9999">
<button
<o-button
@click="${this.open}"
class="w-full h-16 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-2xl hover:shadow-2xl transition-all duration-200 flex items-center justify-center text-xl focus:outline-hidden focus:ring-4 focus:ring-blue-500 focus:ring-offset-4"
title="${translateText("matchmaking_modal.title")}"
>
Matchmaking
</button>
translationKey="matchmaking_modal.title"
block
secondary
></o-button>
</div>
<matchmaking-modal></matchmaking-modal>
`;
+9
View File
@@ -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<typeof UserMeResponseSchema>;
+1
View File
@@ -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();
-1
View File
@@ -58,7 +58,6 @@ export interface ServerConfig {
subdomain(): string;
stripePublishableKey(): string;
allowedFlares(): string[] | undefined;
enableMatchmaking(): boolean;
getRandomPublicGameModifiers(): PublicGameModifiers;
supportsCompactMapForTeams(map: GameMapType): boolean;
}
-3
View File
@@ -222,9 +222,6 @@ export abstract class DefaultServerConfig implements ServerConfig {
workerPortByIndex(index: number): number {
return 3001 + index;
}
enableMatchmaking(): boolean {
return false;
}
getRandomPublicGameModifiers(): PublicGameModifiers {
return {
+10 -15
View File
@@ -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}`);
-3
View File
@@ -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.");
}