diff --git a/src/client/GutterAdModal.ts b/src/client/GutterAdModal.ts new file mode 100644 index 000000000..bbeedaf5f --- /dev/null +++ b/src/client/GutterAdModal.ts @@ -0,0 +1,145 @@ +import { LitElement, css, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; + +@customElement("gutter-ad-modal") +export class GutterAdModal extends LitElement { + @state() + private isVisible: boolean = false; + + @state() + private adLoaded: boolean = false; + + private leftAdType: string = "left_rail"; + private rightAdType: string = "right_rail"; + 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; + } + + static styles = css``; + + // Called after the component's DOM is first rendered + firstUpdated() { + // DOM is guaranteed to be available here + console.log("GutterAdModal DOM is ready"); + } + + public show(): void { + this.isVisible = true; + this.requestUpdate(); + + // Wait for the update to complete, then load ads + this.updateComplete.then(() => { + this.loadAds(); + }); + } + + public hide(): void { + this.isVisible = false; + this.destroyAds(); + this.adLoaded = false; + this.requestUpdate(); + } + + private loadAds(): void { + // Ensure the container elements exist before loading ads + 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.ramp) { + console.warn("Playwire RAMP not available"); + return; + } + + if (this.adLoaded) { + console.log("Ads already loaded, skipping"); + return; + } + + try { + window.ramp.que.push(() => { + 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 (error) { + console.error("Failed to load Playwire ads:", error); + } + } + + private destroyAds(): void { + if (!window.ramp || !this.adLoaded) { + return; + } + try { + window.ramp.destroyUnits("all"); + } catch (error) { + console.error("Failed to destroy Playwire ad:", error); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.destroyAds(); + } + + render() { + if (!this.isVisible) { + return html``; + } + + return html` + + + + + + `; + } +} diff --git a/src/client/Main.ts b/src/client/Main.ts index 4eb9c8c82..f042d918c 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -11,7 +11,7 @@ import "./FlagInput"; import { FlagInput } from "./FlagInput"; import { GameStartingModal } from "./GameStartingModal"; import "./GoogleAdElement"; -import GoogleAdElement from "./GoogleAdElement"; +import { GutterAdModal } from "./GutterAdModal"; import { HelpModal } from "./HelpModal"; import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal"; @@ -34,6 +34,26 @@ import "./components/baseComponents/Modal"; import { discordLogin, getUserMe, isLoggedIn, logOut } from "./jwt"; import "./styles.css"; +declare global { + interface Window { + PageOS: { + session: { + newPageView: () => void; + }; + }; + ramp: { + que: Array<() => void>; + passiveMode: boolean; + spaAddAds: (ads: Array<{ type: string; selectorId: string }>) => void; + destroyUnits: (adType: string) => void; + settings?: { + slots?: any; + }; + spaNewPage: (url: string) => void; + }; + } +} + export interface JoinLobbyEvent { clientID: string; // Multiplayer games only have gameID, gameConfig is not known until game starts. @@ -53,8 +73,8 @@ class Client { private joinModal: JoinPrivateLobbyModal; private publicLobby: PublicLobby; - private googleAds: NodeListOf; private userSettings: UserSettings = new UserSettings(); + private gutterAdModal: GutterAdModal; constructor() {} @@ -122,9 +142,6 @@ class Client { } this.publicLobby = document.querySelector("public-lobby") as PublicLobby; - this.googleAds = document.querySelectorAll( - "google-ad", - ) as NodeListOf; window.addEventListener("beforeunload", () => { console.log("Browser is closing"); @@ -149,6 +166,12 @@ class Client { } }); + this.gutterAdModal = document.querySelector( + "gutter-ad-modal", + ) as GutterAdModal; + this.gutterAdModal instanceof GutterAdModal; + this.gutterAdModal.show(); + // const ctModal = document.querySelector("chat-modal") as ChatModal; // ctModal instanceof ChatModal; // document.getElementById("chat-button").addEventListener("click", () => { @@ -350,6 +373,14 @@ class Client { () => { this.joinModal.close(); this.publicLobby.stop(); + this.gutterAdModal.hide(); + + try { + window.PageOS.session.newPageView(); + } catch (e) { + console.error("Error calling newPageView", e); + } + document.querySelectorAll(".ad").forEach((ad) => { (ad as HTMLElement).style.display = "none"; }); diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index b759a907d..b9f3d55c2 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -15,6 +15,7 @@ import { GameLeftSidebar } from "./layers/GameLeftSidebar"; import { HeadsUpMessage } from "./layers/HeadsUpMessage"; import { Layer } from "./layers/Layer"; import { Leaderboard } from "./layers/Leaderboard"; +import { LeftInGameAd } from "./layers/LeftInGameAd"; import { MainRadialMenu } from "./layers/MainRadialMenu"; import { MultiTabModal } from "./layers/MultiTabModal"; import { NameLayer } from "./layers/NameLayer"; @@ -205,6 +206,14 @@ export function createRenderer( unitInfoModal.structureLayer = structureLayer; // unitInfoModal.eventBus = eventBus; + const leftInGameAd = document.querySelector( + "left-in-game-ad", + ) as LeftInGameAd; + if (!(leftInGameAd instanceof LeftInGameAd)) { + console.error("left in game ad not found"); + } + leftInGameAd.g = game; + const layers: Layer[] = [ new TerrainLayer(game, transformHandler), new TerritoryLayer(game, eventBus, transformHandler), @@ -241,6 +250,7 @@ export function createRenderer( headsUpMessage, unitInfoModal, multiTabModal, + leftInGameAd, ]; return new GameRenderer( diff --git a/src/client/graphics/layers/LeftInGameAd.ts b/src/client/graphics/layers/LeftInGameAd.ts new file mode 100644 index 000000000..07db7657a --- /dev/null +++ b/src/client/graphics/layers/LeftInGameAd.ts @@ -0,0 +1,133 @@ +import { LitElement, css, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { GameView } from "../../../core/game/GameView"; +import { Layer } from "./Layer"; + +const BREAKPOINT = { + width: 1000, + height: 800, +}; + +const AD_TYPE = "standard_iab_modl2"; // 320x50/320x100 - better for bottom positioning +const AD_CONTAINER_ID = "bottom-left-ad-container"; + +@customElement("left-in-game-ad") +export class LeftInGameAd extends LitElement implements Layer { + public g: GameView; + + @state() + private isVisible: boolean = false; + + @state() + private adLoaded: boolean = false; + + // Override createRenderRoot to disable shadow DOM + createRenderRoot() { + return this; + } + + static styles = css``; + + constructor() { + super(); + } + + public show(): void { + this.isVisible = true; + this.requestUpdate(); + // Load the ad when showing (with small delay to ensure DOM is ready) + setTimeout(() => this.loadAd(), 100); + } + + public hide(): void { + this.isVisible = false; + this.adLoaded = false; + this.requestUpdate(); + // Destroy the ad when hiding + this.destroyAd(); + } + + public async tick() { + if (!this.isVisible && !this.g.inSpawnPhase() && this.screenLargeEnough()) { + console.log("showing bottom left ad"); + this.show(); + } + if (this.isVisible && !this.screenLargeEnough()) { + console.log("hiding bottom left ad"); + this.hide(); + } + } + + private screenLargeEnough(): boolean { + return ( + window.innerWidth > BREAKPOINT.width && + window.innerHeight > BREAKPOINT.height + ); + } + + private loadAd(): void { + if (!window.ramp) { + console.warn("Playwire RAMP not available"); + return; + } + if (this.adLoaded) { + console.log("Ad already loaded, skipping"); + return; + } + try { + window.ramp.que.push(() => { + window.ramp.spaAddAds([ + { + type: AD_TYPE, + selectorId: AD_CONTAINER_ID, + }, + ]); + this.adLoaded = true; + console.log("Playwire ad loaded:", AD_TYPE); + }); + } catch (error) { + console.error("Failed to load Playwire ad:", error); + } + } + + private destroyAd(): void { + if (!window.ramp || !this.adLoaded) { + return; + } + try { + window.ramp.que.push(() => { + window.ramp.destroyUnits(AD_TYPE); + console.log("Playwire ad destroyed:", AD_TYPE); + }); + } catch (error) { + console.error("Failed to destroy Playwire ad:", error); + } + } + + disconnectedCallback() { + super.disconnectedCallback(); + // Clean up ad when component is removed + this.destroyAd(); + } + + render() { + if (!this.isVisible) { + return html``; + } + + return html` +
+
+ ${!this.adLoaded + ? html`Loading ad...` + : ""} +
+
+ `; + } +} diff --git a/src/client/index.html b/src/client/index.html index 5292bbd18..5c2c5546d 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -45,61 +45,6 @@ filter: blur(4px) brightness(0.7); } - .left-gutter-ad { - position: fixed; - left: 0; - top: 200px; - /* Changed from top: 50% */ - transform: none; - /* Removed translateY(-50%) since we don't need to center anymore */ - z-index: 40; - width: 300px; - height: 600px; - display: flex; - flex-direction: column; - justify-content: center; - pointer-events: auto; - } - - .left-gutter-ad google-ad { - width: 100%; - height: 100%; - display: block; - } - - .right-gutter-ad { - position: fixed; - right: 0; - top: 200px; - /* Changed from top: 50% */ - transform: none; - /* Removed translateY(-50%) since we don't need to center anymore */ - z-index: 40; - width: 300px; - height: 600px; - display: flex; - flex-direction: column; - justify-content: center; - pointer-events: auto; - } - - .right-gutter-ad google-ad { - width: 100%; - height: 100%; - display: block; - } - - /* Media query to hide the gutter ad on smaller screens */ - @media (max-width: 1280px) { - .left-gutter-ad { - display: none; - } - - .right-gutter-ad { - display: none; - } - } - /* display:none if child has class parent-hidden since we can't use shadow DOM in Lit due to Tailwind */ .component-hideable:has(> .parent-hidden) { display: none; @@ -111,6 +56,13 @@ document.documentElement.className = "preload"; + + + + + +