crazy games integrations (#2675)

## Description:

Integrate with crazy games SDK

## Please complete the following:

- [ ] I have added screenshots for all UI updates
- [ ] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [ ] I have added relevant tests to the test directory
- [ ] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
Evan
2025-12-23 09:11:00 -08:00
committed by GitHub
parent 29d6cee4e1
commit a810e0ad34
7 changed files with 261 additions and 15 deletions
+205
View File
@@ -0,0 +1,205 @@
declare global {
interface Window {
CrazyGames?: {
SDK: {
init: () => Promise<void>;
game: {
gameplayStart: () => Promise<void>;
gameplayStop: () => Promise<void>;
happytime: () => Promise<void>;
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<void> {
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<void> {
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<void> {
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<void> {
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();
+8 -4
View File
@@ -27,6 +27,7 @@ import "./components/baseComponents/Modal";
import "./components/Difficulties";
import "./components/LobbyTeamView";
import "./components/Maps";
import { crazyGamesSDK } from "./CrazyGamesSDK";
import { JoinLobbyEvent } from "./Main";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { renderUnitTypeOptions } from "./utilities/RenderUnitTypeOptions";
@@ -35,6 +36,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;
@@ -607,7 +609,7 @@ export class HostLobbyModal extends LitElement {
createLobby(this.lobbyCreatorClientID)
.then((lobby) => {
this.lobbyId = lobby.gameID;
// join lobby
crazyGamesSDK.showInviteButton(this.lobbyId);
})
.then(() => {
this.dispatchEvent(
@@ -622,11 +624,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) {
@@ -828,7 +835,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}`,
);
@@ -851,8 +857,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 ?? [];
});
}
+26 -4
View File
@@ -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";
@@ -115,6 +116,7 @@ class Client {
constructor() {}
async initialize(): Promise<void> {
crazyGamesSDK.maybeInit();
// Prefetch turnstile token so it is available when
// the user joins a lobby.
this.turnstileTokenPromise = getTurnstileToken();
@@ -161,10 +163,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();
}
});
@@ -343,7 +346,7 @@ class Client {
}
// Attempt to join lobby
this.handleHash();
this.handleUrl();
const onHashUpdate = () => {
// Reset the UI to its initial state
@@ -353,7 +356,7 @@ class Client {
}
// Attempt to join lobby
this.handleHash();
this.handleUrl();
};
// Handle browser navigation & manual hash edits
@@ -380,7 +383,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,
@@ -449,6 +462,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) {
@@ -546,6 +560,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",
@@ -564,6 +580,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");
@@ -580,6 +599,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)
@@ -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() {
@@ -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() {
+3
View File
@@ -16,6 +16,7 @@ import {
handlePurchase,
patternRelationship,
} from "../../Cosmetics";
import { crazyGamesSDK } from "../../CrazyGamesSDK";
import { SendWinnerEvent } from "../../Transport";
import { Layer } from "./Layer";
@@ -293,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],
@@ -315,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(),
+9 -1
View File
@@ -130,6 +130,12 @@
document.documentElement.className = "preload";
</script>
<!-- CrazyGames SDK -->
<script
src="https://sdk.crazygames.com/crazygames-sdk-v3.js"
async
></script>
<!-- Cloudflare Turnstile -->
<script
src="https://challenges.cloudflare.com/turnstile/v0/api.js"
@@ -480,7 +486,9 @@
></div>
<button
class="mt-4 w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded"
onclick="document.getElementById('language-modal').classList.add('hidden')"
onclick="
document.getElementById('language-modal').classList.add('hidden')
"
>
Close
</button>