mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-06 17:46:48 +00:00
support for unlockable flags (#3479)
## Description: Add support for purchasable/gated flags. * Create a new "Store" modal that renders both skins & flags * move all store related logic out of TerritoryPatternsModal * use nation:code for existing nation flags & flag:key for gated flags * check if user has the appropriate flags before purchasing ## 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:
@@ -0,0 +1,97 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Product } from "../../core/CosmeticSchemas";
|
||||
import { translateCosmetic } from "../Cosmetics";
|
||||
import "./PurchaseButton";
|
||||
|
||||
export interface FlagItem {
|
||||
key: string;
|
||||
name: string;
|
||||
url: string;
|
||||
product?: Product | null;
|
||||
}
|
||||
|
||||
@customElement("flag-button")
|
||||
export class FlagButton extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
selected: boolean = false;
|
||||
|
||||
@property({ type: Object })
|
||||
flag!: FlagItem;
|
||||
|
||||
@property({ type: Boolean })
|
||||
requiresPurchase: boolean = false;
|
||||
|
||||
@property({ type: Function })
|
||||
onSelect?: (flagKey: string) => void;
|
||||
|
||||
@property({ type: Function })
|
||||
onPurchase?: () => void;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
this.onSelect?.(this.flag.key);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-col items-center justify-between gap-1 p-2 bg-white/5 backdrop-blur-sm border rounded-lg w-36 h-full transition-all duration-200 ${this
|
||||
.selected
|
||||
? "border-green-500 shadow-[0_0_15px_rgba(34,197,94,0.5)]"
|
||||
: "hover:bg-white/10 hover:border-white/20 hover:shadow-xl border-white/10"}"
|
||||
>
|
||||
<button
|
||||
class="group relative flex flex-col items-center w-full gap-1 rounded-lg cursor-pointer transition-all duration-200
|
||||
disabled:cursor-not-allowed flex-1"
|
||||
?disabled=${this.requiresPurchase}
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div
|
||||
class="text-[10px] font-bold text-white uppercase tracking-wider mb-0.5 text-center truncate w-full ${this
|
||||
.requiresPurchase
|
||||
? "opacity-50"
|
||||
: ""}"
|
||||
title="${translateCosmetic("flags", this.flag.name)}"
|
||||
>
|
||||
${translateCosmetic("flags", this.flag.name)}
|
||||
</div>
|
||||
<div class="h-[14px] mb-1 w-full"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full aspect-square flex items-center justify-center bg-white/5 rounded-lg p-2 border border-white/10 group-hover:border-white/20 transition-colors duration-200 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src=${this.flag.url}
|
||||
alt=${this.flag.name}
|
||||
class="w-full h-full object-contain pointer-events-none"
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
@error=${(e: Event) => {
|
||||
const img = e.currentTarget as HTMLImageElement;
|
||||
const fallback = "/flags/xx.svg";
|
||||
if (img.src && !img.src.endsWith(fallback)) {
|
||||
img.src = fallback;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
${this.requiresPurchase && this.flag.product
|
||||
? html`
|
||||
<purchase-button
|
||||
.product=${this.flag.product}
|
||||
.onPurchase=${() => this.onPurchase?.()}
|
||||
></purchase-button>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
} from "../../core/CosmeticSchemas";
|
||||
import { PatternDecoder } from "../../core/PatternDecoder";
|
||||
import { PlayerPattern } from "../../core/Schemas";
|
||||
import { translateCosmetic } from "../Cosmetics";
|
||||
import { translateText } from "../Utils";
|
||||
import "./PurchaseButton";
|
||||
|
||||
export const BUTTON_WIDTH = 150;
|
||||
|
||||
@@ -36,18 +38,6 @@ export class PatternButton extends LitElement {
|
||||
return this;
|
||||
}
|
||||
|
||||
private translateCosmetic(prefix: string, patternName: string): string {
|
||||
const translation = translateText(`${prefix}.${patternName}`);
|
||||
if (translation.startsWith(prefix)) {
|
||||
return patternName
|
||||
.split("_")
|
||||
.filter((word) => word.length > 0)
|
||||
.map((word) => word[0].toUpperCase() + word.substring(1))
|
||||
.join(" ");
|
||||
}
|
||||
return translation;
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
if (this.pattern === null) {
|
||||
this.onSelect?.(null);
|
||||
@@ -60,8 +50,7 @@ export class PatternButton extends LitElement {
|
||||
} satisfies PlayerPattern);
|
||||
}
|
||||
|
||||
private handlePurchase(e: Event) {
|
||||
e.stopPropagation();
|
||||
private handlePurchase() {
|
||||
if (this.pattern?.product) {
|
||||
this.onPurchase?.(this.pattern, this.colorPalette ?? null);
|
||||
}
|
||||
@@ -91,14 +80,14 @@ export class PatternButton extends LitElement {
|
||||
: ""}"
|
||||
title="${isDefaultPattern
|
||||
? translateText("territory_patterns.pattern.default")
|
||||
: this.translateCosmetic(
|
||||
: translateCosmetic(
|
||||
"territory_patterns.pattern",
|
||||
this.pattern!.name,
|
||||
)}"
|
||||
>
|
||||
${isDefaultPattern
|
||||
? translateText("territory_patterns.pattern.default")
|
||||
: this.translateCosmetic(
|
||||
: translateCosmetic(
|
||||
"territory_patterns.pattern",
|
||||
this.pattern!.name,
|
||||
)}
|
||||
@@ -111,7 +100,7 @@ export class PatternButton extends LitElement {
|
||||
? "opacity-50"
|
||||
: ""}"
|
||||
>
|
||||
${this.translateCosmetic(
|
||||
${translateCosmetic(
|
||||
"territory_patterns.color_palette",
|
||||
this.colorPalette!.name,
|
||||
)}
|
||||
@@ -139,18 +128,10 @@ export class PatternButton extends LitElement {
|
||||
|
||||
${this.requiresPurchase && this.pattern?.product
|
||||
? html`
|
||||
<div class="w-full mt-2">
|
||||
<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)]"
|
||||
@click=${this.handlePurchase}
|
||||
>
|
||||
${translateText("territory_patterns.purchase")}
|
||||
<span class="ml-1 text-white/60"
|
||||
>(${this.pattern.product.price})</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<purchase-button
|
||||
.product=${this.pattern.product}
|
||||
.onPurchase=${() => this.handlePurchase()}
|
||||
></purchase-button>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
|
||||
@@ -121,6 +121,11 @@ export class PlayPage extends LitElement {
|
||||
adaptive-size
|
||||
class="shrink-0 lg:hidden"
|
||||
></pattern-input>
|
||||
<flag-input
|
||||
id="flag-input-mobile"
|
||||
show-select-label
|
||||
class="shrink-0 lg:hidden h-10 w-10"
|
||||
></flag-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Product } from "../../core/CosmeticSchemas";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
@customElement("purchase-button")
|
||||
export class PurchaseButton extends LitElement {
|
||||
@property({ type: Object })
|
||||
product!: Product;
|
||||
|
||||
@property({ type: Function })
|
||||
onPurchase?: () => void;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private handleClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
this.onPurchase?.();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="no-crazygames w-full mt-2">
|
||||
<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)]"
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
${translateText("territory_patterns.purchase")}
|
||||
<span class="ml-1 text-white/60">(${this.product.price})</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user