From 3d9f0aec6c5e95478db9c85ea50a30c0ce5d0727 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 26 Jan 2026 13:34:04 -0800 Subject: [PATCH] Migrate from publift to playwire ads (#3039) ## Description: Use playwire ad integration ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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 --- index.html | 18 ++- src/client/GutterAds.ts | 152 +++++++++---------- src/client/Main.ts | 58 ++----- src/client/graphics/GameRenderer.ts | 12 +- src/client/graphics/layers/AdTimer.ts | 28 ---- src/client/graphics/layers/InGameHeaderAd.ts | 112 ++++++++++++++ 6 files changed, 219 insertions(+), 161 deletions(-) delete mode 100644 src/client/graphics/layers/AdTimer.ts create mode 100644 src/client/graphics/layers/InGameHeaderAd.ts diff --git a/index.html b/index.html index cad490b1c..b1f825ae2 100644 --- a/index.html +++ b/index.html @@ -75,11 +75,11 @@ defer > - - + + diff --git a/src/client/GutterAds.ts b/src/client/GutterAds.ts index caa33623a..91cc08e30 100644 --- a/src/client/GutterAds.ts +++ b/src/client/GutterAds.ts @@ -1,70 +1,46 @@ -import { LitElement, html } from "lit"; +import { LitElement, css, html } from "lit"; import { customElement, state } from "lit/decorators.js"; -import { UserMeResponse } from "../core/ApiSchemas"; -import { isInIframe } from "./Utils"; - -const LEFT_FUSE = "gutter-ad-container-left"; -const RIGHT_FUSE = "gutter-ad-container-right"; -// Minimum screen width to show ads (larger than typical Chromebook) -const MIN_SCREEN_WIDTH = 1400; @customElement("gutter-ads") export class GutterAds extends LitElement { @state() private isVisible: boolean = false; + @state() + private adLoaded: boolean = false; + + private leftAdType: string = "standard_iab_left2"; + private rightAdType: string = "standard_iab_rght1"; + private leftContainerId: string = "gutter-ad-container-left"; + private rightContainerId: string = "gutter-ad-container-right"; + private margin: string = "10px"; + // Override createRenderRoot to disable shadow DOM createRenderRoot() { return this; } - private readonly boundUserMeHandler = (event: Event) => - this.onUserMe((event as CustomEvent).detail); + static styles = css``; connectedCallback() { super.connectedCallback(); - document.addEventListener( - "userMeResponse", - this.boundUserMeHandler as EventListener, - ); - } - - private onUserMe(userMeResponse: UserMeResponse | false): void { - const flares = - userMeResponse === false ? [] : (userMeResponse.player.flares ?? []); - const hasFlare = flares.some((flare) => flare.startsWith("pattern:")); - if (hasFlare) { - console.log("No ads because you have patterns"); - window.enableAds = false; - } else { - console.log("No flares, showing ads"); - this.show(); - window.enableAds = true; - } - } - - private isScreenLargeEnough(): boolean { - return window.innerWidth >= MIN_SCREEN_WIDTH; + document.addEventListener("userMeResponse", () => { + if (window.adsEnabled) { + console.log("showing gutter ads"); + this.show(); + } else { + console.log("not showing gutter ads"); + } + }); } // Called after the component's DOM is first rendered firstUpdated() { // DOM is guaranteed to be available here - console.log("GutterAd DOM is ready"); + console.log("GutterAdModal DOM is ready"); } public show(): void { - if (!this.isScreenLargeEnough()) { - console.log("Screen too small for gutter ads, skipping"); - return; - } - - if (isInIframe()) { - console.log("In iframe, showing gutter ads"); - return; - } - - console.log("showing GutterAds"); this.isVisible = true; this.requestUpdate(); @@ -74,58 +50,57 @@ export class GutterAds extends LitElement { }); } - public hide(): void { - this.isVisible = false; - console.log("hiding GutterAds"); - this.destroyAds(); - document.removeEventListener( - "userMeResponse", - this.boundUserMeHandler as EventListener, - ); - this.requestUpdate(); - } - private loadAds(): void { + console.log("loading ramp ads"); // Ensure the container elements exist before loading ads - const leftContainer = this.querySelector(`#${LEFT_FUSE}`); - const rightContainer = this.querySelector(`#${RIGHT_FUSE}`); + const leftContainer = this.querySelector(`#${this.leftContainerId}`); + const rightContainer = this.querySelector(`#${this.rightContainerId}`); if (!leftContainer || !rightContainer) { console.warn("Ad containers not found in DOM"); return; } - if (!window.fusetag) { - console.warn("Fuse tag not available"); + if (!window.ramp) { + console.warn("Playwire RAMP not available"); + return; + } + + if (this.adLoaded) { + console.log("Ads already loaded, skipping"); return; } try { - console.log("registering zones"); - window.fusetag.que.push(() => { - window.fusetag.registerZone(LEFT_FUSE); - window.fusetag.registerZone(RIGHT_FUSE); + window.ramp.que.push(() => { + try { + window.ramp.spaAddAds([ + { + type: this.leftAdType, + selectorId: this.leftContainerId, + }, + { + type: this.rightAdType, + selectorId: this.rightContainerId, + }, + ]); + this.adLoaded = true; + console.log( + "Playwire ads loaded:", + this.leftAdType, + this.rightAdType, + ); + } catch (e) { + console.log(e); + } }); } catch (error) { - console.error("Failed to load fuse ads:", error); - this.hide(); + console.error("Failed to load Playwire ads:", error); } } - private destroyAds(): void { - if (!window.fusetag) { - return; - } - window.fusetag.que.push(() => { - window.fusetag.destroyZone(LEFT_FUSE); - window.fusetag.destroyZone(RIGHT_FUSE); - }); - this.requestUpdate(); - } - disconnectedCallback() { super.disconnectedCallback(); - this.hide(); } render() { @@ -134,11 +109,26 @@ export class GutterAds extends LitElement { } return html` -
-
+ + -
-
+ + + `; } diff --git a/src/client/Main.ts b/src/client/Main.ts index 5f4342e3e..57644d6b8 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -7,7 +7,7 @@ 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 { getUserMe, hasLinkedAccount } from "./Api"; import { userAuth } from "./Auth"; import { joinLobby } from "./ClientGameRunner"; import { fetchCosmetics } from "./Cosmetics"; @@ -163,19 +163,12 @@ declare global { GIT_COMMIT: string; INSTANCE_ID: string; turnstile: any; - enableAds: boolean; + adsEnabled: boolean; PageOS: { session: { newPageView: () => void; }; }; - fusetag: { - registerZone: (id: string) => void; - destroyZone: (id: string) => void; - pageInit: (options?: any) => void; - que: Array<() => void>; - destroySticky: () => void; - }; ramp: { que: Array<() => void>; passiveMode: boolean; @@ -184,7 +177,7 @@ declare global { settings?: { slots?: any; }; - spaNewPage: (url: string) => void; + spaNewPage: (url?: string) => void; }; showPage?: (pageId: string) => void; } @@ -475,15 +468,14 @@ class Client { const onUserMe = async (userMeResponse: UserMeResponse | false) => { // Check if user has actual authentication (discord or email), not just a publicId - const loggedIn = - userMeResponse !== false && - userMeResponse !== null && - typeof userMeResponse === "object" && - userMeResponse.user && - (userMeResponse.user.discord !== undefined || - userMeResponse.user.email !== undefined); - updateMatchmakingButton(loggedIn); + const isLinked: boolean = hasLinkedAccount(userMeResponse); + updateMatchmakingButton(isLinked); updateAccountNavButton(userMeResponse); + const adsEnabled = + !crazyGamesSDK.isOnCrazyGames() && + ((userMeResponse || null)?.player?.flares?.length ?? 0) === 0; + console.log("ads enabled: ", adsEnabled); + window.adsEnabled = adsEnabled; document.dispatchEvent( new CustomEvent("userMeResponse", { detail: userMeResponse, @@ -653,8 +645,6 @@ class Client { updateSliderProgress(slider); slider.addEventListener("input", () => updateSliderProgress(slider)); }); - - this.initializeFuseTag(); } private handleUrl() { @@ -847,7 +837,6 @@ class Client { if (startingModal && startingModal instanceof GameStartingModal) { startingModal.show(); } - this.gutterAds.hide(); }, () => { this.joinModal.close(); @@ -858,6 +847,9 @@ class Client { (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"); @@ -902,8 +894,6 @@ class Client { document.body.classList.remove("in-game"); crazyGamesSDK.gameplayStop(); - - this.gutterAds.hide(); this.publicLobby.leaveLobby(); } @@ -925,28 +915,6 @@ class Client { } } - private initializeFuseTag() { - const tryInitFuseTag = (): boolean => { - if (window.fusetag && typeof window.fusetag.pageInit === "function") { - console.log("initializing fuse tag"); - window.fusetag.que.push(() => { - window.fusetag.pageInit({ - blockingFuseIds: ["lhs_sticky_vrec", "rhs_sticky_vrec"], - }); - }); - return true; - } else { - return false; - } - }; - - const interval = setInterval(() => { - if (tryInitFuseTag()) { - clearInterval(interval); - } - }, 100); - } - private async getTurnstileToken( lobby: JoinLobbyEvent, ): Promise { diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index b4cd3eb38..d501c095f 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -6,7 +6,6 @@ import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; import { FrameProfiler } from "./FrameProfiler"; import { TransformHandler } from "./TransformHandler"; import { UIState } from "./UIState"; -import { AdTimer } from "./layers/AdTimer"; import { AlertFrame } from "./layers/AlertFrame"; import { BuildMenu } from "./layers/BuildMenu"; import { ChatDisplay } from "./layers/ChatDisplay"; @@ -20,6 +19,7 @@ import { GameLeftSidebar } from "./layers/GameLeftSidebar"; import { GameRightSidebar } from "./layers/GameRightSidebar"; import { HeadsUpMessage } from "./layers/HeadsUpMessage"; import { ImmunityTimer } from "./layers/ImmunityTimer"; +import { InGameHeaderAd } from "./layers/InGameHeaderAd"; import { Layer } from "./layers/Layer"; import { Leaderboard } from "./layers/Leaderboard"; import { MainRadialMenu } from "./layers/MainRadialMenu"; @@ -244,6 +244,14 @@ export function createRenderer( } immunityTimer.game = game; + const inGameHeaderAd = document.querySelector( + "in-game-header-ad", + ) as InGameHeaderAd; + if (!(inGameHeaderAd instanceof InGameHeaderAd)) { + console.error("in-game header ad not found"); + } + inGameHeaderAd.game = game; + // When updating these layers please be mindful of the order. // Try to group layers by the return value of shouldTransform. // Not grouping the layers may cause excessive calls to context.save() and context.restore(). @@ -287,7 +295,7 @@ export function createRenderer( playerPanel, headsUpMessage, multiTabModal, - new AdTimer(game), + inGameHeaderAd, alertFrame, performanceOverlay, ]; diff --git a/src/client/graphics/layers/AdTimer.ts b/src/client/graphics/layers/AdTimer.ts deleted file mode 100644 index 367744df9..000000000 --- a/src/client/graphics/layers/AdTimer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { GameView } from "../../../core/game/GameView"; -import { Layer } from "./Layer"; - -const AD_SHOW_TICKS = 2 * 60 * 10; // 2 minute - -export class AdTimer implements Layer { - private isHidden: boolean = false; - - constructor(private g: GameView) {} - - init() {} - - public async tick() { - if (this.isHidden) { - return; - } - - const gameTicks = this.g.ticks() - this.g.config().numSpawnPhaseTurns(); - if (gameTicks > AD_SHOW_TICKS) { - console.log("destroying sticky ads"); - window.fusetag?.que?.push(() => { - window.fusetag?.destroySticky?.(); - }); - this.isHidden = true; - return; - } - } -} diff --git a/src/client/graphics/layers/InGameHeaderAd.ts b/src/client/graphics/layers/InGameHeaderAd.ts new file mode 100644 index 000000000..f3925508a --- /dev/null +++ b/src/client/graphics/layers/InGameHeaderAd.ts @@ -0,0 +1,112 @@ +import { LitElement, html } from "lit"; +import { customElement } from "lit/decorators.js"; +import { GameView } from "../../../core/game/GameView"; +import { Layer } from "./Layer"; + +const AD_SHOW_TICKS = 2 * 60 * 10; // 2 minutes +const HEADER_AD_TYPE = "standard_iab_head1"; +const HEADER_AD_CONTAINER_ID = "header-ad-container"; +const TWO_XL_BREAKPOINT = 1536; + +@customElement("in-game-header-ad") +export class InGameHeaderAd extends LitElement implements Layer { + public game: GameView; + + private isHidden: boolean = false; + private adLoaded: boolean = false; + private shouldShow: boolean = false; + + createRenderRoot() { + return this; + } + + init() { + this.showHeaderAd(); + } + + private showHeaderAd(): void { + // Don't show header ad on screens smaller than 2xl + if (window.innerWidth < TWO_XL_BREAKPOINT) { + return; + } + if (!window.adsEnabled) { + return; + } + + this.shouldShow = true; + this.requestUpdate(); + + // Wait for the element to render before loading the ad + this.updateComplete.then(() => { + this.loadAd(); + }); + } + + private loadAd(): void { + if (!window.ramp) { + console.warn("Playwire RAMP not available for header ad"); + return; + } + + try { + window.ramp.que.push(() => { + try { + window.ramp.spaAddAds([ + { + type: HEADER_AD_TYPE, + selectorId: HEADER_AD_CONTAINER_ID, + }, + ]); + this.adLoaded = true; + console.log("Header ad loaded:", HEADER_AD_TYPE); + } catch (e) { + console.error("Failed to add header ad:", e); + } + }); + } catch (error) { + console.error("Failed to load header ad:", error); + } + } + + private hideHeaderAd(): void { + this.shouldShow = false; + this.adLoaded = false; + this.requestUpdate(); + } + + public tick() { + if (this.isHidden) { + return; + } + + const gameTicks = + this.game.ticks() - this.game.config().numSpawnPhaseTurns(); + if (gameTicks > AD_SHOW_TICKS) { + console.log("destroying header ad and refreshing PageOS"); + this.hideHeaderAd(); + this.isHidden = true; + + if (window.PageOS?.session?.newPageView) { + window.PageOS.session.newPageView(); + } + return; + } + } + + shouldTransform(): boolean { + return false; + } + + render() { + if (!this.shouldShow) { + return html``; + } + + return html` + + `; + } +}