import version from "resources/version.txt?raw"; import { UserMeResponse } from "../core/ApiSchemas"; import { EventBus } from "../core/EventBus"; import { GAME_ID_REGEX, GameInfo, GameRecord, GameStartInfo, PublicGameInfo, } from "../core/Schemas"; import { GameEnv } from "../core/configuration/Config"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { GameType } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; import "./AccountModal"; import { getUserMe } from "./Api"; import { userAuth } from "./Auth"; import { joinLobby, type JoinLobbyResult } from "./ClientGameRunner"; import { getPlayerCosmeticsRefs } from "./Cosmetics"; import { crazyGamesSDK } from "./CrazyGamesSDK"; import "./FlagInput"; import { FlagInput } from "./FlagInput"; import "./FlagInputModal"; import { FlagInputModal } from "./FlagInputModal"; import { GameInfoModal } from "./GameInfoModal"; import "./GameModeSelector"; import { GameModeSelector } from "./GameModeSelector"; import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; import { GutterAds } from "./GutterAds"; import { HelpModal } from "./HelpModal"; import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; import { JoinLobbyModal } from "./JoinLobbyModal"; import "./LangSelector"; import { LangSelector } from "./LangSelector"; import { initLayout } from "./Layout"; import "./LeaderboardModal"; import "./Matchmaking"; import { MatchmakingModal } from "./Matchmaking"; import { initNavigation } from "./Navigation"; import "./NewsModal"; import "./PatternInput"; import "./SinglePlayerModal"; import { StoreModal } from "./Store"; import "./TerritoryPatternsModal"; import { TerritoryPatternsModal } from "./TerritoryPatternsModal"; import { TokenLoginModal } from "./TokenLoginModal"; import { SendKickPlayerIntentEvent, SendUpdateGameConfigIntentEvent, } from "./Transport"; import { UserSettingModal } from "./UserSettingModal"; import "./UsernameInput"; import { genAnonUsername, UsernameInput } from "./UsernameInput"; import { getDiscordAvatarUrl, incrementGamesPlayed, isInIframe, translateText, } from "./Utils"; import "./components/DesktopNavBar"; import "./components/Footer"; import "./components/HomeFooterAd"; import "./components/MainLayout"; import "./components/MobileNavBar"; import "./components/PlayPage"; import "./components/RankedModal"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; import "./styles.css"; import "./styles/core/typography.css"; import "./styles/core/variables.css"; import "./styles/layout/container.css"; import "./styles/layout/header.css"; import "./styles/modal/chat.css"; function updateAccountNavButton(userMeResponse: UserMeResponse | false) { const button = document.getElementById("nav-account-button"); if (!button) return; const avatarEl = document.getElementById("nav-account-avatar") as | (HTMLImageElement & { _navToken?: symbol }) | null; const personIconEl = document.getElementById( "nav-account-person-icon", ) as SVGElement | null; const emailBadgeEl = document.getElementById( "nav-account-email-badge", ) as HTMLElement | null; const signInTextEl = document.getElementById( "nav-account-signin-text", ) as HTMLSpanElement | null; // Unique token for this update call const navToken = Symbol(); if (avatarEl) avatarEl._navToken = navToken; const showAvatar = (src: string, alt?: string) => { if (avatarEl) { avatarEl.alt = alt ?? translateText("main.discord_avatar_alt"); // If the avatar fails to load (bad URL / CDN issue / offline), fall back // to the default sign-in UI instead of leaving a broken image. avatarEl.onerror = () => { // Only handle if this is the latest update if (avatarEl._navToken !== navToken) return; avatarEl.src = ""; // If the user is still logged in via email, show the email badge state. const email = userMeResponse !== false ? userMeResponse.user.email : undefined; if (email) { showEmailLoggedIn(); } else { showSignIn(); } }; avatarEl.onload = () => { // Only handle if this is the latest update if (avatarEl._navToken !== navToken) return; // Clear error handler after a successful load. avatarEl.onerror = null; }; avatarEl.src = src; avatarEl.classList.remove("hidden"); } personIconEl?.classList.add("hidden"); emailBadgeEl?.classList.add("hidden"); signInTextEl?.classList.add("hidden"); button?.classList.remove("border", "border-white/20"); }; const showSignIn = () => { avatarEl?.classList.add("hidden"); personIconEl?.classList.remove("hidden"); emailBadgeEl?.classList.add("hidden"); signInTextEl?.classList.remove("hidden"); // Restore border when showing signin state button?.classList.add("border", "border-white/20"); }; const showEmailLoggedIn = () => { avatarEl?.classList.add("hidden"); personIconEl?.classList.remove("hidden"); emailBadgeEl?.classList.remove("hidden"); signInTextEl?.classList.add("hidden"); button?.classList.add("border", "border-white/20"); }; const discord = userMeResponse !== false ? userMeResponse.user.discord : undefined; if (discord && avatarEl) { const avatarAlt = translateText("main.user_avatar_alt", { username: discord.username, }); const url = getDiscordAvatarUrl(discord); if (url) { showAvatar(url, avatarAlt); return; } } const email = userMeResponse !== false ? userMeResponse.user.email : undefined; if (email) { showEmailLoggedIn(); return; } showSignIn(); } declare global { interface Window { GIT_COMMIT: string; INSTANCE_ID: string; turnstile: any; adsEnabled: boolean; PageOS: { session: { newPageView: () => void; }; }; ramp: { que: Array<() => void>; passiveMode: boolean; spaAddAds: (ads: Array<{ type: string; selectorId?: string }>) => void; destroyUnits: (adType: string | string[]) => Promise; settings?: { slots?: any; }; spaNewPage: (url?: string) => void; spaAds: (config?: { ads?: Array<{ type: string; selectorId?: string }>; countPageview?: boolean; path?: string; }) => void; // Video ad methods onPlayerReady: (() => void) | null; addUnits: (units: Array<{ type: string }>) => Promise; displayUnits: () => void; }; Bolt: { on: (unitType: string, event: string, callback: () => void) => void; BOLT_AD_REQUEST_START: string; BOLT_AD_IMPRESSION: string; BOLT_AD_STARTED: string; BOLT_FIRST_QUARTILE: string; BOLT_MIDPOINT: string; BOLT_THIRD_QUARTILE: string; BOLT_AD_COMPLETE: string; BOLT_AD_ERROR: string; BOLT_AD_PAUSED: string; BOLT_AD_CLICKED: string; SHOW_HIDDEN_CONTAINER: string; }; currentPageId?: string; showPage?: (pageId: string) => void; } // Extend the global interfaces to include your custom events interface DocumentEventMap { "join-lobby": CustomEvent; "kick-player": CustomEvent; "join-changed": CustomEvent; "open-matchmaking": CustomEvent; } } export interface JoinLobbyEvent { // Multiplayer games only have gameID, gameConfig is not known until game starts. gameID: string; // GameConfig only exists when playing a singleplayer game. gameStartInfo?: GameStartInfo; // GameRecord exists when replaying an archived game. gameRecord?: GameRecord; source?: "public" | "private" | "host" | "matchmaking" | "singleplayer"; publicLobbyInfo?: GameInfo | PublicGameInfo; } class Client { private lobbyHandle: JoinLobbyResult | null = null; private eventBus: EventBus = new EventBus(); private currentUrl: string | null = null; private usernameInput: UsernameInput | null = null; private flagInput: FlagInput | null = null; private hostModal: HostPrivateLobbyModal; private joinModal: JoinLobbyModal; private gameModeSelector: GameModeSelector; private userSettings: UserSettings = new UserSettings(); private storeModal: StoreModal; private tokenLoginModal: TokenLoginModal; private matchmakingModal: MatchmakingModal; private gutterAds: GutterAds; private turnstileTokenPromise: Promise<{ token: string; createdAt: number; }> | null = null; async initialize(): Promise { crazyGamesSDK.maybeInit(); // Prefetch turnstile token so it is available when // the user joins a lobby. this.turnstileTokenPromise = getTurnstileToken(); // Wait for components to render before setting version await customElements.whenDefined("mobile-nav-bar"); await customElements.whenDefined("desktop-nav-bar"); const versionElements = document.querySelectorAll( "#game-version, .game-version-display", ); if (versionElements.length === 0) { console.warn("Game version element not found"); } else { const trimmed = version.trim(); const displayVersion = trimmed.startsWith("v") ? trimmed : `v${trimmed}`; versionElements.forEach((el) => { el.textContent = displayVersion; }); } const langSelector = document.querySelector( "lang-selector", ) as LangSelector; if (!langSelector) { console.warn("Lang selector element not found"); } this.flagInput = document.querySelector("flag-input") as FlagInput; if (!this.flagInput) { console.warn("Flag input element not found"); } this.usernameInput = document.querySelector( "username-input", ) as UsernameInput; if (!this.usernameInput) { console.warn("Username input element not found"); } this.gameModeSelector = document.querySelector( "game-mode-selector", ) as GameModeSelector; window.addEventListener("beforeunload", async () => { console.log("Browser is closing"); if (this.lobbyHandle !== null) { this.lobbyHandle.stop(true); await crazyGamesSDK.gameplayStop(); } }); const gutterAds = document.querySelector("gutter-ads"); if (!(gutterAds instanceof GutterAds)) throw new Error("Missing gutter-ads"); this.gutterAds = gutterAds; document.addEventListener("join-lobby", this.handleJoinLobby.bind(this)); document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this)); document.addEventListener("kick-player", this.handleKickPlayer.bind(this)); document.addEventListener( "update-game-config", this.handleUpdateGameConfig.bind(this), ); document.addEventListener( "open-matchmaking", this.handleOpenMatchmaking.bind(this), ); const hlpModal = document.querySelector("help-modal") as HelpModal; if (!hlpModal || !(hlpModal instanceof HelpModal)) { console.warn("Help modal element not found"); } const giModal = document.querySelector("game-info-modal") as GameInfoModal; if (!giModal || !(giModal instanceof GameInfoModal)) { console.warn("Game info modal element not found"); } const helpButton = document.getElementById("help-button"); if (helpButton) { helpButton.addEventListener("click", () => { if (hlpModal && hlpModal instanceof HelpModal) { hlpModal.open(); } }); } const flagInputModal = document.querySelector( "flag-input-modal", ) as FlagInputModal; if (!flagInputModal || !(flagInputModal instanceof FlagInputModal)) { console.warn("Flag input modal element not found"); } // Attach listener to any flag-input component (desktop or potentially others) document.querySelectorAll("flag-input").forEach((flagInput) => { flagInput.addEventListener("flag-input-click", () => { if (flagInputModal && flagInputModal instanceof FlagInputModal) { flagInputModal.open(); } }); }); this.storeModal = document.getElementById("page-item-store") as StoreModal; if (!this.storeModal || !(this.storeModal instanceof StoreModal)) { console.warn("Store modal element not found"); } const patternsModal = document.getElementById( "territory-patterns-modal", ) as TerritoryPatternsModal; if (!patternsModal || !(patternsModal instanceof TerritoryPatternsModal)) { console.warn("Patterns modal element not found"); } // Attach listener to any pattern-input component document.querySelectorAll("pattern-input").forEach((patternInput) => { patternInput.addEventListener("pattern-input-click", () => { patternsModal.open(); }); }); if (isInIframe()) { const mobilePat = document.getElementById("pattern-input-mobile"); if (mobilePat) mobilePat.style.display = "none"; } if (!this.storeModal || !(this.storeModal instanceof StoreModal)) { console.warn("Store modal element not found"); } // We no longer need to manually manage the preview button as PatternInput handles it component-side. // However, we still want to ensure the modal can be opened. // The setupPatternInput above handles the click event for the new buttons. this.storeModal.refresh(); window.addEventListener("showPage", (e: any) => { if (typeof e?.detail === "string" && e.detail === "page-play") { setTimeout(() => { this.storeModal.refresh(); }, 50); } }); this.tokenLoginModal = document.querySelector( "token-login", ) as TokenLoginModal; if ( !this.tokenLoginModal || !(this.tokenLoginModal instanceof TokenLoginModal) ) { console.warn("Token login modal element not found"); } this.matchmakingModal = document.querySelector( "matchmaking-modal", ) as MatchmakingModal; if ( !this.matchmakingModal || !(this.matchmakingModal instanceof MatchmakingModal) ) { console.warn("Matchmaking modal element not found"); } const onUserMe = async (userMeResponse: UserMeResponse | false) => { updateAccountNavButton(userMeResponse); const hasLinkedAccount = !crazyGamesSDK.isOnCrazyGames() && ((userMeResponse || null)?.player?.flares?.length ?? 0) > 0; console.log("ads enabled: ", hasLinkedAccount); window.adsEnabled = !hasLinkedAccount && !crazyGamesSDK.isOnCrazyGames(); document.dispatchEvent( new CustomEvent("userMeResponse", { detail: userMeResponse, bubbles: true, cancelable: true, }), ); if (userMeResponse !== false) { // Authorized console.log( `Your player ID is ${userMeResponse.player.publicId}\n` + "Sharing this ID will allow others to view your game history and stats.", ); } }; if ((await userAuth()) === false) { // Not logged in onUserMe(false); } else { // JWT appears to be valid // TODO: Add caching getUserMe().then(onUserMe); } const settingsModal = document.querySelector( "user-setting", ) as UserSettingModal; if (!settingsModal || !(settingsModal instanceof UserSettingModal)) { console.warn("User settings modal element not found"); } document .getElementById("settings-button") ?.addEventListener("click", () => { if (settingsModal && settingsModal instanceof UserSettingModal) { settingsModal.open(); } }); this.hostModal = document.querySelector( "host-lobby-modal", ) as HostPrivateLobbyModal; if (!this.hostModal || !(this.hostModal instanceof HostPrivateLobbyModal)) { console.warn("Host private lobby modal element not found"); } else { this.hostModal.eventBus = this.eventBus; } this.joinModal = document.querySelector( "join-lobby-modal", ) as JoinLobbyModal; if (!this.joinModal || !(this.joinModal instanceof JoinLobbyModal)) { console.warn("Join lobby modal element not found"); } else { this.joinModal.eventBus = this.eventBus; } if (this.userSettings.darkMode()) { document.documentElement.classList.add("dark"); } else { document.documentElement.classList.remove("dark"); } // Attempt to join lobby if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => this.handleUrl()); } else { this.handleUrl(); } const onHashUpdate = () => { // Reset the UI to its initial state this.joinModal?.close(); onJoinChanged(); }; const onPopState = () => { if (this.currentUrl !== null && this.lobbyHandle !== null) { console.info("Game is active"); if (!this.lobbyHandle.stop()) { console.info("Player is active, ask before leaving game"); const isConfirmed = confirm( translateText("help_modal.exit_confirmation"), ); if (!isConfirmed) { // Rollback navigator history history.pushState(null, "", this.currentUrl); return; } } console.info("Player is not active, leave the game immediately"); crazyGamesSDK.gameplayStop().then(() => { // redirect to the home page window.location.href = "/"; }); } else { console.info("Game not active, handle hash update"); onHashUpdate(); } }; const onJoinChanged = () => { if (this.lobbyHandle !== null) { this.handleLeaveLobby(); } // Attempt to join lobby this.handleUrl(); }; // Handle browser navigation & manual hash edits window.addEventListener("popstate", onPopState); window.addEventListener("hashchange", onHashUpdate); window.addEventListener("join-changed", onJoinChanged); function updateSliderProgress(slider: HTMLInputElement) { const percent = ((Number(slider.value) - Number(slider.min)) / (Number(slider.max) - Number(slider.min))) * 100; slider.style.setProperty("--progress", `${percent}%`); } document .querySelectorAll( "#bots-count, #private-lobby-bots-count", ) .forEach((slider) => { updateSliderProgress(slider); slider.addEventListener("input", () => updateSliderProgress(slider)); }); } private async handleUrl() { // Wait for modal custom elements to be defined await Promise.all([ customElements.whenDefined("join-lobby-modal"), customElements.whenDefined("host-lobby-modal"), ]); // Check if CrazyGames SDK is enabled first (no hash needed in CrazyGames) if (crazyGamesSDK.isOnCrazyGames()) { const lobbyId = await crazyGamesSDK.getInviteGameId(); console.log("got game id", lobbyId); if (lobbyId && GAME_ID_REGEX.test(lobbyId)) { console.log("game parsed successfully"); // Wait 2 seconds to ensure all elements are actually loaded, // On low end-chromebooks the join modal was not registered in time. await new Promise((resolve) => setTimeout(resolve, 2000)); window.showPage?.("page-join-lobby"); this.joinModal?.open(lobbyId); console.log(`CrazyGames: joining lobby ${lobbyId} from invite param`); return; } } crazyGamesSDK.isInstantMultiplayer().then((isInstant) => { if (isInstant) { console.log( `CrazyGames: joining instant multiplayer lobby from CrazyGames`, ); this.hostModal.open(); } }); const strip = () => history.replaceState( null, "", window.location.pathname + window.location.search, ); const alertAndStrip = (message: string) => { alert(message); strip(); }; const hash = window.location.hash; // Decode the hash first to handle encoded characters const decodedHash = decodeURIComponent(hash); const params = new URLSearchParams(decodedHash.split("?")[1] || ""); // Handle different hash sections if (decodedHash.startsWith("#purchase-completed")) { // Parse params after the ? const status = params.get("status"); if (status !== "true") { alertAndStrip("purchase failed"); return; } const cosmeticName = params.get("cosmetic"); if (!cosmeticName) { alert("Something went wrong. Please contact support."); console.error("purchase-completed but no pattern name"); return; } const setCosmetic = () => { if (cosmeticName.startsWith("pattern:")) { this.userSettings.setSelectedPatternName(cosmeticName); } else if (cosmeticName.startsWith("flag:")) { this.userSettings.setFlag(cosmeticName); } }; const token = params.get("login-token"); if (token) { strip(); window.addEventListener("beforeunload", () => { // The page reloads after token login, so we need to save the pattern name // in case it is unset during reload. setCosmetic(); }); this.tokenLoginModal.openWithToken(token); } else { alertAndStrip(`purchase succeeded: ${cosmeticName}`); setCosmetic(); this.storeModal.refresh(); } return; } if (decodedHash.startsWith("#token-login")) { const token = params.get("token-login"); if (!token) { alertAndStrip( `login failed! Please try again later or contact support.`, ); return; } strip(); this.tokenLoginModal.openWithToken(token); return; } 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-lobby"); this.joinModal.open(lobbyId); console.log(`joining lobby ${lobbyId}`); return; } if (decodedHash.startsWith("#affiliate=")) { const affiliateCode = decodedHash.replace("#affiliate=", ""); strip(); if (affiliateCode) { this.storeModal?.open(affiliateCode); } } if (decodedHash.startsWith("#refresh")) { window.location.href = "/"; } // Handle requeue parameter for ranked matchmaking const searchParams = new URLSearchParams(window.location.search); if (searchParams.has("requeue")) { // Remove only the requeue parameter, preserving other params and hash searchParams.delete("requeue"); const newUrl = window.location.pathname + (searchParams.toString() ? "?" + searchParams.toString() : "") + window.location.hash; history.replaceState(null, "", newUrl); // Wait for matchmaking button to be defined, then trigger its click handler. customElements.whenDefined("matchmaking-button").then(() => { const matchmakingButton = document.querySelector( "matchmaking-button button", ) as HTMLButtonElement | null; if (matchmakingButton) { matchmakingButton.click(); } else { console.warn( "Requeue requested, but matchmaking button not found in DOM.", ); } }); } } private async handleJoinLobby(event: CustomEvent) { const lobby = event.detail; console.log(`joining lobby ${lobby.gameID}`); if (this.lobbyHandle !== null) { console.log("joining lobby, stopping existing game"); this.lobbyHandle.stop(true); document.body.classList.remove("in-game"); } if (lobby.source === "public") { this.joinModal?.open(lobby.gameID, lobby.publicLobbyInfo); } const config = await getServerConfigFromClient(); // Only update URL immediately for private lobbies, not public ones if (lobby.source !== "public") { this.updateJoinUrlForShare(lobby.gameID, config); } this.lobbyHandle = joinLobby(this.eventBus, { gameID: lobby.gameID, serverConfig: config, cosmetics: await getPlayerCosmeticsRefs(), turnstileToken: await this.getTurnstileToken(lobby), playerName: this.usernameInput?.getCurrentUsername() ?? genAnonUsername(), gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info, gameRecord: lobby.gameRecord, }); this.lobbyHandle.prestart.then(() => { console.log("Closing modals"); document.getElementById("settings-button")?.classList.add("hidden"); if (this.usernameInput) { // fix edge case where username-validation-error is re-rendered and hidden tag removed this.usernameInput.validationError = ""; } document .getElementById("username-validation-error") ?.classList.add("hidden"); this.joinModal?.closeWithoutLeaving(); [ "single-player-modal", "host-lobby-modal", "game-starting-modal", "game-top-bar", "help-modal", "user-setting", "troubleshooting-modal", "territory-patterns-modal", "store-modal", "language-modal", "news-modal", "flag-input-modal", "account-button", "leaderboard-button", "token-login", "matchmaking-modal", "lang-selector", "gutter-ads", ].forEach((tag) => { const modal = document.querySelector(tag) as HTMLElement & { close?: () => void; isModalOpen?: boolean; }; if (modal?.close) { modal.close(); } else if (modal && "isModalOpen" in modal) { modal.isModalOpen = false; } }); this.gameModeSelector.stop(); document.querySelectorAll(".ad").forEach((ad) => { (ad as HTMLElement).style.display = "none"; }); crazyGamesSDK.loadingStart(); // show when the game loads const startingModal = document.querySelector( "game-starting-modal", ) as GameStartingModal; if (startingModal && startingModal instanceof GameStartingModal) { startingModal.show(); } }); this.lobbyHandle.join.then(() => { this.joinModal?.closeWithoutLeaving(); this.gameModeSelector.stop(); incrementGamesPlayed(); document.querySelectorAll(".ad").forEach((ad) => { (ad as HTMLElement).style.display = "none"; }); if (window.PageOS?.session?.newPageView) { window.PageOS.session.newPageView(); } crazyGamesSDK.loadingStop(); crazyGamesSDK.gameplayStart(); document.body.classList.add("in-game"); // Ensure there's a homepage entry in history before adding the lobby entry if (window.location.hash === "" || window.location.hash === "#") { history.replaceState(null, "", window.location.origin + "#refresh"); } const lobbyIdHidden = !this.userSettings.lobbyIdVisibility(); history.pushState( null, "", lobbyIdHidden ? "/streamer-mode" : `/${config.workerPath(lobby.gameID)}/game/${lobby.gameID}?live`, ); // Store current URL for popstate confirmation this.currentUrl = window.location.href; }); } private updateJoinUrlForShare( lobbyId: string, config: Awaited>, ) { const lobbyIdHidden = !this.userSettings.lobbyIdVisibility(); const targetUrl = lobbyIdHidden ? "/streamer-mode" : `/${config.workerPath(lobbyId)}/game/${lobbyId}`; const currentUrl = window.location.pathname; if (currentUrl !== targetUrl) { history.replaceState(null, "", targetUrl); } } private async handleLeaveLobby(event?: CustomEvent) { if (this.lobbyHandle === null) { return; } console.log("leaving lobby, cancelling game"); this.lobbyHandle.stop(true); this.lobbyHandle = null; this.currentUrl = null; try { history.replaceState(null, "", "/"); } catch (e) { console.warn("Failed to restore URL on leave:", e); } document.body.classList.remove("in-game"); if (this.joinModal.isOpen()) { this.joinModal.close(); if (event?.detail.cause === "full-lobby") { window.dispatchEvent( new CustomEvent("show-message", { detail: { message: translateText("public_lobby.join_timeout"), color: "red", duration: 3500, }, }), ); } } crazyGamesSDK.gameplayStop(); } private handleOpenMatchmaking(_event: CustomEvent) { this.matchmakingModal?.open(); } private handleKickPlayer(event: CustomEvent) { const { target } = event.detail; // Forward to eventBus if available if (this.eventBus) { this.eventBus.emit(new SendKickPlayerIntentEvent(target)); } } private handleUpdateGameConfig(event: CustomEvent) { const { config } = event.detail; // Forward to eventBus if available if (this.eventBus) { this.eventBus.emit(new SendUpdateGameConfigIntentEvent(config)); } } private async getTurnstileToken( lobby: JoinLobbyEvent, ): Promise { const config = await getServerConfigFromClient(); if ( config.env() === GameEnv.Dev || lobby.gameStartInfo?.config.gameType === GameType.Singleplayer ) { return null; } // Always request a new token on crazygames. if (this.turnstileTokenPromise === null || crazyGamesSDK.isOnCrazyGames()) { console.log("No prefetched turnstile token, getting new token"); return (await getTurnstileToken())?.token ?? null; } const token = await this.turnstileTokenPromise; // Clear promise so a new token is fetched next time this.turnstileTokenPromise = null; if (!token) { console.log("No turnstile token"); return null; } const tokenTTL = 3 * 60 * 1000; if (Date.now() < token.createdAt + tokenTTL) { console.log("Prefetched turnstile token is valid"); return token.token; } else { console.log("Turnstile token expired, getting new token"); return (await getTurnstileToken())?.token ?? null; } } } // Hide elements with no-crazygames class if on CrazyGames const hideCrazyGamesElements = () => { if (crazyGamesSDK.isOnCrazyGames()) { document.querySelectorAll(".no-crazygames").forEach((el) => { (el as HTMLElement).style.display = "none"; }); } }; // Initialize the client when the DOM is loaded const bootstrap = () => { initLayout(); new Client().initialize(); initNavigation(); // Hide elements immediately hideCrazyGamesElements(); // Also hide elements after a short delay to catch late-rendered components setTimeout(hideCrazyGamesElements, 100); setTimeout(hideCrazyGamesElements, 500); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bootstrap); } else { bootstrap(); } async function getTurnstileToken(): Promise<{ token: string; createdAt: number; }> { // Wait for Turnstile script to load (handles slow connections) let attempts = 0; while (typeof window.turnstile === "undefined" && attempts < 100) { await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } if (typeof window.turnstile === "undefined") { throw new Error("Failed to load Turnstile script"); } const config = await getServerConfigFromClient(); const widgetId = window.turnstile.render("#turnstile-container", { sitekey: config.turnstileSiteKey(), size: "normal", appearance: "interaction-only", theme: "light", }); return new Promise((resolve, reject) => { window.turnstile.execute(widgetId, { callback: (token: string) => { window.turnstile.remove(widgetId); console.log(`Turnstile token received: ${token}`); resolve({ token, createdAt: Date.now() }); }, "error-callback": (errorCode: string) => { window.turnstile.remove(widgetId); console.error(`Turnstile error: ${errorCode}`); alert(`Turnstile error: ${errorCode}. Please refresh and try again.`); reject(new Error(`Turnstile failed: ${errorCode}`)); }, }); }); }