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")}
-
-
-

-
-
- ${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";
+
+
+