${segment("host_modal.visibility_private", false)}
${segment("host_modal.visibility_public", true)}
${this.renderAutoStartTimer()}
`;
}
// Countdown until the listed lobby starts automatically (hosts can't sit
// on a public listing). Hidden once the real start countdown is running —
// the Start button shows that one.
private renderAutoStartTimer() {
if (
!this.publiclyListed ||
this.autoStartAt === null ||
this.lobbyStartAt !== null
) {
return nothing;
}
const seconds = getSecondsUntilServerTimestamp(
this.autoStartAt,
this.serverTimeOffset,
);
return html`
this.kickPlayer(clientID)}
.onToggleNameReveal=${(clientID: string) =>
this.toggleNameReveal(clientID)}
.nameReveals=${this.nameReveals}
.anonymizeNames=${this.anonymizeNames}
>
${this.showSubscriptionRequired
? html`
(this.showSubscriptionRequired = false)}
@confirm=${() => {
this.showSubscriptionRequired = false;
window.location.href = "/#modal=store&tab=subscriptions";
}}
>`
: ""}
`;
}
protected onOpen(): void {
// Re-armed here (not in onClose's reset) so that once
// closeWithoutLeaving() disarms it, no close cascade — e.g. another
// modal's close() navigating via showPage, which force-closes this one —
// can re-arm it and disconnect the host mid game-start.
this.leaveLobbyOnClose = true;
this.startLobbyUpdates();
void getUserMe().then((userMe) => {
// Dev skips the subscription gate (matching the server) so the
// listing flow is testable locally.
this.canListPublicly =
ClientEnv.env() === GameEnv.Dev ||
(userMe !== false && hasActiveSubscription(userMe));
});
// The server mints the game id, so we don't know it until createLobby
// resolves. clientID is assigned by the server when we join the lobby.
// Pass auth token for creator identification (server extracts persistentID from it)
createLobby()
.then(async (lobby) => {
this.lobbyId = lobby.gameID;
if (!isValidGameID(this.lobbyId)) {
throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
}
crazyGamesSDK.showInviteButton(this.lobbyId);
// Now that we have the id, build and copy the share link. If lobby
// creation fails, the catch below clears the clipboard.
const url = await this.constructUrl();
this.updateLobbyHistory(url);
await this.updateComplete;
void (this.querySelector("copy-button") as CopyButton)?.handleCopy();
})
.then(() => {
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
gameID: this.lobbyId,
source: "host",
} as JoinLobbyEvent,
bubbles: true,
composed: true,
}),
);
})
.catch(() => {
// Clear clipboard so the host doesn't accidentally share a dead link
void navigator.clipboard.writeText("").catch(() => {});
});
// BaseModal.firstUpdated() owns modalEl.onClose so the o-modal close path
// (backdrop / close button) runs confirmBeforeClose(). Don't override it
// here — doing so would bypass the leave-lobby confirmation.
this.loadNationCount();
}
private leaveLobby() {
if (!this.lobbyId) {
return;
}
this.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: this.lobbyId },
bubbles: true,
composed: true,
}),
);
}
// Close as part of the lobby -> game transition (e.g. auto-start of a
// listed lobby): the host is entering the game, not leaving the lobby, so
// closing must not disconnect them (the server would tear the lobby down).
// disarmLeaveOnClose is separate because closing ANY page-modal navigates
// via showPage, which force-closes the currently visible page — so all
// lobby modals must be disarmed before any of them is closed.
public disarmLeaveOnClose() {
this.leaveLobbyOnClose = false;
}
public closeWithoutLeaving() {
this.disarmLeaveOnClose();
this.close();
}
public confirmBeforeClose(): boolean | Promise