diff --git a/src/client/CrazyGamesSDK.ts b/src/client/CrazyGamesSDK.ts new file mode 100644 index 000000000..b00cfe7de --- /dev/null +++ b/src/client/CrazyGamesSDK.ts @@ -0,0 +1,205 @@ +declare global { + interface Window { + CrazyGames?: { + SDK: { + init: () => Promise; + game: { + gameplayStart: () => Promise; + gameplayStop: () => Promise; + happytime: () => Promise; + loadingStart: () => void; + loadingStop: () => void; + showInviteButton: (options: { + gameId: string | number; + [key: string]: string | number; + }) => string; + hideInviteButton: () => void; + getInviteParam: (paramName: string) => string | null; + }; + }; + }; + } +} + +export class CrazyGamesSDK { + private initialized = false; + private isGameplayActive = false; + + isOnCrazyGames(): boolean { + try { + // Check if we're in an iframe + if (window.self !== window.top) { + // Try to access parent URL + return window?.top?.location?.hostname.includes("crazygames") ?? false; + } + return false; + } catch (e) { + // If we get a cross-origin error, we're definitely iframed + // Check our own referrer as fallback + return document.referrer.includes("crazygames"); + } + } + + isReady(): boolean { + return this.isOnCrazyGames() && this.initialized; + } + + async maybeInit(): Promise { + if (this.initialized) { + console.warn("CrazyGames SDK already initialized"); + return; + } + + if (!this.isOnCrazyGames()) { + console.log("Not running on CrazyGames platform, not initializing SDK"); + return; + } + + // Wait for SDK to load + let attempts = 0; + while (typeof window.CrazyGames === "undefined" && attempts < 100) { + await new Promise((resolve) => setTimeout(resolve, 100)); + attempts++; + } + + if (typeof window.CrazyGames === "undefined") { + console.warn("CrazyGames SDK not available"); + return; + } + + try { + await window.CrazyGames.SDK.init(); + this.initialized = true; + console.log("CrazyGames SDK initialized"); + } catch (error) { + console.error("Failed to initialize CrazyGames SDK:", error); + } + } + + async gameplayStart(): Promise { + if (!this.isReady()) { + return; + } + + if (this.isGameplayActive) { + console.warn("Gameplay already started"); + return; + } + + try { + await window.CrazyGames!.SDK.game.gameplayStart(); + this.isGameplayActive = true; + console.log("CrazyGames: gameplay started"); + } catch (error) { + console.error("Failed to report gameplay start:", error); + } + } + + async gameplayStop(): Promise { + if (!this.isReady()) { + return; + } + + if (!this.isGameplayActive) { + return; + } + + try { + await window.CrazyGames!.SDK.game.gameplayStop(); + this.isGameplayActive = false; + console.log("CrazyGames: gameplay stopped"); + } catch (error) { + console.error("Failed to report gameplay stop:", error); + } + } + + async happytime(): Promise { + if (!this.isReady()) { + return; + } + + try { + await window.CrazyGames!.SDK.game.happytime(); + console.log("CrazyGames: happy time triggered"); + } catch (error) { + console.error("Failed to trigger happy time:", error); + } + } + + loadingStart(): void { + if (!this.isReady()) { + return; + } + + try { + window.CrazyGames!.SDK.game.loadingStart(); + console.log("CrazyGames: loading started"); + } catch (error) { + console.error("Failed to report loading start:", error); + } + } + + loadingStop(): void { + if (!this.isReady()) { + return; + } + + try { + window.CrazyGames!.SDK.game.loadingStop(); + console.log("CrazyGames: loading stopped"); + } catch (error) { + console.error("Failed to report loading stop:", error); + } + } + + showInviteButton(gameId: string): string | null { + if (!this.isReady()) { + return null; + } + + try { + const options: { + gameId: string | number; + [key: string]: string | number; + } = { + gameId, + }; + const link = window.CrazyGames!.SDK.game.showInviteButton(options); + console.log("CrazyGames: invite button shown, link:", link); + return link; + } catch (error) { + console.error("Failed to show invite button:", error); + return null; + } + } + + hideInviteButton(): void { + if (!this.isReady()) { + return; + } + + try { + window.CrazyGames!.SDK.game.hideInviteButton(); + console.log("CrazyGames: invite button hidden"); + } catch (error) { + console.error("Failed to hide invite button:", error); + } + } + + getInviteGameId(): string | null { + if (!this.isReady()) { + return null; + } + + try { + const value = window.CrazyGames!.SDK.game.getInviteParam("gameId"); + console.log(`CrazyGames: got invite gameId:`, value); + return value; + } catch (error) { + console.error(`Failed to get invite gameId:`, error); + return null; + } + } +} + +export const crazyGamesSDK = new CrazyGamesSDK(); diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 09d840048..247ebe441 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -28,6 +28,7 @@ import "./components/Difficulties"; import "./components/FluentSlider"; import "./components/LobbyTeamView"; import "./components/Maps"; +import { crazyGamesSDK } from "./CrazyGamesSDK"; import { JoinLobbyEvent } from "./Main"; import { terrainMapFileLoader } from "./TerrainMapFileLoader"; import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions"; @@ -36,6 +37,7 @@ export class HostLobbyModal extends LitElement { @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; + onClose?: () => void; }; @state() private selectedMap: GameMapType = GameMapType.World; @state() private selectedDifficulty: Difficulty = Difficulty.Medium; @@ -600,7 +602,7 @@ export class HostLobbyModal extends LitElement { createLobby(this.lobbyCreatorClientID) .then((lobby) => { this.lobbyId = lobby.gameID; - // join lobby + crazyGamesSDK.showInviteButton(this.lobbyId); }) .then(() => { this.dispatchEvent( @@ -615,11 +617,16 @@ export class HostLobbyModal extends LitElement { ); }); this.modalEl?.open(); + this.modalEl.onClose = () => { + this.close(); + }; this.playersInterval = setInterval(() => this.pollPlayers(), 1000); this.loadNationCount(); } public close() { + console.log("Closing host lobby modal"); + crazyGamesSDK.hideInviteButton(); this.modalEl?.close(); this.copySuccess = false; if (this.playersInterval) { @@ -822,7 +829,6 @@ export class HostLobbyModal extends LitElement { private async copyToClipboard() { try { - //TODO: Convert id to url and copy await navigator.clipboard.writeText( `${location.origin}/#join=${this.lobbyId}`, ); @@ -845,8 +851,6 @@ export class HostLobbyModal extends LitElement { }) .then((response) => response.json()) .then((data: GameInfo) => { - console.log(`got game info response: ${JSON.stringify(data)}`); - this.clients = data.clients ?? []; }); } diff --git a/src/client/Main.ts b/src/client/Main.ts index 75d54c3a3..13573896d 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -12,6 +12,7 @@ import { getUserMe } from "./Api"; import { userAuth } from "./Auth"; import { joinLobby } from "./ClientGameRunner"; import { fetchCosmetics } from "./Cosmetics"; +import { crazyGamesSDK } from "./CrazyGamesSDK"; import "./DarkModeButton"; import { DarkModeButton } from "./DarkModeButton"; import "./FlagInput"; @@ -116,6 +117,7 @@ class Client { constructor() {} async initialize(): Promise { + crazyGamesSDK.maybeInit(); // Prefetch turnstile token so it is available when // the user joins a lobby. this.turnstileTokenPromise = getTurnstileToken(); @@ -162,10 +164,11 @@ class Client { this.publicLobby = document.querySelector("public-lobby") as PublicLobby; - window.addEventListener("beforeunload", () => { + window.addEventListener("beforeunload", async () => { console.log("Browser is closing"); if (this.gameStop !== null) { this.gameStop(); + await crazyGamesSDK.gameplayStop(); } }); @@ -348,7 +351,7 @@ class Client { } // Attempt to join lobby - this.handleHash(); + this.handleUrl(); const onHashUpdate = () => { // Reset the UI to its initial state @@ -358,7 +361,7 @@ class Client { } // Attempt to join lobby - this.handleHash(); + this.handleUrl(); }; // Handle browser navigation & manual hash edits @@ -385,7 +388,17 @@ class Client { this.initializeFuseTag(); } - private handleHash() { + private handleUrl() { + // 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) { + this.joinModal.open(lobbyId); + console.log(`CrazyGames: joining lobby ${lobbyId} from invite param`); + return; + } + } + const strip = () => history.replaceState( null, @@ -454,6 +467,7 @@ 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) { @@ -551,6 +565,8 @@ class Client { document.documentElement.classList.add("in-game"); removeSnowflakes(); // Stop snowflakes when joining a game + crazyGamesSDK.loadingStart(); + // show when the game loads const startingModal = document.querySelector( "game-starting-modal", @@ -569,6 +585,9 @@ class Client { (ad as HTMLElement).style.display = "none"; }); + crazyGamesSDK.loadingStop(); + crazyGamesSDK.gameplayStart(); + // 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"); @@ -585,6 +604,9 @@ class Client { console.log("leaving lobby, cancelling game"); this.gameStop(); this.gameStop = null; + + crazyGamesSDK.gameplayStop(); + this.gutterAds.hide(); this.publicLobby.leaveLobby(); // Show snowflakes when leaving lobby (back to homepage) diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts index 0fd6592d9..8f89812f5 100644 --- a/src/client/components/baseComponents/Modal.ts +++ b/src/client/components/baseComponents/Modal.ts @@ -8,6 +8,7 @@ export class OModal extends LitElement { @property({ type: String }) title = ""; @property({ type: String }) translationKey = ""; @property({ type: Boolean }) alwaysMaximized = false; + @property({ type: Function }) onClose?: () => void; static styles = css` .c-modal { @@ -75,10 +76,10 @@ export class OModal extends LitElement { this.isModalOpen = true; } public close() { - this.isModalOpen = false; - this.dispatchEvent( - new CustomEvent("modal-close", { bubbles: true, composed: true }), - ); + if (this.isModalOpen) { + this.isModalOpen = false; + this.onClose?.(); + } } render() { diff --git a/src/client/graphics/layers/AdTimer.ts b/src/client/graphics/layers/AdTimer.ts index 2d5462275..367744df9 100644 --- a/src/client/graphics/layers/AdTimer.ts +++ b/src/client/graphics/layers/AdTimer.ts @@ -1,7 +1,7 @@ import { GameView } from "../../../core/game/GameView"; import { Layer } from "./Layer"; -const AD_SHOW_TICKS = 5 * 60 * 10; // 5 minutes +const AD_SHOW_TICKS = 2 * 60 * 10; // 2 minute export class AdTimer implements Layer { private isHidden: boolean = false; diff --git a/src/client/graphics/layers/GameRightSidebar.ts b/src/client/graphics/layers/GameRightSidebar.ts index 394668e03..d73c47360 100644 --- a/src/client/graphics/layers/GameRightSidebar.ts +++ b/src/client/graphics/layers/GameRightSidebar.ts @@ -9,6 +9,7 @@ import { EventBus } from "../../../core/EventBus"; import { GameType } from "../../../core/game/Game"; import { GameUpdateType } from "../../../core/game/GameUpdates"; import { GameView } from "../../../core/game/GameView"; +import { crazyGamesSDK } from "../../CrazyGamesSDK"; import { PauseGameEvent } from "../../Transport"; import { translateText } from "../../Utils"; import { Layer } from "./Layer"; @@ -106,8 +107,10 @@ export class GameRightSidebar extends LitElement implements Layer { ); if (!isConfirmed) return; } - // redirect to the home page - window.location.href = "/"; + crazyGamesSDK.gameplayStop().then(() => { + // redirect to the home page + window.location.href = "/"; + }); } private onSettingsButtonClick() { diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 8de7b4abf..d044fc3e5 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -1,6 +1,5 @@ import { LitElement, TemplateResult, html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import ofmWintersLogo from "../../../../resources/images/OfmWintersLogo.png"; import { getGamesPlayed, isInIframe, @@ -17,6 +16,7 @@ import { handlePurchase, patternRelationship, } from "../../Cosmetics"; +import { crazyGamesSDK } from "../../CrazyGamesSDK"; import { SendWinnerEvent } from "../../Transport"; import { Layer } from "./Layer"; @@ -115,8 +115,6 @@ export class WinModal extends LitElement implements Layer { if (this.rand < 0.25) { return this.steamWishlist(); } else if (this.rand < 0.5) { - return this.ofmDisplay(); - } else if (this.rand < 0.75) { return this.discordDisplay(); } else { return this.renderPatternButton(); @@ -229,34 +227,6 @@ export class WinModal extends LitElement implements Layer {

`; } - ofmDisplay(): TemplateResult { - return html` -
-

- ${translateText("win_modal.ofm_winter")} -

-
- OpenFront Masters Winter -
-

- ${translateText("win_modal.ofm_winter_description")} -

- - ${translateText("win_modal.join_tournament")} - -
- `; - } - discordDisplay(): TemplateResult { return html`
@@ -324,6 +294,7 @@ export class WinModal extends LitElement implements Layer { if (wu.winner[1] === this.game.myPlayer()?.team()) { this._title = translateText("win_modal.your_team"); this.isWin = true; + crazyGamesSDK.happytime(); } else { this._title = translateText("win_modal.other_team", { team: wu.winner[1], @@ -346,6 +317,7 @@ export class WinModal extends LitElement implements Layer { ) { this._title = translateText("win_modal.you_won"); this.isWin = true; + crazyGamesSDK.happytime(); } else { this._title = translateText("win_modal.other_won", { player: winner.name(), diff --git a/src/client/index.html b/src/client/index.html index 112485910..868c8e1df 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -130,6 +130,12 @@ document.documentElement.className = "preload"; + + +