mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-19 11:55:00 +00:00
Rewarded videos ads to test a skin (#3120)
## Description: Added rewarded video ads for skin trials via Playwire's manuallyCreateRewardUi API. Users can now click "Try me" to watch a video ad and receive a temporary skin trial. Upon completion a temporary flare is granted to the player so they have ~5 minutes to use the skin. added getPlayerCosmeticsRefs and getPlayerCosmetics to Cosmetics.ts to centralize cosmetic retrieval & validation. <img width="801" height="534" alt="Screenshot 2026-02-10 at 7 58 14 PM" src="https://github.com/user-attachments/assets/51cc378c-2feb-4692-8cf2-20ee54cea3b8" /> ## 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
This commit is contained in:
@@ -1,14 +1,17 @@
|
||||
import { Colord } from "colord";
|
||||
import { base64url } from "jose";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import {
|
||||
ColorPalette,
|
||||
DefaultPattern,
|
||||
Pattern,
|
||||
} from "../../core/CosmeticSchemas";
|
||||
import { UserSettings } from "../../core/game/UserSettings";
|
||||
import { PatternDecoder } from "../../core/PatternDecoder";
|
||||
import { PlayerPattern } from "../../core/Schemas";
|
||||
import { grantTemporaryFlare } from "../Api";
|
||||
import { showRewardedAd } from "../RewardedVideoPromo";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
export const BUTTON_WIDTH = 150;
|
||||
@@ -26,16 +29,61 @@ export class PatternButton extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
requiresPurchase: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
trialTimeRemaining: number = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
allowTrial: boolean = true;
|
||||
|
||||
@property({ type: Boolean })
|
||||
trialCooldown: boolean = false;
|
||||
|
||||
@property({ type: Function })
|
||||
onSelect?: (pattern: PlayerPattern | null) => void;
|
||||
|
||||
@property({ type: Function })
|
||||
onPurchase?: (pattern: Pattern, colorPalette: ColorPalette | null) => void;
|
||||
|
||||
private _countdownInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
@state()
|
||||
private _adLoading: boolean = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
updated(changedProperties: Map<string, unknown>) {
|
||||
if (changedProperties.has("trialTimeRemaining")) {
|
||||
this.setupCountdown();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.clearCountdown();
|
||||
}
|
||||
|
||||
private setupCountdown() {
|
||||
this.clearCountdown();
|
||||
if (this.trialTimeRemaining > 0) {
|
||||
this._countdownInterval = setInterval(() => {
|
||||
this.trialTimeRemaining--;
|
||||
if (this.trialTimeRemaining <= 0) {
|
||||
this.trialTimeRemaining = 0;
|
||||
this.clearCountdown();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private clearCountdown() {
|
||||
if (this._countdownInterval !== null) {
|
||||
clearInterval(this._countdownInterval);
|
||||
this._countdownInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
private translateCosmetic(prefix: string, patternName: string): string {
|
||||
const translation = translateText(`${prefix}.${patternName}`);
|
||||
if (translation.startsWith(prefix)) {
|
||||
@@ -60,6 +108,99 @@ export class PatternButton extends LitElement {
|
||||
} satisfies PlayerPattern);
|
||||
}
|
||||
|
||||
private async grantTrial() {
|
||||
const flare =
|
||||
this.colorPalette?.name === undefined
|
||||
? `pattern:${this.pattern!.name}`
|
||||
: `pattern:${this.pattern!.name}:${this.colorPalette.name}`;
|
||||
await grantTemporaryFlare(flare);
|
||||
new UserSettings().setSelectedPatternName(flare);
|
||||
alert(translateText("territory_patterns.trial_granted"));
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
private showSteamModal(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className =
|
||||
"fixed inset-0 bg-black/80 flex items-center justify-center z-[9999]";
|
||||
|
||||
let secondsLeft = 10;
|
||||
const updateContent = () => {
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-slate-900 border border-white/20 rounded-xl p-8 max-w-md text-center">
|
||||
<h2 class="text-2xl font-bold text-white mb-4">Wishlist on Steam!</h2>
|
||||
<p class="text-white/70 mb-6">${translateText("territory_patterns.steam_wishlist_prompt")}</p>
|
||||
<a
|
||||
href="https://store.steampowered.com/app/3560670"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-block px-6 py-3 bg-[#1b2838] hover:bg-[#2a475e] text-white font-bold rounded-lg mb-6 transition-colors"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658a3.387 3.387 0 0 1 1.912-.59c.064 0 .128.003.191.006l2.866-4.158v-.058c0-2.495 2.03-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.091 2.921c0 .054.003.108.003.163 0 1.871-1.523 3.393-3.394 3.393-1.646 0-3.02-1.179-3.33-2.74L.453 15.406C1.727 20.279 6.228 24 11.979 24 18.627 24 24 18.627 24 12S18.627 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012zm11.415-9.303a3.015 3.015 0 0 0-3.015-3.015 3.015 3.015 0 1 0 3.015 3.015zm-5.273-.005c0-1.248 1.013-2.26 2.262-2.26a2.26 2.26 0 1 1 0 4.52 2.261 2.261 0 0 1-2.262-2.26z"/>
|
||||
</svg>
|
||||
Wishlist on Steam
|
||||
</span>
|
||||
</a>
|
||||
<div class="text-white/50 text-sm">
|
||||
${translateText("territory_patterns.reward_countdown", { seconds: secondsLeft.toString() })}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
updateContent();
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
secondsLeft--;
|
||||
if (secondsLeft <= 0) {
|
||||
clearInterval(interval);
|
||||
overlay.remove();
|
||||
resolve();
|
||||
} else {
|
||||
updateContent();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleTryMe(e: Event) {
|
||||
e.stopPropagation();
|
||||
if (this.pattern === null || this._adLoading) return;
|
||||
|
||||
if (this.trialCooldown) {
|
||||
alert(translateText("territory_patterns.trial_cooldown"));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[PatternButton] handleTryMe called");
|
||||
this._adLoading = true;
|
||||
|
||||
try {
|
||||
console.log("[PatternButton] Calling showRewardedAd...");
|
||||
await showRewardedAd();
|
||||
console.log("[PatternButton] showRewardedAd resolved");
|
||||
await this.grantTrial();
|
||||
} catch (error) {
|
||||
console.error("[PatternButton] Rewarded ad failed:", error);
|
||||
// Show Steam wishlist modal with countdown
|
||||
await this.showSteamModal();
|
||||
await this.grantTrial();
|
||||
} finally {
|
||||
this._adLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private formatTimeRemaining(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
if (m > 0) return `${m}m ${s}s`;
|
||||
return `${s}s`;
|
||||
}
|
||||
|
||||
private handlePurchase(e: Event) {
|
||||
e.stopPropagation();
|
||||
if (this.pattern?.product) {
|
||||
@@ -137,9 +278,52 @@ export class PatternButton extends LitElement {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
${this.requiresPurchase && this.pattern?.product
|
||||
${(this.requiresPurchase || this.trialTimeRemaining > 0) &&
|
||||
this.pattern?.product
|
||||
? html`
|
||||
<div class="w-full mt-2">
|
||||
<div class="w-full mt-2 flex flex-col gap-2">
|
||||
${this.trialTimeRemaining > 0
|
||||
? html`
|
||||
<div
|
||||
class="w-full px-4 py-2 bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded-lg text-xs font-bold uppercase tracking-wider text-center"
|
||||
>
|
||||
${this.formatTimeRemaining(this.trialTimeRemaining)}
|
||||
${translateText("territory_patterns.trial_remaining")}
|
||||
</div>
|
||||
`
|
||||
: this.allowTrial
|
||||
? html`
|
||||
<button
|
||||
class="w-full px-4 py-2 bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded-lg text-xs font-bold uppercase tracking-wider cursor-pointer transition-all duration-200
|
||||
hover:bg-blue-500/30 hover:shadow-[0_0_15px_rgba(59,130,246,0.2)] flex items-center justify-center gap-2"
|
||||
@click=${this.handleTryMe}
|
||||
?disabled=${this._adLoading}
|
||||
>
|
||||
${this._adLoading
|
||||
? html`<svg
|
||||
class="animate-spin h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>`
|
||||
: translateText("territory_patterns.try_me")}
|
||||
</button>
|
||||
`
|
||||
: null}
|
||||
<button
|
||||
class="w-full px-4 py-2 bg-green-500/20 text-green-400 border border-green-500/30 rounded-lg text-xs font-bold uppercase tracking-wider cursor-pointer transition-all duration-200
|
||||
hover:bg-green-500/30 hover:shadow-[0_0_15px_rgba(74,222,128,0.2)]"
|
||||
|
||||
Reference in New Issue
Block a user