mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:30:45 +00:00
matchmaking
This commit is contained in:
@@ -342,7 +342,9 @@
|
||||
"connecting": "Connecting to matchmaking server...",
|
||||
"searching": "Searching for game...",
|
||||
"waiting_for_game": "Waiting for game to start...",
|
||||
"elo": "Your ELO: {elo}"
|
||||
"elo": "Your ELO: {elo}",
|
||||
"ad_blocked_title": "Ad Blocker Detected",
|
||||
"ad_blocked_message": "Please disable your ad blocker or make a purchase to play ranked games."
|
||||
},
|
||||
"username": {
|
||||
"enter_username": "Enter your username",
|
||||
|
||||
@@ -69,6 +69,7 @@ export function joinLobby(
|
||||
lobbyConfig: LobbyConfig,
|
||||
onPrestart: () => void,
|
||||
onJoin: () => void,
|
||||
waitBeforeJoin: Promise<void> = Promise.resolve(),
|
||||
): (force?: boolean) => boolean {
|
||||
console.log(
|
||||
`joining lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}`,
|
||||
@@ -95,7 +96,7 @@ export function joinLobby(
|
||||
};
|
||||
let terrainLoad: Promise<TerrainMapData> | null = null;
|
||||
|
||||
const onmessage = (message: ServerMessage) => {
|
||||
const onmessage = async (message: ServerMessage) => {
|
||||
if (message.type === "prestart") {
|
||||
console.log(
|
||||
`lobby: game prestarting: ${JSON.stringify(message, replacer)}`,
|
||||
@@ -105,14 +106,15 @@ export function joinLobby(
|
||||
message.gameMapSize,
|
||||
terrainMapFileLoader,
|
||||
);
|
||||
onPrestart();
|
||||
waitBeforeJoin.then(onPrestart);
|
||||
}
|
||||
if (message.type === "start") {
|
||||
// Trigger prestart for singleplayer games
|
||||
onPrestart();
|
||||
waitBeforeJoin.then(onPrestart);
|
||||
console.log(
|
||||
`lobby: game started: ${JSON.stringify(message, replacer, 2)}`,
|
||||
);
|
||||
await waitBeforeJoin;
|
||||
onJoin();
|
||||
// For multiplayer games, GameStartInfo is not known until game starts.
|
||||
lobbyConfig.gameStartInfo = message.gameStartInfo;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -198,6 +216,8 @@ export interface JoinLobbyEvent {
|
||||
gameStartInfo?: GameStartInfo;
|
||||
// GameRecord exists when replaying an archived game.
|
||||
gameRecord?: GameRecord;
|
||||
|
||||
waitBeforeJoin?: Promise<void>;
|
||||
}
|
||||
|
||||
class Client {
|
||||
@@ -850,6 +870,7 @@ class Client {
|
||||
// Store current URL for popstate confirmation
|
||||
this.currentUrl = window.location.href;
|
||||
},
|
||||
lobby.waitBeforeJoin,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+110
-3
@@ -9,6 +9,7 @@ import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/Difficulties";
|
||||
import "./components/PatternButton";
|
||||
import { modalHeader } from "./components/ui/ModalHeader";
|
||||
import "./components/VideoAd";
|
||||
import { JoinLobbyEvent } from "./Main";
|
||||
import { translateText } from "./Utils";
|
||||
|
||||
@@ -19,7 +20,12 @@ export class MatchmakingModal extends BaseModal {
|
||||
@state() private connected = false;
|
||||
@state() private socket: WebSocket | null = null;
|
||||
@state() private gameID: string | null = null;
|
||||
@state() private videoAdComplete = false;
|
||||
@state() private adBlocked = false;
|
||||
private elo: number | "unknown" = "unknown";
|
||||
private adCompleteResolve: (() => void) | null = null;
|
||||
private adMidpointResolve: (() => void) | null = null;
|
||||
private adMidpointReached = false;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -30,9 +36,89 @@ export class MatchmakingModal extends BaseModal {
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleAdComplete = () => {
|
||||
this.videoAdComplete = true;
|
||||
if (this.adCompleteResolve) {
|
||||
this.adCompleteResolve();
|
||||
this.adCompleteResolve = null;
|
||||
}
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private handleAdMidpoint = () => {
|
||||
this.adMidpointReached = true;
|
||||
if (this.adMidpointResolve) {
|
||||
this.adMidpointResolve();
|
||||
this.adMidpointResolve = null;
|
||||
}
|
||||
};
|
||||
|
||||
private handleAdBlocked = () => {
|
||||
console.log("[Matchmaking] Ad blocked detected");
|
||||
this.adBlocked = true;
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
private waitForAdComplete = (): Promise<void> => {
|
||||
// If ad is already complete or ads are disabled, resolve immediately
|
||||
if (this.videoAdComplete || !window.adsEnabled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.adCompleteResolve = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
private waitForAdMidpoint = (): Promise<void> => {
|
||||
// If midpoint already reached or ads are disabled, resolve immediately
|
||||
if (this.adMidpointReached || !window.adsEnabled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
this.adMidpointResolve = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
private renderVideoAd() {
|
||||
console.log(
|
||||
"[Matchmaking] renderVideoAd, adsEnabled:",
|
||||
window.adsEnabled,
|
||||
"videoAdComplete:",
|
||||
this.videoAdComplete,
|
||||
"adBlocked:",
|
||||
this.adBlocked,
|
||||
);
|
||||
if (!window.adsEnabled || this.videoAdComplete) {
|
||||
return html``;
|
||||
}
|
||||
if (this.adBlocked) {
|
||||
return html`
|
||||
<div
|
||||
class="w-full flex flex-col items-center justify-center mb-4 px-6 py-8 bg-red-900/30 border border-red-500/50 rounded-lg"
|
||||
>
|
||||
<p class="text-red-400 text-lg font-semibold text-center mb-2">
|
||||
${translateText("matchmaking_modal.ad_blocked_title")}
|
||||
</p>
|
||||
<p class="text-white/70 text-sm text-center">
|
||||
${translateText("matchmaking_modal.ad_blocked_message")}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html`
|
||||
<div class="w-full flex justify-center mb-4 px-6">
|
||||
<video-ad
|
||||
.onComplete="${this.handleAdComplete}"
|
||||
.onMidpoint="${this.handleAdMidpoint}"
|
||||
.onAdBlocked="${this.handleAdBlocked}"
|
||||
></video-ad>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const eloDisplay = html`
|
||||
<p class="text-center mt-2 mb-4 text-white/60">
|
||||
<p class="text-center text-white/60">
|
||||
${translateText("matchmaking_modal.elo", { elo: this.elo })}
|
||||
</p>
|
||||
`;
|
||||
@@ -48,8 +134,8 @@ export class MatchmakingModal extends BaseModal {
|
||||
onBack: this.close,
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}
|
||||
<div class="flex-1 flex flex-col items-center justify-center gap-6 p-6">
|
||||
${eloDisplay} ${this.renderInner()}
|
||||
<div class="flex-1 flex flex-col items-center gap-4 p-6 pt-2">
|
||||
${eloDisplay} ${this.renderInner()} ${this.renderVideoAd()}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -71,6 +157,10 @@ export class MatchmakingModal extends BaseModal {
|
||||
}
|
||||
|
||||
private renderInner() {
|
||||
// Don't show spinner/status when ad is blocked
|
||||
if (this.adBlocked) {
|
||||
return html``;
|
||||
}
|
||||
if (!this.connected) {
|
||||
return html`
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
@@ -125,6 +215,17 @@ export class MatchmakingModal extends BaseModal {
|
||||
// otherwise the "searching" message will be shown immediately.
|
||||
// Also wait so people who back out immediately aren't added
|
||||
// to the matchmaking queue.
|
||||
|
||||
// Wait for ad midpoint before sending join request
|
||||
// This is so the ad doesn't get delay game start too long.
|
||||
await this.waitForAdMidpoint();
|
||||
|
||||
// Early return if modal was closed while waiting for ad
|
||||
if (!this.isModalOpen) {
|
||||
this.socket?.close();
|
||||
return;
|
||||
}
|
||||
|
||||
this.socket.send(
|
||||
JSON.stringify({
|
||||
type: "join",
|
||||
@@ -154,6 +255,11 @@ export class MatchmakingModal extends BaseModal {
|
||||
}
|
||||
|
||||
protected async onOpen(): Promise<void> {
|
||||
// Reset video ad state for each new matchmaking session
|
||||
this.videoAdComplete = false;
|
||||
this.adMidpointReached = false;
|
||||
this.adBlocked = false;
|
||||
|
||||
const userMe = await getUserMe();
|
||||
// Early return if modal was closed during async operation
|
||||
if (!this.isModalOpen) {
|
||||
@@ -232,6 +338,7 @@ export class MatchmakingModal extends BaseModal {
|
||||
detail: {
|
||||
gameID: this.gameID,
|
||||
clientID: generateID(),
|
||||
waitBeforeJoin: this.waitForAdComplete(),
|
||||
} as JoinLobbyEvent,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
console.log("[VideoAd] Module loaded");
|
||||
|
||||
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 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();
|
||||
console.log("[VideoAd] 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;
|
||||
}
|
||||
}
|
||||
|
||||
public showVideoAd(): void {
|
||||
if (!window.ramp) {
|
||||
// Wait for ramp to be available
|
||||
const checkRamp = setInterval(() => {
|
||||
if (window.ramp && window.ramp.que) {
|
||||
clearInterval(checkRamp);
|
||||
this.loadVideoAd();
|
||||
}
|
||||
}, 100);
|
||||
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
|
||||
window.ramp.onPlayerReady = () => {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user