mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 07:40:43 +00:00
Merge branch 'v29'
This commit is contained in:
@@ -267,6 +267,7 @@
|
||||
<spawn-timer></spawn-timer>
|
||||
<immunity-timer></immunity-timer>
|
||||
<in-game-header-ad></in-game-header-ad>
|
||||
<spawn-video-ad></spawn-video-ad>
|
||||
<game-info-modal></game-info-modal>
|
||||
<alert-frame></alert-frame>
|
||||
<chat-modal></chat-modal>
|
||||
|
||||
+34
-26
@@ -47,34 +47,42 @@ export async function fetchPlayerById(
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export async function getUserMe(): Promise<UserMeResponse | false> {
|
||||
try {
|
||||
const userAuthResult = await userAuth();
|
||||
if (!userAuthResult) return false;
|
||||
const { jwt } = userAuthResult;
|
||||
|
||||
// Get the user object
|
||||
const response = await fetch(getApiBase() + "/users/@me", {
|
||||
headers: {
|
||||
authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
if (response.status === 401) {
|
||||
await logOut();
|
||||
return false;
|
||||
}
|
||||
if (response.status !== 200) return false;
|
||||
const body = await response.json();
|
||||
const result = UserMeResponseSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
const error = z.prettifyError(result.error);
|
||||
console.error("Invalid response", error);
|
||||
return false;
|
||||
}
|
||||
return result.data;
|
||||
} catch (e) {
|
||||
return false;
|
||||
let __userMe: Promise<UserMeResponse | false> | null = null;
|
||||
export async function getUserMe(): Promise<UserMeResponse | false> {
|
||||
if (__userMe !== null) {
|
||||
return __userMe;
|
||||
}
|
||||
__userMe = (async () => {
|
||||
try {
|
||||
const userAuthResult = await userAuth();
|
||||
if (!userAuthResult) return false;
|
||||
const { jwt } = userAuthResult;
|
||||
|
||||
// Get the user object
|
||||
const response = await fetch(getApiBase() + "/users/@me", {
|
||||
headers: {
|
||||
authorization: `Bearer ${jwt}`,
|
||||
},
|
||||
});
|
||||
if (response.status === 401) {
|
||||
await logOut();
|
||||
return false;
|
||||
}
|
||||
if (response.status !== 200) return false;
|
||||
const body = await response.json();
|
||||
const result = UserMeResponseSchema.safeParse(body);
|
||||
if (!result.success) {
|
||||
const error = z.prettifyError(result.error);
|
||||
console.error("Invalid response", error);
|
||||
return false;
|
||||
}
|
||||
return result.data;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
return __userMe;
|
||||
}
|
||||
|
||||
export async function createCheckoutSession(
|
||||
|
||||
@@ -30,6 +30,18 @@ export async function handlePurchase(
|
||||
}
|
||||
|
||||
let __cosmetics: Promise<Cosmetics | null> | null = null;
|
||||
let __cosmeticsHash: string | null = null;
|
||||
|
||||
function simpleHash(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash = hash & hash;
|
||||
}
|
||||
return hash.toString(36);
|
||||
}
|
||||
|
||||
export async function fetchCosmetics(): Promise<Cosmetics | null> {
|
||||
if (__cosmetics !== null) {
|
||||
return __cosmetics;
|
||||
@@ -46,6 +58,11 @@ export async function fetchCosmetics(): Promise<Cosmetics | null> {
|
||||
console.error(`Invalid cosmetics: ${result.error.message}`);
|
||||
return null;
|
||||
}
|
||||
const patternKeys = Object.keys(result.data.patterns).sort();
|
||||
const hashInput = patternKeys
|
||||
.map((k) => k + (result.data.patterns[k].product ? "sale" : ""))
|
||||
.join(",");
|
||||
__cosmeticsHash = simpleHash(hashInput);
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
console.error("Error getting cosmetics:", error);
|
||||
@@ -55,6 +72,11 @@ export async function fetchCosmetics(): Promise<Cosmetics | null> {
|
||||
return __cosmetics;
|
||||
}
|
||||
|
||||
export async function getCosmeticsHash(): Promise<string | null> {
|
||||
await fetchCosmetics();
|
||||
return __cosmeticsHash;
|
||||
}
|
||||
|
||||
export function patternRelationship(
|
||||
pattern: Pattern,
|
||||
colorPalette: { name: string; isArchived?: boolean } | null,
|
||||
|
||||
@@ -178,6 +178,24 @@ declare global {
|
||||
slots?: any;
|
||||
};
|
||||
spaNewPage: (url?: string) => void;
|
||||
// Video ad methods
|
||||
onPlayerReady: (() => void) | null;
|
||||
addUnits: (units: Array<{ type: string }>) => Promise<void>;
|
||||
displayUnits: () => void;
|
||||
};
|
||||
Bolt: {
|
||||
on: (unitType: string, event: string, callback: () => void) => void;
|
||||
BOLT_AD_REQUEST_START: string;
|
||||
BOLT_AD_IMPRESSION: string;
|
||||
BOLT_AD_STARTED: string;
|
||||
BOLT_FIRST_QUARTILE: string;
|
||||
BOLT_MIDPOINT: string;
|
||||
BOLT_THIRD_QUARTILE: string;
|
||||
BOLT_AD_COMPLETE: string;
|
||||
BOLT_AD_ERROR: string;
|
||||
BOLT_AD_PAUSED: string;
|
||||
BOLT_AD_CLICKED: string;
|
||||
SHOW_HIDDEN_CONTAINER: string;
|
||||
};
|
||||
showPage?: (pageId: string) => void;
|
||||
}
|
||||
|
||||
+26
-24
@@ -15,24 +15,15 @@ import { translateText } from "./Utils";
|
||||
@customElement("matchmaking-modal")
|
||||
export class MatchmakingModal extends BaseModal {
|
||||
private gameCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private connectTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
@state() private connected = false;
|
||||
@state() private socket: WebSocket | null = null;
|
||||
@state() private gameID: string | null = null;
|
||||
private elo = "unknown";
|
||||
private elo: number | "unknown" = "unknown";
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.id = "page-matchmaking";
|
||||
document.addEventListener("userMeResponse", (event: Event) => {
|
||||
const customEvent = event as CustomEvent;
|
||||
if (customEvent.detail) {
|
||||
const userMeResponse = customEvent.detail as UserMeResponse;
|
||||
this.elo =
|
||||
userMeResponse.player?.leaderboard?.oneVone?.elo?.toString() ??
|
||||
"unknown";
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
@@ -125,18 +116,24 @@ export class MatchmakingModal extends BaseModal {
|
||||
);
|
||||
this.socket.onopen = async () => {
|
||||
console.log("Connected to matchmaking server");
|
||||
setTimeout(() => {
|
||||
this.connectTimeout = setTimeout(async () => {
|
||||
if (this.socket?.readyState !== WebSocket.OPEN) {
|
||||
console.warn("[Matchmaking] socket not ready");
|
||||
return;
|
||||
}
|
||||
// Set a delay so the user can see the "connecting" message,
|
||||
// otherwise the "searching" message will be shown immediately.
|
||||
// Also wait so people who back out immediately aren't added
|
||||
// to the matchmaking queue.
|
||||
this.socket.send(
|
||||
JSON.stringify({
|
||||
type: "join",
|
||||
jwt: await getPlayToken(),
|
||||
}),
|
||||
);
|
||||
this.connected = true;
|
||||
this.requestUpdate();
|
||||
}, 1000);
|
||||
this.socket?.send(
|
||||
JSON.stringify({
|
||||
type: "join",
|
||||
jwt: await getPlayToken(),
|
||||
}),
|
||||
);
|
||||
}, 2000);
|
||||
};
|
||||
this.socket.onmessage = (event) => {
|
||||
console.log(event.data);
|
||||
@@ -145,6 +142,7 @@ export class MatchmakingModal extends BaseModal {
|
||||
this.socket?.close();
|
||||
console.log(`matchmaking: got game ID: ${data.gameId}`);
|
||||
this.gameID = data.gameId;
|
||||
this.gameCheckInterval = setInterval(() => this.checkGame(), 1000);
|
||||
}
|
||||
};
|
||||
this.socket.onerror = (event: ErrorEvent) => {
|
||||
@@ -157,7 +155,6 @@ export class MatchmakingModal extends BaseModal {
|
||||
|
||||
protected async onOpen(): Promise<void> {
|
||||
const userMe = await getUserMe();
|
||||
|
||||
// Early return if modal was closed during async operation
|
||||
if (!this.isModalOpen) {
|
||||
return;
|
||||
@@ -180,15 +177,21 @@ export class MatchmakingModal extends BaseModal {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.elo = userMe.player.leaderboard?.oneVone?.elo ?? "unknown";
|
||||
|
||||
this.connected = false;
|
||||
this.gameID = null;
|
||||
this.connect();
|
||||
this.gameCheckInterval = setInterval(() => this.checkGame(), 1000);
|
||||
}
|
||||
|
||||
protected onClose(): void {
|
||||
this.connected = false;
|
||||
this.socket?.close();
|
||||
if (this.connectTimeout) {
|
||||
clearTimeout(this.connectTimeout);
|
||||
this.connectTimeout = null;
|
||||
}
|
||||
if (this.gameCheckInterval) {
|
||||
clearInterval(this.gameCheckInterval);
|
||||
this.gameCheckInterval = null;
|
||||
@@ -263,7 +266,7 @@ export class MatchmakingButton extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const button = this.isLoggedIn
|
||||
return this.isLoggedIn
|
||||
? html`
|
||||
<button
|
||||
@click="${this.handleLoggedInClick}"
|
||||
@@ -279,6 +282,7 @@ export class MatchmakingButton extends LitElement {
|
||||
${translateText("matchmaking_button.description")}
|
||||
</span>
|
||||
</button>
|
||||
<matchmaking-modal></matchmaking-modal>
|
||||
`
|
||||
: html`
|
||||
<button
|
||||
@@ -290,8 +294,6 @@ export class MatchmakingButton extends LitElement {
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
|
||||
return html` ${button} <matchmaking-modal></matchmaking-modal> `;
|
||||
}
|
||||
|
||||
private handleLoggedInClick() {
|
||||
|
||||
@@ -25,6 +25,16 @@ export abstract class BaseModal extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
if (this.modalEl) {
|
||||
this.modalEl.onClose = () => {
|
||||
if (this.isModalOpen) {
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.unregisterEscapeHandler();
|
||||
super.disconnectedCallback();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { getCosmeticsHash } from "../Cosmetics";
|
||||
import { getGamesPlayed } from "../Utils";
|
||||
|
||||
const HELP_SEEN_KEY = "helpSeen";
|
||||
const STORE_SEEN_HASH_KEY = "storeSeenHash";
|
||||
|
||||
@customElement("desktop-nav-bar")
|
||||
export class DesktopNavBar extends LitElement {
|
||||
@state() private _helpSeen = localStorage.getItem(HELP_SEEN_KEY) === "true";
|
||||
@state() private _hasNewCosmetics = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -23,6 +26,12 @@ export class DesktopNavBar extends LitElement {
|
||||
this._updateActiveState(current);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if cosmetics have changed
|
||||
getCosmeticsHash().then((hash: string | null) => {
|
||||
const seenHash = localStorage.getItem(STORE_SEEN_HASH_KEY);
|
||||
this._hasNewCosmetics = hash !== null && hash !== seenHash;
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
@@ -46,14 +55,29 @@ export class DesktopNavBar extends LitElement {
|
||||
}
|
||||
|
||||
private showHelpDot(): boolean {
|
||||
// Only show one dot at a time to prevent
|
||||
// overwhelming users.
|
||||
return getGamesPlayed() < 10 && !this._helpSeen;
|
||||
}
|
||||
|
||||
private showStoreDot(): boolean {
|
||||
return this._hasNewCosmetics && !this.showHelpDot();
|
||||
}
|
||||
|
||||
private onHelpClick = () => {
|
||||
localStorage.setItem(HELP_SEEN_KEY, "true");
|
||||
this._helpSeen = true;
|
||||
};
|
||||
|
||||
private onStoreClick = () => {
|
||||
this._hasNewCosmetics = false;
|
||||
getCosmeticsHash().then((hash: string | null) => {
|
||||
if (hash !== null) {
|
||||
localStorage.setItem(STORE_SEEN_HASH_KEY, hash);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<nav
|
||||
@@ -123,11 +147,18 @@ export class DesktopNavBar extends LitElement {
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
@click=${this.onStoreClick}
|
||||
></button>
|
||||
<span
|
||||
class="absolute -top-3 -right-2 bg-gradient-to-br from-red-600 to-red-700 text-white text-[9px] font-black tracking-wider px-2 py-0.5 rounded rotate-12 shadow-lg shadow-red-600/50 animate-pulse pointer-events-none"
|
||||
data-i18n="main.store_new_badge"
|
||||
></span>
|
||||
${this.showStoreDot()
|
||||
? html`
|
||||
<span
|
||||
class="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full animate-ping"
|
||||
></span>
|
||||
<span
|
||||
class="absolute -top-1 -right-1 w-2 h-2 bg-red-500 rounded-full"
|
||||
></span>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
const VIDEO_AD_UNIT_TYPE = "precontent_ad_video";
|
||||
|
||||
@customElement("video-ad")
|
||||
export class VideoAd extends LitElement {
|
||||
@state()
|
||||
private isVisible: boolean = true;
|
||||
|
||||
@property({ attribute: false })
|
||||
onComplete?: () => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
onMidpoint?: () => void;
|
||||
|
||||
@property({ attribute: false })
|
||||
onAdBlocked?: () => void;
|
||||
|
||||
private adLoadTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private rampCheckInterval: ReturnType<typeof setInterval> | null = null;
|
||||
private rampWaitTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
private adStarted = false;
|
||||
|
||||
// How long to wait for ad to start before assuming it's blocked
|
||||
private static readonly AD_LOAD_TIMEOUT_MS = 8000;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// Set dimensions on the custom element itself (required by Playwire)
|
||||
// Playwire requires explicit pixel dimensions, use max-width for responsiveness
|
||||
this.style.display = "block";
|
||||
this.style.width = "100%";
|
||||
this.style.maxWidth = "800px";
|
||||
this.style.aspectRatio = "16/9";
|
||||
this.showVideoAd();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
// Clean up timeout if component is removed
|
||||
if (this.adLoadTimeout) {
|
||||
clearTimeout(this.adLoadTimeout);
|
||||
this.adLoadTimeout = null;
|
||||
}
|
||||
if (this.rampCheckInterval) {
|
||||
clearInterval(this.rampCheckInterval);
|
||||
this.rampCheckInterval = null;
|
||||
}
|
||||
if (this.rampWaitTimeout) {
|
||||
clearTimeout(this.rampWaitTimeout);
|
||||
this.rampWaitTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
public showVideoAd(): void {
|
||||
if (!window.ramp) {
|
||||
// Wait for ramp to be available, but give up after timeout
|
||||
this.rampCheckInterval = setInterval(() => {
|
||||
if (window.ramp && window.ramp.que) {
|
||||
if (this.rampCheckInterval) {
|
||||
clearInterval(this.rampCheckInterval);
|
||||
this.rampCheckInterval = null;
|
||||
}
|
||||
if (this.rampWaitTimeout) {
|
||||
clearTimeout(this.rampWaitTimeout);
|
||||
this.rampWaitTimeout = null;
|
||||
}
|
||||
this.loadVideoAd();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Stop polling after timeout (e.g. adblocker preventing ramp from loading)
|
||||
this.rampWaitTimeout = setTimeout(() => {
|
||||
if (this.rampCheckInterval) {
|
||||
clearInterval(this.rampCheckInterval);
|
||||
this.rampCheckInterval = null;
|
||||
}
|
||||
console.log("[VideoAd] Ramp SDK never loaded - possible adblocker");
|
||||
this.handleAdBlocked();
|
||||
}, VideoAd.AD_LOAD_TIMEOUT_MS);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadVideoAd();
|
||||
}
|
||||
|
||||
private loadVideoAd(): void {
|
||||
// Start timeout to detect if ad doesn't load (e.g., due to adblocker)
|
||||
this.adLoadTimeout = setTimeout(() => {
|
||||
if (!this.adStarted) {
|
||||
console.log("[VideoAd] Ad load timeout - possible adblocker detected");
|
||||
this.handleAdBlocked();
|
||||
}
|
||||
}, VideoAd.AD_LOAD_TIMEOUT_MS);
|
||||
|
||||
// Set up event listeners when player is ready, chaining any existing handler
|
||||
const prevOnPlayerReady = window.ramp.onPlayerReady;
|
||||
window.ramp.onPlayerReady = () => {
|
||||
if (prevOnPlayerReady) prevOnPlayerReady();
|
||||
if (window.Bolt) {
|
||||
// Listen for ad start to know ad is loading successfully
|
||||
window.Bolt.on(
|
||||
VIDEO_AD_UNIT_TYPE,
|
||||
window.Bolt.BOLT_AD_STARTED ?? "boltAdStarted",
|
||||
() => {
|
||||
console.log("[VideoAd] Ad started");
|
||||
this.adStarted = true;
|
||||
// Clear the timeout since ad is playing
|
||||
if (this.adLoadTimeout) {
|
||||
clearTimeout(this.adLoadTimeout);
|
||||
this.adLoadTimeout = null;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_COMPLETE, () => {
|
||||
console.log("[VideoAd] Ad completed");
|
||||
this.hideElement();
|
||||
});
|
||||
|
||||
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_AD_ERROR, () => {
|
||||
console.log("[VideoAd] Ad error/no fill");
|
||||
this.handleAdBlocked();
|
||||
});
|
||||
|
||||
window.Bolt.on(VIDEO_AD_UNIT_TYPE, window.Bolt.BOLT_MIDPOINT, () => {
|
||||
console.log("[VideoAd] Ad midpoint");
|
||||
if (this.onMidpoint) {
|
||||
this.onMidpoint();
|
||||
}
|
||||
});
|
||||
|
||||
window.Bolt.on(
|
||||
VIDEO_AD_UNIT_TYPE,
|
||||
window.Bolt.SHOW_HIDDEN_CONTAINER ?? "showHiddenContainer",
|
||||
() => {
|
||||
console.log("[VideoAd] Ad finished");
|
||||
this.hideElement();
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Queue the video ad initialization
|
||||
window.ramp.que.push(() => {
|
||||
const pwUnits = [{ type: VIDEO_AD_UNIT_TYPE }];
|
||||
|
||||
window.ramp
|
||||
.addUnits(pwUnits)
|
||||
.then(() => {
|
||||
window.ramp.displayUnits();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
console.error("[VideoAd] Error adding units:", e);
|
||||
window.ramp.displayUnits();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private handleAdBlocked(): void {
|
||||
// Clear timeout if still pending
|
||||
if (this.adLoadTimeout) {
|
||||
clearTimeout(this.adLoadTimeout);
|
||||
this.adLoadTimeout = null;
|
||||
}
|
||||
|
||||
// Call the callback if provided
|
||||
if (this.onAdBlocked) {
|
||||
this.onAdBlocked();
|
||||
}
|
||||
}
|
||||
|
||||
private hideElement(): void {
|
||||
this.style.display = "none";
|
||||
this.isVisible = false;
|
||||
// Call the callback if provided
|
||||
if (this.onComplete) {
|
||||
this.onComplete();
|
||||
}
|
||||
// Also dispatch event for backwards compatibility
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("ad-complete", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isVisible) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Provide a container for the Playwire video player to render into
|
||||
// Structure matches Playwire example: wrapper > game-video-ad > precontent-video-location
|
||||
return html`
|
||||
<div
|
||||
class="game-video-ad"
|
||||
style="width: 100%; height: 100%; overflow: hidden;"
|
||||
>
|
||||
<div
|
||||
id="precontent-video-location"
|
||||
style="width: 100%; height: 100%;"
|
||||
></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import { ReplayPanel } from "./layers/ReplayPanel";
|
||||
import { SAMRadiusLayer } from "./layers/SAMRadiusLayer";
|
||||
import { SettingsModal } from "./layers/SettingsModal";
|
||||
import { SpawnTimer } from "./layers/SpawnTimer";
|
||||
import { SpawnVideoAd } from "./layers/SpawnVideoAd";
|
||||
import { StructureIconsLayer } from "./layers/StructureIconsLayer";
|
||||
import { StructureLayer } from "./layers/StructureLayer";
|
||||
import { TeamStats } from "./layers/TeamStats";
|
||||
@@ -252,6 +253,12 @@ export function createRenderer(
|
||||
}
|
||||
inGameHeaderAd.game = game;
|
||||
|
||||
const spawnVideoAd = document.querySelector("spawn-video-ad") as SpawnVideoAd;
|
||||
if (!(spawnVideoAd instanceof SpawnVideoAd)) {
|
||||
console.error("spawn video ad not found");
|
||||
}
|
||||
spawnVideoAd.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().
|
||||
@@ -296,6 +303,7 @@ export function createRenderer(
|
||||
headsUpMessage,
|
||||
multiTabModal,
|
||||
inGameHeaderAd,
|
||||
spawnVideoAd,
|
||||
alertFrame,
|
||||
performanceOverlay,
|
||||
];
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { crazyGamesSDK } from "src/client/CrazyGamesSDK";
|
||||
import { getGamesPlayed } from "src/client/Utils";
|
||||
import { GameType } from "src/core/game/Game";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import "../../components/VideoAd";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
@customElement("spawn-video-ad")
|
||||
export class SpawnVideoAd extends LitElement implements Layer {
|
||||
public game: GameView;
|
||||
|
||||
@state() private shouldShow = false;
|
||||
@state() private adComplete = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (
|
||||
!window.adsEnabled ||
|
||||
window.innerWidth < 768 ||
|
||||
crazyGamesSDK.isOnCrazyGames() ||
|
||||
this.game.config().gameConfig().gameType === GameType.Singleplayer ||
|
||||
getGamesPlayed() < 3 || // Don't show to new players
|
||||
getGamesPlayed() % 3 !== 0 // Only show 1 in 3 times
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.shouldShow = true;
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (this.adComplete) return;
|
||||
// Hide when spawn phase ends
|
||||
if (this.shouldShow && !this.game.inSpawnPhase()) {
|
||||
this.shouldShow = false;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private handleComplete = () => {
|
||||
this.adComplete = true;
|
||||
this.shouldShow = false;
|
||||
};
|
||||
|
||||
shouldTransform(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.shouldShow || this.adComplete) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="fixed bottom-0 left-0 z-[9999] pointer-events-auto">
|
||||
<video-ad
|
||||
style="width: 400px; max-width: 400px; height: 225px; aspect-ratio: auto;"
|
||||
.onComplete="${this.handleComplete}"
|
||||
></video-ad>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -293,17 +293,16 @@ export class GameServer {
|
||||
const parsed = ClientMessageSchema.safeParse(JSON.parse(message));
|
||||
if (!parsed.success) {
|
||||
const error = z.prettifyError(parsed.error);
|
||||
this.log.error("Failed to parse client message", error, {
|
||||
this.log.warn(`Failed to parse client message ${error}`, {
|
||||
clientID: client.clientID,
|
||||
});
|
||||
client.ws.send(
|
||||
JSON.stringify({
|
||||
type: "error",
|
||||
error,
|
||||
message,
|
||||
message: `Server could not parse message from client: ${message}`,
|
||||
} satisfies ServerErrorMessage),
|
||||
);
|
||||
client.ws.close(1002, "ClientMessageSchema");
|
||||
return;
|
||||
}
|
||||
const clientMsg = parsed.data;
|
||||
|
||||
Reference in New Issue
Block a user