mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:40:42 +00:00
fix
This commit is contained in:
@@ -332,12 +332,15 @@
|
||||
"teams": "{num} teams",
|
||||
"players_per_team": "of {num}",
|
||||
"started": "Started",
|
||||
"current": "Current public lobby",
|
||||
"vote_button": "Vote on maps",
|
||||
"vote_cta": "Vote for next map!",
|
||||
"vote_count_label": "{count, plural, one {# vote influences the next map} other {# votes influence the next map}}",
|
||||
"vote_count_tooltip": "Number of players currently voting (connected & waiting in public lobby)",
|
||||
"vote_title": "Map Voting",
|
||||
"vote_description": "Select any number of maps to influence the next public lobby.",
|
||||
"vote_saved": "Your selection is saved on this device.",
|
||||
"vote_login_required": "Log in to submit votes."
|
||||
"vote_login_required": "Log in to submit votes.",
|
||||
"vote_submit": "Vote",
|
||||
"vote_toast_submitted": "Vote submitted!"
|
||||
},
|
||||
"matchmaking_modal": {
|
||||
"title": "1v1 Ranked Matchmaking (ALPHA)",
|
||||
|
||||
@@ -3,12 +3,14 @@ import { GameInfo } from "../core/Schemas";
|
||||
|
||||
type LobbyUpdateHandler = (lobbies: GameInfo[]) => void;
|
||||
type VoteRequestHandler = () => void;
|
||||
type VoteStatsHandler = (activeVoteCount: number) => void;
|
||||
|
||||
interface LobbySocketOptions {
|
||||
reconnectDelay?: number;
|
||||
maxWsAttempts?: number;
|
||||
pollIntervalMs?: number;
|
||||
onVoteRequest?: VoteRequestHandler;
|
||||
onVoteStats?: VoteStatsHandler;
|
||||
}
|
||||
|
||||
export class PublicLobbySocket {
|
||||
@@ -23,6 +25,7 @@ export class PublicLobbySocket {
|
||||
private readonly pollIntervalMs: number;
|
||||
private readonly onLobbiesUpdate: LobbyUpdateHandler;
|
||||
private readonly onVoteRequest?: VoteRequestHandler;
|
||||
private readonly onVoteStats?: VoteStatsHandler;
|
||||
private pendingVote: { token: string; maps: GameMapType[] } | null = null;
|
||||
|
||||
constructor(
|
||||
@@ -34,6 +37,7 @@ export class PublicLobbySocket {
|
||||
this.maxWsAttempts = options?.maxWsAttempts ?? 3;
|
||||
this.pollIntervalMs = options?.pollIntervalMs ?? 1000;
|
||||
this.onVoteRequest = options?.onVoteRequest;
|
||||
this.onVoteStats = options?.onVoteStats;
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -87,6 +91,9 @@ export class PublicLobbySocket {
|
||||
this.onLobbiesUpdate(message.data?.lobbies ?? []);
|
||||
} else if (message.type === "map_vote_request") {
|
||||
this.onVoteRequest?.();
|
||||
} else if (message.type === "map_vote_stats") {
|
||||
const activeVoteCount = Number(message.data?.activeVoteCount ?? 0);
|
||||
this.onVoteStats?.(activeVoteCount);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error parsing WebSocket message:", error);
|
||||
|
||||
@@ -59,6 +59,30 @@ export class MapVoteModal extends BaseModal {
|
||||
);
|
||||
}
|
||||
|
||||
private handleVoteSubmit = () => {
|
||||
const maps = Array.from(this.selectedMaps);
|
||||
saveStoredMapVotes(maps);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("map-vote-submit", {
|
||||
detail: { maps },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: this.loggedIn
|
||||
? translateText("public_lobby.vote_toast_submitted")
|
||||
: translateText("public_lobby.vote_saved"),
|
||||
color: "green",
|
||||
duration: 2500,
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.close();
|
||||
};
|
||||
|
||||
private renderCategory(
|
||||
categoryKey: string,
|
||||
maps: GameMapType[],
|
||||
@@ -166,6 +190,25 @@ export class MapVoteModal extends BaseModal {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-end gap-3 px-6 py-4 border-t border-white/10"
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-xs font-bold uppercase tracking-widest rounded-lg bg-white/10 text-white/70 hover:text-white hover:bg-white/20 transition-colors"
|
||||
type="button"
|
||||
@click=${() => this.close()}
|
||||
>
|
||||
${translateText("common.cancel")}
|
||||
</button>
|
||||
<button
|
||||
class="px-5 py-2 text-xs font-bold uppercase tracking-widest rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition-colors"
|
||||
type="button"
|
||||
@click=${this.handleVoteSubmit}
|
||||
>
|
||||
${translateText("public_lobby.vote_submit")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export class PublicLobby extends LitElement {
|
||||
@state() private mapImages: Map<GameID, string> = new Map();
|
||||
@state() private joiningDotIndex: number = 0;
|
||||
@state() private isLoggedIn: boolean = false;
|
||||
@state() private activeVoteCount: number = 0;
|
||||
|
||||
private joiningInterval: number | null = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
@@ -40,6 +41,9 @@ export class PublicLobby extends LitElement {
|
||||
onVoteRequest: () => {
|
||||
void this.sendStoredVotes();
|
||||
},
|
||||
onVoteStats: (activeVoteCount) => {
|
||||
this.activeVoteCount = activeVoteCount;
|
||||
},
|
||||
},
|
||||
);
|
||||
private handleUserMeResponse = (event: Event) => {
|
||||
@@ -56,6 +60,11 @@ export class PublicLobby extends LitElement {
|
||||
if (!customEvent.detail?.maps) return;
|
||||
void this.handleMapVoteChange(customEvent);
|
||||
};
|
||||
private handleMapVoteSubmitEvent = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ maps: GameMapType[] }>;
|
||||
if (!customEvent.detail?.maps) return;
|
||||
void this.handleMapVoteSubmit(customEvent.detail.maps);
|
||||
};
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -65,6 +74,7 @@ export class PublicLobby extends LitElement {
|
||||
super.connectedCallback();
|
||||
document.addEventListener("userMeResponse", this.handleUserMeResponse);
|
||||
document.addEventListener("map-vote-change", this.handleMapVoteChangeEvent);
|
||||
document.addEventListener("map-vote-submit", this.handleMapVoteSubmitEvent);
|
||||
this.lobbySocket.start();
|
||||
}
|
||||
|
||||
@@ -75,6 +85,10 @@ export class PublicLobby extends LitElement {
|
||||
"map-vote-change",
|
||||
this.handleMapVoteChangeEvent,
|
||||
);
|
||||
document.removeEventListener(
|
||||
"map-vote-submit",
|
||||
this.handleMapVoteSubmitEvent,
|
||||
);
|
||||
this.lobbySocket.stop();
|
||||
this.stopJoiningAnimation();
|
||||
}
|
||||
@@ -95,6 +109,13 @@ export class PublicLobby extends LitElement {
|
||||
this.lobbySocket.sendMapVote(auth.jwt, event.detail.maps);
|
||||
}
|
||||
|
||||
private async handleMapVoteSubmit(maps: GameMapType[]) {
|
||||
if (!this.isLoggedIn) return;
|
||||
const auth = await userAuth();
|
||||
if (!auth) return;
|
||||
this.lobbySocket.sendMapVote(auth.jwt, maps);
|
||||
}
|
||||
|
||||
private openMapVoteModal() {
|
||||
const modal = document.querySelector("map-vote-modal") as
|
||||
| (HTMLElement & { open: () => void })
|
||||
@@ -177,10 +198,7 @@ export class PublicLobby extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="flex flex-col gap-2">
|
||||
<div
|
||||
class="flex items-center gap-2 text-xs uppercase tracking-widest text-white/60 px-1"
|
||||
>
|
||||
<span>${translateText("public_lobby.current")}</span>
|
||||
<div class="flex items-center gap-3 text-xs px-1">
|
||||
<button
|
||||
class="group flex items-center justify-center w-7 h-7 rounded-full border border-white/10 bg-slate-900/70 hover:bg-slate-800/90 text-white/80 hover:text-white transition-colors"
|
||||
@click=${this.openMapVoteModal}
|
||||
@@ -201,6 +219,20 @@ export class PublicLobby extends LitElement {
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs uppercase tracking-widest text-white/70">
|
||||
${translateText("public_lobby.vote_cta")}
|
||||
</span>
|
||||
<span
|
||||
class="text-[10px] text-white/50 normal-case tracking-normal"
|
||||
title=${translateText("public_lobby.vote_count_tooltip")}
|
||||
aria-label=${translateText("public_lobby.vote_count_tooltip")}
|
||||
>
|
||||
${translateText("public_lobby.vote_count_label", {
|
||||
count: this.activeVoteCount,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative">
|
||||
|
||||
@@ -131,6 +131,33 @@ function broadcastMapVoteRequest() {
|
||||
});
|
||||
}
|
||||
|
||||
function getActiveVoteCount(): number {
|
||||
return mapVotesByUser.size;
|
||||
}
|
||||
|
||||
function broadcastMapVoteStats() {
|
||||
const message = JSON.stringify({
|
||||
type: "map_vote_stats",
|
||||
data: { activeVoteCount: getActiveVoteCount() },
|
||||
});
|
||||
const clientsToRemove: WebSocket[] = [];
|
||||
|
||||
connectedClients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(message);
|
||||
} else if (
|
||||
client.readyState === WebSocket.CLOSED ||
|
||||
client.readyState === WebSocket.CLOSING
|
||||
) {
|
||||
clientsToRemove.push(client);
|
||||
}
|
||||
});
|
||||
|
||||
clientsToRemove.forEach((client) => {
|
||||
connectedClients.delete(client);
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeVotedMaps(maps: GameMapType[]): Set<GameMapType> {
|
||||
const unique = new Set<GameMapType>();
|
||||
maps.forEach((map) => {
|
||||
@@ -165,6 +192,7 @@ function unregisterVoteConnection(ws: WebSocket) {
|
||||
if (connections.size === 0) {
|
||||
mapVoteConnectionsByUser.delete(userId);
|
||||
mapVotesByUser.delete(userId);
|
||||
broadcastMapVoteStats();
|
||||
} else {
|
||||
mapVoteConnectionsByUser.set(userId, connections);
|
||||
}
|
||||
@@ -173,9 +201,11 @@ function unregisterVoteConnection(ws: WebSocket) {
|
||||
function setUserVote(userId: string, maps: Set<GameMapType>) {
|
||||
if (maps.size === 0) {
|
||||
mapVotesByUser.delete(userId);
|
||||
broadcastMapVoteStats();
|
||||
return;
|
||||
}
|
||||
mapVotesByUser.set(userId, maps);
|
||||
broadcastMapVoteStats();
|
||||
}
|
||||
|
||||
function collectMapVoteWeights(): Map<GameMapType, number> {
|
||||
@@ -190,6 +220,7 @@ function collectMapVoteWeights(): Map<GameMapType, number> {
|
||||
|
||||
function clearMapVotes() {
|
||||
mapVotesByUser.clear();
|
||||
broadcastMapVoteStats();
|
||||
}
|
||||
|
||||
async function handleLobbyMessage(ws: WebSocket, raw: WebSocket.RawData) {
|
||||
@@ -239,6 +270,12 @@ export async function startMaster() {
|
||||
ws.send(
|
||||
JSON.stringify({ type: "lobbies_update", data: publicLobbiesData }),
|
||||
);
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
type: "map_vote_stats",
|
||||
data: { activeVoteCount: getActiveVoteCount() },
|
||||
}),
|
||||
);
|
||||
|
||||
ws.on("close", () => {
|
||||
connectedClients.delete(ws);
|
||||
|
||||
Reference in New Issue
Block a user