From dd9ad7472f68a681a2fba141d6e2c43b370ae8cd Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 23 Oct 2025 15:02:13 -0700 Subject: [PATCH] update header ads (#2266) ## Description: 1. Remove SpawnAds and replace it with AdTimer which will delete the in-game ad after the first minute. 2. remove login blocker UI, we don't use it anymore 3. convert TerritoryPatternsModal & GutterAds to use event based when checking for flares 4. remove window.PageOS.session.newPageView(); because it was throwing an exception 5. Convert SpawnTimer to a lit element to give it a higher z-index to stay above the header ad ## 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 --- src/client/Cosmetics.ts | 5 +- src/client/GutterAds.ts | 32 +++++- src/client/Main.ts | 113 +------------------ src/client/TerritoryPatternsModal.ts | 22 +++- src/client/components/PatternButton.ts | 1 - src/client/graphics/GameRenderer.ts | 19 ++-- src/client/graphics/layers/AdTimer.ts | 28 +++++ src/client/graphics/layers/SpawnAd.ts | 135 ----------------------- src/client/graphics/layers/SpawnTimer.ts | 74 +++++++++---- src/client/graphics/layers/WinModal.ts | 7 +- src/client/index.html | 9 +- 11 files changed, 153 insertions(+), 292 deletions(-) create mode 100644 src/client/graphics/layers/AdTimer.ts delete mode 100644 src/client/graphics/layers/SpawnAd.ts diff --git a/src/client/Cosmetics.ts b/src/client/Cosmetics.ts index c0a501d8b..8eeb1f74d 100644 --- a/src/client/Cosmetics.ts +++ b/src/client/Cosmetics.ts @@ -74,10 +74,11 @@ export async function fetchCosmetics(): Promise { export function patternRelationship( pattern: Pattern, colorPalette: { name: string; isArchived?: boolean } | null, - userMeResponse: UserMeResponse | null, + userMeResponse: UserMeResponse | false, affiliateCode: string | null, ): "owned" | "purchasable" | "blocked" { - const flares = userMeResponse?.player.flares ?? []; + const flares = + userMeResponse === false ? [] : (userMeResponse.player.flares ?? []); if (flares.includes("pattern:*")) { return "owned"; } diff --git a/src/client/GutterAds.ts b/src/client/GutterAds.ts index b47f2939b..caa33623a 100644 --- a/src/client/GutterAds.ts +++ b/src/client/GutterAds.ts @@ -1,5 +1,6 @@ import { LitElement, 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"; @@ -17,6 +18,31 @@ export class GutterAds extends LitElement { return this; } + private readonly boundUserMeHandler = (event: Event) => + this.onUserMe((event as CustomEvent).detail); + + 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; } @@ -52,6 +78,10 @@ export class GutterAds extends LitElement { this.isVisible = false; console.log("hiding GutterAds"); this.destroyAds(); + document.removeEventListener( + "userMeResponse", + this.boundUserMeHandler as EventListener, + ); this.requestUpdate(); } @@ -95,7 +125,7 @@ export class GutterAds extends LitElement { disconnectedCallback() { super.disconnectedCallback(); - this.destroyAds(); + this.hide(); } render() { diff --git a/src/client/Main.ts b/src/client/Main.ts index 2d61e78b6..e6297b63c 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -2,7 +2,6 @@ import version from "../../resources/version.txt"; import { UserMeResponse } from "../core/ApiSchemas"; import { EventBus } from "../core/EventBus"; import { GameRecord, GameStartInfo, ID } from "../core/Schemas"; -import { ServerConfig } from "../core/configuration/Config"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { UserSettings } from "../core/game/UserSettings"; import "./AccountModal"; @@ -36,17 +35,17 @@ import { generateCryptoRandomUUID, incrementGamesPlayed, isInIframe, - translateText, } from "./Utils"; import "./components/NewsButton"; import { NewsButton } from "./components/NewsButton"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; -import { discordLogin, getUserMe, isLoggedIn } from "./jwt"; +import { getUserMe, isLoggedIn } from "./jwt"; import "./styles.css"; declare global { interface Window { + enableAds: boolean; PageOS: { session: { newPageView: () => void; @@ -57,6 +56,7 @@ declare global { destroyZone: (id: string) => void; pageInit: (options?: any) => void; que: Array<() => void>; + destroySticky: () => void; }; ramp: { que: Array<() => void>; @@ -265,98 +265,12 @@ class Client { }), ); - const config = await getServerConfigFromClient(); - if (!hasAllowedFlare(userMeResponse, config)) { - if (userMeResponse === false) { - // Login is required - document.body.innerHTML = ` -
-
-

${translateText("auth.login_required")}

-

${translateText("auth.redirecting")}

-
-
-
-
-
-
- - `; - setTimeout(discordLogin, 5000); - } else { - // Unauthorized - document.body.innerHTML = ` -
-
-

${translateText("auth.not_authorized")}

-

${translateText("auth.contact_admin")}

-
-
-
- `; - } - return; - } else if (userMeResponse === false) { - // Not logged in - this.patternsModal.onUserMe(null); - } else { + if (userMeResponse !== false) { // Authorized console.log( `Your player ID is ${userMeResponse.player.publicId}\n` + "Sharing this ID will allow others to view your game history and stats.", ); - this.patternsModal.onUserMe(userMeResponse); - const flares = (userMeResponse.player.flares ?? []).filter((flare) => - flare.startsWith("pattern:"), - ); - if (flares.length > 0) { - console.log("Hiding gutter ads because you have patterns"); - this.gutterAds.hide(); - } } }; @@ -628,12 +542,6 @@ class Client { this.publicLobby.stop(); incrementGamesPlayed(); - try { - window.PageOS.session.newPageView(); - } catch (e) { - console.error("Error calling newPageView", e); - } - document.querySelectorAll(".ad").forEach((ad) => { (ad as HTMLElement).style.display = "none"; }); @@ -675,7 +583,6 @@ class Client { window.fusetag.pageInit({ blockingFuseIds: ["lhs_sticky_vrec", "rhs_sticky_vrec"], }); - this.gutterAds.show(); }); return true; } else { @@ -735,15 +642,3 @@ function getPersistentIDFromCookie(): string { return newID; } - -function hasAllowedFlare( - userMeResponse: UserMeResponse | false, - config: ServerConfig, -) { - const allowed = config.allowedFlares(); - if (allowed === undefined) return true; - if (userMeResponse === false) return false; - const flares = userMeResponse.player.flares; - if (flares === undefined) return false; - return allowed.length === 0 || allowed.some((f) => flares.includes(f)); -} diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index baf75e7be..f13faf670 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -37,14 +37,24 @@ export class TerritoryPatternsModal extends LitElement { private affiliateCode: string | null = null; - private userMeResponse: UserMeResponse | null = null; + private userMeResponse: UserMeResponse | false = false; constructor() { super(); } - async onUserMe(userMeResponse: UserMeResponse | null) { - if (userMeResponse === null) { + connectedCallback() { + super.connectedCallback(); + document.addEventListener( + "userMeResponse", + (event: CustomEvent) => { + this.onUserMe(event.detail); + }, + ); + } + + async onUserMe(userMeResponse: UserMeResponse | false) { + if (userMeResponse === false) { this.userSettings.setSelectedPatternName(undefined); this.selectedPattern = null; this.selectedColor = null; @@ -136,7 +146,11 @@ export class TerritoryPatternsModal extends LitElement { } private renderColorSwatchGrid(): TemplateResult { - const hexCodes = (this.userMeResponse?.player.flares ?? []) + const hexCodes = ( + this.userMeResponse === false + ? [] + : (this.userMeResponse.player.flares ?? []) + ) .filter((flare) => flare.startsWith("color:")) .map((flare) => "#" + flare.split(":")[1]); return html` diff --git a/src/client/components/PatternButton.ts b/src/client/components/PatternButton.ts index 1b7146b13..9c35347e1 100644 --- a/src/client/components/PatternButton.ts +++ b/src/client/components/PatternButton.ts @@ -137,7 +137,6 @@ export function renderPatternPreview( width: number, height: number, ): TemplateResult { - console.log("renderPatternPreview", pattern); if (pattern === null) { return renderBlankPreview(width, height); } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index 4491dede9..8b8080dcb 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -5,6 +5,7 @@ import { GameStartingModal } from "../GameStartingModal"; import { RefreshGraphicsEvent as RedrawGraphicsEvent } from "../InputHandler"; 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"; @@ -27,7 +28,6 @@ import { PlayerPanel } from "./layers/PlayerPanel"; import { RailroadLayer } from "./layers/RailroadLayer"; import { ReplayPanel } from "./layers/ReplayPanel"; import { SettingsModal } from "./layers/SettingsModal"; -import { SpawnAd } from "./layers/SpawnAd"; import { SpawnTimer } from "./layers/SpawnTimer"; import { StructureIconsLayer } from "./layers/StructureIconsLayer"; import { StructureLayer } from "./layers/StructureLayer"; @@ -209,18 +209,19 @@ export function createRenderer( fpsDisplay.eventBus = eventBus; fpsDisplay.userSettings = userSettings; - const spawnAd = document.querySelector("spawn-ad") as SpawnAd; - if (!(spawnAd instanceof SpawnAd)) { - console.error("spawn ad not found"); - } - spawnAd.g = game; - const alertFrame = document.querySelector("alert-frame") as AlertFrame; if (!(alertFrame instanceof AlertFrame)) { console.error("alert frame not found"); } alertFrame.game = game; + const spawnTimer = document.querySelector("spawn-timer") as SpawnTimer; + if (!(spawnTimer instanceof SpawnTimer)) { + console.error("spawn timer not found"); + } + spawnTimer.game = game; + spawnTimer.transformHandler = transformHandler; + // 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(). @@ -246,7 +247,7 @@ export function createRenderer( uiState, playerPanel, ), - new SpawnTimer(game, transformHandler), + spawnTimer, leaderboard, gameLeftSidebar, unitDisplay, @@ -260,7 +261,7 @@ export function createRenderer( playerPanel, headsUpMessage, multiTabModal, - spawnAd, + new AdTimer(game), alertFrame, fpsDisplay, ]; diff --git a/src/client/graphics/layers/AdTimer.ts b/src/client/graphics/layers/AdTimer.ts new file mode 100644 index 000000000..4184e6a43 --- /dev/null +++ b/src/client/graphics/layers/AdTimer.ts @@ -0,0 +1,28 @@ +import { GameView } from "../../../core/game/GameView"; +import { Layer } from "./Layer"; + +const AD_SHOW_TICKS = 60 * 10; // 1 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/SpawnAd.ts b/src/client/graphics/layers/SpawnAd.ts deleted file mode 100644 index f8f39294f..000000000 --- a/src/client/graphics/layers/SpawnAd.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { LitElement, css, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import { translateText } from "../../../client/Utils"; -import { GameView } from "../../../core/game/GameView"; -import { getGamesPlayed } from "../../Utils"; -import { Layer } from "./Layer"; - -const AD_TYPE = "bottom_rail"; -const AD_CONTAINER_ID = "bottom-rail-ad-container"; - -@customElement("spawn-ad") -export class SpawnAd extends LitElement implements Layer { - public g: GameView; - - @state() - private isVisible: boolean = false; - - @state() - private adLoaded: boolean = false; - - private gamesPlayed: number = 0; - - // Override createRenderRoot to disable shadow DOM - createRenderRoot() { - return this; - } - - static styles = css``; - - constructor() { - super(); - } - - init() { - this.gamesPlayed = getGamesPlayed(); - } - - public show(): void { - this.isVisible = true; - this.loadAd(); - this.requestUpdate(); - } - - public hide(): void { - // Destroy the ad when hiding - this.destroyAd(); - this.isVisible = false; - this.adLoaded = false; - this.requestUpdate(); - } - - public async tick() { - if ( - !this.isVisible && - this.g.inSpawnPhase() && - this.g.ticks() > 10 && - this.gamesPlayed > 5 - ) { - console.log("not showing spawn ad"); - // this.show(); - } - if (this.isVisible && !this.g.inSpawnPhase()) { - console.log("hiding bottom left ad"); - this.hide(); - } - } - - 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("all"); - console.log("Playwire spawn ad destroyed"); - }); - } 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`${translateText("spawn_ad.loading")}` - : ""} -
-
- `; - } -} diff --git a/src/client/graphics/layers/SpawnTimer.ts b/src/client/graphics/layers/SpawnTimer.ts index da96c89a1..393cf96d4 100644 --- a/src/client/graphics/layers/SpawnTimer.ts +++ b/src/client/graphics/layers/SpawnTimer.ts @@ -1,18 +1,34 @@ +import { LitElement, html } from "lit"; +import { customElement } from "lit/decorators.js"; import { GameMode, Team } from "../../../core/game/Game"; import { GameView } from "../../../core/game/GameView"; import { TransformHandler } from "../TransformHandler"; import { Layer } from "./Layer"; -export class SpawnTimer implements Layer { +@customElement("spawn-timer") +export class SpawnTimer extends LitElement implements Layer { + public game: GameView; + public transformHandler: TransformHandler; + private ratios = [0]; private colors = ["rgba(0, 128, 255, 0.7)", "rgba(0, 0, 0, 0.5)"]; - constructor( - private game: GameView, - private transformHandler: TransformHandler, - ) {} + private isVisible = false; - init() {} + createRenderRoot() { + this.style.position = "fixed"; + this.style.top = "0"; + this.style.left = "0"; + this.style.width = "100%"; + this.style.height = "7px"; + this.style.zIndex = "1000"; + this.style.pointerEvents = "none"; + return this; + } + + init() { + this.isVisible = true; + } tick() { if (this.game.inSpawnPhase()) { @@ -21,6 +37,7 @@ export class SpawnTimer implements Layer { this.game.ticks() / this.game.config().numSpawnPhaseTurns(), ]; this.colors = ["rgba(0, 128, 255, 0.7)"]; + this.requestUpdate(); return; } @@ -28,6 +45,7 @@ export class SpawnTimer implements Layer { this.colors = []; if (this.game.config().gameConfig().gameMode !== GameMode.Team) { + this.requestUpdate(); return; } @@ -41,44 +59,52 @@ export class SpawnTimer implements Layer { const theme = this.game.config().theme(); const total = sumIterator(teamTiles.values()); - if (total === 0) return; + if (total === 0) { + this.requestUpdate(); + return; + } for (const [team, count] of teamTiles) { const ratio = count / total; this.ratios.push(ratio); this.colors.push(theme.teamColor(team).toRgbString()); } + this.requestUpdate(); } shouldTransform(): boolean { return false; } - renderLayer(context: CanvasRenderingContext2D) { - if (this.ratios.length === 0 || this.colors.length === 0) return; + render() { + if (!this.isVisible) { + return html``; + } - const barHeight = 10; - const barWidth = this.transformHandler.width(); + if (this.ratios.length === 0 || this.colors.length === 0) { + return html``; + } if ( !this.game.inSpawnPhase() && this.game.config().gameConfig().gameMode !== GameMode.Team ) { - return; + return html``; } - let x = 0; - let filledRatio = 0; - for (let i = 0; i < this.ratios.length && i < this.colors.length; i++) { - const ratio = this.ratios[i] ?? 1 - filledRatio; - const segmentWidth = barWidth * ratio; - - context.fillStyle = this.colors[i]; - context.fillRect(x, 0, segmentWidth, barHeight); - - x += segmentWidth; - filledRatio += ratio; - } + return html` +
+ ${this.ratios.map((ratio, i) => { + const color = this.colors[i] || "rgba(0, 0, 0, 0.5)"; + return html` +
+ `; + })} +
+ `; } } diff --git a/src/client/graphics/layers/WinModal.ts b/src/client/graphics/layers/WinModal.ts index 809eb5465..3e042afe1 100644 --- a/src/client/graphics/layers/WinModal.ts +++ b/src/client/graphics/layers/WinModal.ts @@ -132,12 +132,7 @@ export class WinModal extends LitElement implements Layer { for (const pattern of Object.values(patterns?.patterns ?? {})) { for (const colorPalette of pattern.colorPalettes ?? []) { if ( - patternRelationship( - pattern, - colorPalette, - me !== false ? me : null, - null, - ) === "purchasable" + patternRelationship(pattern, colorPalette, me, null) === "purchasable" ) { const palette = patterns?.colorPalettes?.[colorPalette.name]; if (palette) { diff --git a/src/client/index.html b/src/client/index.html index 2dcf835e7..ae46d19e8 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -97,6 +97,13 @@ src="https://cdn.fuseplatform.net/publift/tags/2/4121/fuse.js" > + +