mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
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:
@@ -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
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -58,7 +58,6 @@ export interface ServerConfig {
|
||||
subdomain(): string;
|
||||
stripePublishableKey(): string;
|
||||
allowedFlares(): string[] | undefined;
|
||||
enableMatchmaking(): boolean;
|
||||
getRandomPublicGameModifiers(): PublicGameModifiers;
|
||||
supportsCompactMapForTeams(map: GameMapType): boolean;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user