diff --git a/resources/lang/en.json b/resources/lang/en.json index 3ee0af481..98bb34f31 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -6,6 +6,7 @@ "lang_code": "en" }, "common": { + "not_logged_in": "Not logged in", "close": "Close", "copy": "Copy", "paste": "Paste", @@ -939,7 +940,6 @@ "territory_patterns": { "title": "Skins", "purchase": "Purchase", - "not_logged_in": "Not logged in", "pattern": { "default": "Default" }, diff --git a/src/client/CrazyGamesSDK.ts b/src/client/CrazyGamesSDK.ts index 1933074ec..ae2d5bf1d 100644 --- a/src/client/CrazyGamesSDK.ts +++ b/src/client/CrazyGamesSDK.ts @@ -25,6 +25,16 @@ declare global { }, ) => void; }; + banner: { + requestBanner: (options: { + id: string; + width: number; + height: number; + }) => Promise; + requestResponsiveBanner: (containerId: string) => Promise; + clearBanner: (containerId: string) => void; + clearAllBanners: () => void; + }; game: { gameplayStart: () => Promise; gameplayStop: () => Promise; @@ -76,11 +86,9 @@ export class CrazyGamesSDK { } return false; } catch (e) { - console.log("[CrazyGames]: ", e); // If we get a cross-origin error, we're definitely iframed // Check our own referrer as fallback const isCrazyGames = document.referrer.includes("crazygames"); - console.log("[CrazyGames], contains referrer: ", isCrazyGames); if (isCrazyGames) { return true; } @@ -323,6 +331,70 @@ export class CrazyGamesSDK { } } + private bottomLeftContainerId = "cg-bottom-left-ad"; + private bottomLeftAdVisible = false; + + createBottomLeftAd(): void { + console.log( + `[CrazyGames] createBottomLeftAd called, isReady=${this.isReady()}`, + ); + if (!this.isReady()) { + console.log("[CrazyGames] SDK not ready, skipping bottom-left ad"); + return; + } + + if (this.bottomLeftAdVisible) { + console.log("[CrazyGames] Bottom-left ad already visible"); + return; + } + + // Remove existing container if any + document.getElementById(this.bottomLeftContainerId)?.remove(); + + const container = document.createElement("div"); + container.id = this.bottomLeftContainerId; + container.style.cssText = ` + position: fixed; + bottom: 0; + left: 0; + width: 300px; + height: 250px; + z-index: 9999; + pointer-events: auto; + `; + document.body.appendChild(container); + console.log("[CrazyGames] Created bottom-left ad container"); + + (async () => { + try { + await window.CrazyGames!.SDK.banner.requestBanner({ + id: this.bottomLeftContainerId, + width: 300, + height: 250, + }); + console.log("[CrazyGames] Bottom-left banner loaded"); + } catch (e) { + console.log("[CrazyGames] Bottom-left banner error:", e); + } + })(); + + this.bottomLeftAdVisible = true; + } + + clearBottomLeftAd(): void { + if (!this.bottomLeftAdVisible) return; + + try { + window.CrazyGames!.SDK.banner.clearBanner(this.bottomLeftContainerId); + } catch (e) { + console.error("[CrazyGames] Error clearing bottom-left banner:", e); + } + + document.getElementById(this.bottomLeftContainerId)?.remove(); + this.bottomLeftAdVisible = false; + console.log("[CrazyGames] Bottom-left ad cleared"); + } + requestMidgameAd(): Promise { return new Promise((resolve) => { if (!this.isReady()) { diff --git a/src/client/FlagInputModal.ts b/src/client/FlagInputModal.ts index fd3370078..8dc9ae1ec 100644 --- a/src/client/FlagInputModal.ts +++ b/src/client/FlagInputModal.ts @@ -10,6 +10,7 @@ import { fetchCosmetics, flagRelationship } from "./Cosmetics"; import { translateText } from "./Utils"; import { BaseModal } from "./components/BaseModal"; import "./components/FlagButton"; +import "./components/NotLoggedInWarning"; import { modalHeader } from "./components/ui/ModalHeader"; @customElement("flag-input-modal") @@ -104,6 +105,7 @@ export class FlagInputModal extends BaseModal { title: translateText("flag_input.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), + rightContent: html``, })}
@@ -119,6 +121,17 @@ export class FlagInputModal extends BaseModal { />
+
+ +
{ - // 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.onerror = null; + avatarEl.src = "https://cdn.discordapp.com/embed/avatars/0.png"; }; avatarEl.onload = () => { // Only handle if this is the latest update diff --git a/src/client/Navigation.ts b/src/client/Navigation.ts index 95f720092..7878155dd 100644 --- a/src/client/Navigation.ts +++ b/src/client/Navigation.ts @@ -19,11 +19,20 @@ export function initNavigation() { // Close mobile sidebar if a nav item was clicked closeMobileSidebar(); - // Hide only the currently visible modal + // Close the currently visible modal properly const visibleModal = document.querySelector(".page-content:not(.hidden)"); if (visibleModal) { - visibleModal.classList.add("hidden"); - visibleModal.classList.remove("block"); + // If it's an open modal component, call close() for proper cleanup (onClose callback, etc.) + if ( + typeof (visibleModal as any).isOpen === "function" && + (visibleModal as any).isOpen() && + typeof (visibleModal as any).close === "function" + ) { + (visibleModal as any).close(); + } else { + visibleModal.classList.add("hidden"); + visibleModal.classList.remove("block"); + } } // Handle page-play separately (it's not a page-content element) diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index f11999594..6d28d49bb 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -145,14 +145,7 @@ export class SinglePlayerModal extends BaseModal { return; } - const achievements = Array.isArray(userMe.player.achievements) - ? userMe.player.achievements - : []; - - const completions = - achievements.find( - (achievement) => achievement?.type === "singleplayer-map", - )?.data ?? []; + const completions = userMe.player.achievements.singleplayerMap; const winsMap = new Map>(); for (const entry of completions) { diff --git a/src/client/Store.ts b/src/client/Store.ts index 5cf67cdc8..d6d469518 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -5,9 +5,9 @@ import { UserMeResponse } from "../core/ApiSchemas"; import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas"; import { UserSettings } from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; -import { hasLinkedAccount } from "./Api"; import { BaseModal } from "./components/BaseModal"; import "./components/FlagButton"; +import "./components/NotLoggedInWarning"; import "./components/PatternButton"; import { modalHeader } from "./components/ui/ModalHeader"; import { @@ -77,11 +77,7 @@ export class StoreModal extends BaseModal { title: translateText("store.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), - rightContent: !hasLinkedAccount(this.userMeResponse) - ? html`
- ${this.renderNotLoggedInWarning()} -
` - : undefined, + rightContent: html``, })}
`; - } - render() { if (!this.isActive && !this.inline) return html``; diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index 83cfe207f..9d9c995ed 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -5,8 +5,8 @@ import { UserMeResponse } from "../core/ApiSchemas"; import { Cosmetics, Pattern } from "../core/CosmeticSchemas"; import { UserSettings } from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; -import { hasLinkedAccount } from "./Api"; import { BaseModal } from "./components/BaseModal"; +import "./components/NotLoggedInWarning"; import "./components/PatternButton"; import { modalHeader } from "./components/ui/ModalHeader"; import { @@ -123,18 +123,6 @@ export class TerritoryPatternsModal extends BaseModal { `; } - private renderNotLoggedInWarning(): TemplateResult { - return html``; - } - render() { const content = html`
@@ -145,13 +133,20 @@ export class TerritoryPatternsModal extends BaseModal { title: translateText("territory_patterns.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), - rightContent: !hasLinkedAccount(this.userMeResponse) - ? html`
- ${this.renderNotLoggedInWarning()} -
` - : undefined, + rightContent: html``, })}
+
+ +
diff --git a/src/client/components/NotLoggedInWarning.ts b/src/client/components/NotLoggedInWarning.ts new file mode 100644 index 000000000..6b972b8f0 --- /dev/null +++ b/src/client/components/NotLoggedInWarning.ts @@ -0,0 +1,49 @@ +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { UserMeResponse } from "../../core/ApiSchemas"; +import { hasLinkedAccount } from "../Api"; + +@customElement("not-logged-in-warning") +export class NotLoggedInWarning extends LitElement { + @state() private linked = false; + + private _onUserMe = (event: CustomEvent) => { + this.linked = hasLinkedAccount(event.detail); + }; + + createRenderRoot() { + return this; + } + + connectedCallback() { + super.connectedCallback(); + document.addEventListener( + "userMeResponse", + this._onUserMe as EventListener, + ); + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener( + "userMeResponse", + this._onUserMe as EventListener, + ); + } + + render() { + if (this.linked) return html``; + + return html`
+ +
`; + } +} diff --git a/src/client/components/baseComponents/stats/DiscordUserHeader.ts b/src/client/components/baseComponents/stats/DiscordUserHeader.ts index 526835203..f223a0805 100644 --- a/src/client/components/baseComponents/stats/DiscordUserHeader.ts +++ b/src/client/components/baseComponents/stats/DiscordUserHeader.ts @@ -31,15 +31,20 @@ export class DiscordUserHeader extends LitElement { } render() { + const defaultAvatar = "https://cdn.discordapp.com/embed/avatars/0.png"; + const imgSrc = this.avatarUrl ?? defaultAvatar; return html`
- ${this.avatarUrl + ${this._data ? html`
${translateText( { + (e.target as HTMLImageElement).src = defaultAvatar; + }} />
` diff --git a/src/client/graphics/layers/InGamePromo.ts b/src/client/graphics/layers/InGamePromo.ts index 097bf5bd6..b4d58e237 100644 --- a/src/client/graphics/layers/InGamePromo.ts +++ b/src/client/graphics/layers/InGamePromo.ts @@ -1,6 +1,7 @@ import { LitElement, html } from "lit"; import { customElement } from "lit/decorators.js"; import { GameView } from "../../../core/game/GameView"; +import { crazyGamesSDK } from "../../CrazyGamesSDK"; import { Layer } from "./Layer"; const AD_TYPE = "standard_iab_left1"; @@ -30,6 +31,7 @@ export class InGamePromo extends LitElement implements Layer { } if (!this.cornerAdShown) { this.cornerAdShown = true; + console.log("[InGamePromo] Spawn phase ended, triggering showAd"); this.showAd(); } } @@ -73,10 +75,19 @@ export class InGamePromo extends LitElement implements Layer { } private showAd(): void { - if (!window.adsEnabled) return; + console.log( + `[InGamePromo] showAd called, isOnCrazyGames=${crazyGamesSDK.isOnCrazyGames()}`, + ); if (window.innerWidth < 1100) return; if (window.innerHeight < 750) return; + if (crazyGamesSDK.isOnCrazyGames()) { + this.showCrazyGamesAd(); + return; + } + + if (!window.adsEnabled) return; + this.shouldShow = true; this.requestUpdate(); @@ -85,6 +96,25 @@ export class InGamePromo extends LitElement implements Layer { }); } + private showCrazyGamesAd(): void { + console.log( + `[InGamePromo] showCrazyGamesAd called, isReady=${crazyGamesSDK.isReady()}, width=${window.innerWidth}, height=${window.innerHeight}`, + ); + if (!crazyGamesSDK.isReady()) { + console.log( + "[InGamePromo] CrazyGames SDK not ready, skipping in-game ad", + ); + return; + } + + this.requestUpdate(); + + this.updateComplete.then(() => { + console.log("[InGamePromo] DOM updated, calling createBottomLeftAd"); + crazyGamesSDK.createBottomLeftAd(); + }); + } + private loadAd(): void { if (!window.ramp) { console.warn("Playwire RAMP not available for in-game ad"); @@ -112,6 +142,14 @@ export class InGamePromo extends LitElement implements Layer { public hideAd(): void { this.destroyBottomRail(); + + if (crazyGamesSDK.isOnCrazyGames()) { + crazyGamesSDK.clearBottomLeftAd(); + this.shouldShow = false; + this.requestUpdate(); + return; + } + if (!window.ramp) { console.warn("Playwire RAMP not available for in-game ad"); return; diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 1ca8d55c1..419152551 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -69,14 +69,9 @@ export const UserMeResponseSchema = z.object({ publicId: z.string(), roles: z.string().array().optional(), flares: z.string().array().optional(), - achievements: z - .array( - z.object({ - type: z.literal("singleplayer-map"), // TODO: change the shape to be more flexible when we have more achievements - data: z.array(SingleplayerMapAchievementSchema), - }), - ) - .optional(), + achievements: z.object({ + singleplayerMap: z.array(SingleplayerMapAchievementSchema), + }), leaderboard: z .object({ oneVone: z