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