{
- this.leaveLobby();
+ this.leaveLobbyOnClose = true;
this.close();
},
ariaLabel: translateText("common.back"),
@@ -821,9 +847,14 @@ export class HostLobbyModal extends BaseModal {
);
createLobby(this.lobbyCreatorClientID)
- .then((lobby) => {
+ .then(async (lobby) => {
this.lobbyId = lobby.gameID;
+ if (!isValidGameID(this.lobbyId)) {
+ throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
+ }
crazyGamesSDK.showInviteButton(this.lobbyId);
+ const url = await this.constructUrl();
+ this.updateHistory(url);
})
.then(() => {
this.dispatchEvent(
@@ -895,6 +926,10 @@ export class HostLobbyModal extends BaseModal {
protected onClose(): void {
console.log("Closing host lobby modal");
+ if (this.leaveLobbyOnClose) {
+ this.leaveLobby();
+ this.updateHistory("/"); // Reset URL to base
+ }
crazyGamesSDK.hideInviteButton();
// Clean up timers and resources
@@ -933,6 +968,8 @@ export class HostLobbyModal extends BaseModal {
this.lobbyCreatorClientID = "";
this.lobbyIdVisible = true;
this.nationCount = 0;
+
+ this.leaveLobbyOnClose = true;
}
private async handleSelectRandomMap() {
@@ -1075,6 +1112,8 @@ export class HostLobbyModal extends BaseModal {
const spawnImmunityTicks = this.spawnImmunityDurationMinutes
? this.spawnImmunityDurationMinutes * 60 * 10
: 0;
+ const url = await this.constructUrl();
+ this.updateHistory(url);
this.dispatchEvent(
new CustomEvent("update-game-config", {
detail: {
@@ -1134,6 +1173,10 @@ export class HostLobbyModal extends BaseModal {
console.log(
`Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
+
+ // If the modal closes as part of starting the game, do not leave the lobby
+ this.leaveLobbyOnClose = false;
+
const config = await getServerConfigFromClient();
const response = await fetch(
`${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`,
@@ -1144,12 +1187,17 @@ export class HostLobbyModal extends BaseModal {
},
},
);
+
+ if (!response.ok) {
+ this.leaveLobbyOnClose = true;
+ }
return response;
}
private async copyToClipboard() {
+ const url = await this.buildLobbyUrl();
await copyToClipboard(
- `${location.origin}/#join=${this.lobbyId}`,
+ url,
() => (this.copySuccess = true),
() => (this.copySuccess = false),
);
diff --git a/src/client/JoinPrivateLobbyModal.ts b/src/client/JoinPrivateLobbyModal.ts
index c61342cc8..cb65b428f 100644
--- a/src/client/JoinPrivateLobbyModal.ts
+++ b/src/client/JoinPrivateLobbyModal.ts
@@ -3,6 +3,7 @@ import { customElement, query, state } from "lit/decorators.js";
import { copyToClipboard, translateText } from "../client/Utils";
import {
ClientInfo,
+ GAME_ID_REGEX,
GameConfig,
GameInfo,
GameRecordSchema,
@@ -32,6 +33,8 @@ export class JoinPrivateLobbyModal extends BaseModal {
private playersInterval: NodeJS.Timeout | null = null;
private userSettings: UserSettings = new UserSettings();
+ private leaveLobbyOnClose = true;
+
updated(changedProperties: Map
) {
super.updated(changedProperties);
}
@@ -354,21 +357,10 @@ export class JoinPrivateLobbyModal extends BaseModal {
}
}
- protected onClose(): void {
- if (this.lobbyIdInput) this.lobbyIdInput.value = "";
- this.currentLobbyId = "";
- this.gameConfig = null;
- this.players = [];
- if (this.playersInterval) {
- clearInterval(this.playersInterval);
- this.playersInterval = null;
+ private leaveLobby() {
+ if (!this.currentLobbyId || !this.hasJoined) {
+ return;
}
- }
-
- public closeAndLeave() {
- this.close();
- this.hasJoined = false;
- this.message = "";
this.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: this.currentLobbyId },
@@ -378,16 +370,43 @@ export class JoinPrivateLobbyModal extends BaseModal {
);
}
+ protected onClose(): void {
+ if (this.lobbyIdInput) this.lobbyIdInput.value = "";
+ this.gameConfig = null;
+ this.players = [];
+ if (this.playersInterval) {
+ clearInterval(this.playersInterval);
+ this.playersInterval = null;
+ }
+ if (this.leaveLobbyOnClose) {
+ this.leaveLobby();
+ // Reset URL to base when modal closes
+ history.replaceState(null, "", window.location.origin + "/");
+ }
+
+ this.hasJoined = false;
+ this.message = "";
+ this.currentLobbyId = "";
+
+ this.leaveLobbyOnClose = true;
+ }
+
+ public closeAndLeave() {
+ this.leaveLobbyOnClose = true;
+ this.close();
+ }
+
private async copyToClipboard() {
+ const config = await getServerConfigFromClient();
await copyToClipboard(
- `${location.origin}/#join=${this.currentLobbyId}`,
+ `${location.origin}/${config.workerPath(this.currentLobbyId)}/game/${this.currentLobbyId}`,
() => (this.copySuccess = true),
() => (this.copySuccess = false),
);
}
private isValidLobbyId(value: string): boolean {
- return /^[a-zA-Z0-9]{8}$/.test(value);
+ return GAME_ID_REGEX.test(value);
}
private normalizeLobbyId(input: string): string | null {
@@ -403,16 +422,19 @@ export class JoinPrivateLobbyModal extends BaseModal {
}
private extractLobbyIdFromUrl(input: string): string {
- if (input.startsWith("http")) {
- if (input.includes("#join=")) {
- const params = new URLSearchParams(input.split("#")[1]);
- return params.get("join") ?? input;
- } else if (input.includes("join/")) {
- return input.split("join/")[1];
- } else {
- return input;
- }
- } else {
+ if (!input.startsWith("http")) {
+ return input;
+ }
+
+ try {
+ const url = new URL(input);
+ const match = url.pathname.match(/game\/([^/]+)/);
+ const candidate = match?.[1];
+ if (candidate && GAME_ID_REGEX.test(candidate)) return candidate;
+
+ return input;
+ } catch (error) {
+ console.warn("Failed to parse lobby URL", error);
return input;
}
}
@@ -502,6 +524,9 @@ export class JoinPrivateLobbyModal extends BaseModal {
this.message = "";
this.hasJoined = true;
+ // If the modal closes as part of joining the game, do not leave the lobby
+ this.leaveLobbyOnClose = false;
+
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
diff --git a/src/client/Main.ts b/src/client/Main.ts
index 732e0f37f..f2cdae39d 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -1,7 +1,7 @@
import version from "resources/version.txt?raw";
import { UserMeResponse } from "../core/ApiSchemas";
import { EventBus } from "../core/EventBus";
-import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
+import { GAME_ID_REGEX, GameRecord, GameStartInfo } from "../core/Schemas";
import { GameEnv } from "../core/configuration/Config";
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game";
@@ -193,6 +193,7 @@ declare global {
interface DocumentEventMap {
"join-lobby": CustomEvent;
"kick-player": CustomEvent;
+ "join-changed": CustomEvent;
}
}
@@ -607,6 +608,7 @@ class Client {
onHashUpdate();
});
window.addEventListener("hashchange", onHashUpdate);
+ window.addEventListener("join-changed", onHashUpdate);
function updateSliderProgress(slider: HTMLInputElement) {
const percent =
@@ -632,7 +634,7 @@ class Client {
// Check if CrazyGames SDK is enabled first (no hash needed in CrazyGames)
if (crazyGamesSDK.isOnCrazyGames()) {
const lobbyId = crazyGamesSDK.getInviteGameId();
- if (lobbyId && ID.safeParse(lobbyId).success) {
+ if (lobbyId && GAME_ID_REGEX.test(lobbyId)) {
window.showPage?.("page-join-private-lobby");
this.joinModal?.open(lobbyId);
console.log(`CrazyGames: joining lobby ${lobbyId} from invite param`);
@@ -708,14 +710,16 @@ class Client {
return;
}
- // Fallback to hash-based join for non-CrazyGames environments
- if (decodedHash.startsWith("#join=")) {
- const lobbyId = decodedHash.substring(6); // Remove "#join="
- if (lobbyId && ID.safeParse(lobbyId).success) {
- window.showPage?.("page-join-private-lobby");
- this.joinModal?.open(lobbyId);
- console.log(`joining lobby ${lobbyId}`);
- }
+ const pathMatch = window.location.pathname.match(
+ /^\/(?:w\d+\/)?game\/([^/]+)/,
+ );
+ const lobbyId =
+ pathMatch && GAME_ID_REGEX.test(pathMatch[1]) ? pathMatch[1] : null;
+ if (lobbyId) {
+ window.showPage?.("page-join-private-lobby");
+ this.joinModal.open(lobbyId);
+ console.log(`joining lobby ${lobbyId}`);
+ return;
}
if (decodedHash.startsWith("#affiliate=")) {
const affiliateCode = decodedHash.replace("#affiliate=", "");
@@ -738,6 +742,7 @@ class Client {
document.body.classList.remove("in-game");
}
const config = await getServerConfigFromClient();
+ this.updateJoinUrlForShare(lobby.gameID, config);
const pattern = this.userSettings.getSelectedPatternName(
await fetchCosmetics(),
@@ -778,15 +783,16 @@ class Client {
"host-lobby-modal",
"join-private-lobby-modal",
"game-starting-modal",
+ "game-top-bar",
"help-modal",
"user-setting",
-
"territory-patterns-modal",
"language-modal",
"news-modal",
"flag-input-modal",
+ "account-button",
+ "stats-button",
"token-login",
-
"matchmaking-modal",
"lang-selector",
].forEach((tag) => {
@@ -817,7 +823,7 @@ class Client {
this.gutterAds.hide();
},
() => {
- this.joinModal?.close();
+ this.joinModal.close();
this.publicLobby.stop();
incrementGamesPlayed();
@@ -833,11 +839,27 @@ class Client {
if (window.location.hash === "" || window.location.hash === "#") {
history.replaceState(null, "", window.location.origin + "#refresh");
}
- history.pushState(null, "", `#join=${lobby.gameID}`);
+ history.pushState(
+ null,
+ "",
+ `/${config.workerPath(lobby.gameID)}/game/${lobby.gameID}?live`,
+ );
},
);
}
+ private updateJoinUrlForShare(
+ lobbyId: string,
+ config: Awaited>,
+ ) {
+ const targetUrl = `/${config.workerPath(lobbyId)}/game/${lobbyId}`;
+ const currentUrl = window.location.pathname;
+
+ if (currentUrl !== targetUrl) {
+ history.replaceState(null, "", targetUrl);
+ }
+ }
+
private async handleLeaveLobby(/* event: CustomEvent */) {
if (this.gameStop === null) {
return;
diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts
index 47b948f87..baa877b4c 100644
--- a/src/client/components/baseComponents/Modal.ts
+++ b/src/client/components/baseComponents/Modal.ts
@@ -73,7 +73,7 @@ export class OModal extends LitElement {
? html`