import { html, LitElement, nothing, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { Flag, Pack, Pattern, Skin, Subscription, } from "../../core/CosmeticSchemas"; import { PlayerPattern } from "../../core/Schemas"; import { PaymentMethod, ResolvedCosmetic, translateCosmetic, } from "../Cosmetics"; import { translateText } from "../Utils"; import "./CapIcon"; import "./CosmeticContainer"; import "./CosmeticInfo"; import { renderPatternPreview } from "./PatternPreview"; import "./PlutoniumIcon"; import { DEFAULT_DOLLAR_LABEL_KEY } from "./PurchaseButton"; @customElement("cosmetic-button") export class CosmeticButton extends LitElement { @property({ type: Object }) resolved!: ResolvedCosmetic; @property({ type: Boolean }) selected: boolean = false; @property({ type: Function }) onSelect?: (resolved: ResolvedCosmetic) => void; @property({ type: Function }) onPurchase?: (resolved: ResolvedCosmetic, method: PaymentMethod) => void; /** True if the user already has a subscription (any tier). */ @property({ type: Boolean }) userHasSubscription: boolean = false; /** Colour variants of one pattern; 2+ become clickable swatches. */ @property({ attribute: false }) variants?: ResolvedCosmetic[]; /** Key of the swatch the user has picked; null until they pick one. */ @state() private activeVariantKey: string | null = null; /** The variant currently previewed/purchased: picked swatch, else fallback. */ private get activeResolved(): ResolvedCosmetic { const variants = this.variants; if (variants && variants.length > 0) { return ( variants.find((v) => v.key === this.activeVariantKey) ?? variants[0] ); } return this.resolved; } createRenderRoot() { return this; } private handleClick() { this.onSelect?.(this.activeResolved); } private get displayName(): string { const c = this.activeResolved.cosmetic; if (c === null) { return translateText("territory_patterns.pattern.default"); } if ( this.activeResolved.type === "pattern" || this.activeResolved.type === "skin" ) { return translateCosmetic("territory_patterns.pattern", c.name); } if (this.activeResolved.type === "pack") { return (c as Pack).displayName; } if (this.activeResolved.type === "subscription") { return translateCosmetic("subscriptions", c.name); } return translateCosmetic("flags", c.name); } /** True when the variants carry colour palettes to show as swatches. */ private get hasColorRow(): boolean { return ( this.variants !== undefined && this.variants.some((v) => v.colorPalette !== null) ); } /** Row of clickable split-circle colour swatches, one per palette. */ private renderColorSwatches(): TemplateResult | typeof nothing { if (!this.hasColorRow) { return nothing; } const activeKey = this.activeResolved.key; return html`
${this.variants!.map((v) => { const primary = v.colorPalette?.primaryColor ?? "#ffffff"; const secondary = v.colorPalette?.secondaryColor ?? "#000000"; const isActive = v.key === activeKey; const label = v.colorPalette ? translateCosmetic( "territory_patterns.color_palette", v.colorPalette.name, ) : ""; const outline = isActive ? "0 0 0 2px rgba(255,255,255,0.95)" : "inset 0 0 0 1px rgba(255,255,255,0.2), 0 0 0 1px rgba(0,0,0,0.45)"; return html``; })}
`; } private renderPreview(): TemplateResult { if (this.activeResolved.type === "pattern") { const c = this.activeResolved.cosmetic; const playerPattern: PlayerPattern | null = c === null ? null : { name: c.name, patternData: (c as Pattern).pattern, colorPalette: this.activeResolved.colorPalette ?? undefined, }; return renderPatternPreview(playerPattern, 150, 150); } if (this.activeResolved.type === "skin") { const c = this.activeResolved.cosmetic as Skin | null; if (c === null) { // "Default" tile — visually consistent with pattern's default tile. return html`
${translateText("territory_patterns.pattern.default")}
`; } return html`${c.name}`; } if (this.activeResolved.type === "pack") { const pack = this.activeResolved.cosmetic as Pack; const isHard = pack.currency === "hard"; const icon = isHard ? html`` : html``; const colorClass = isHard ? "text-green-400" : "text-amber-700"; const currencyKey = isHard ? "cosmetics.hard" : "cosmetics.soft"; return html`
${icon} ${pack.amount.toLocaleString()} ${translateText(currencyKey)} ${pack.bonusAmount > 0 ? html`
${translateText("cosmetics.free", { numFree: pack.bonusAmount.toLocaleString(), })}
` : nothing}
`; } if (this.activeResolved.type === "subscription") { const sub = this.activeResolved.cosmetic as Subscription; return html`
${sub.description}
${sub.dailyHardCurrency.toLocaleString()} ${translateText("cosmetics.per_day")}
${sub.dailySoftCurrency.toLocaleString()} ${translateText("cosmetics.per_day")}
`; } const c = this.activeResolved.cosmetic as Flag; return html`${c.name} { const img = e.currentTarget as HTMLImageElement; const fallback = "/flags/xx.svg"; if (img.src && !img.src.endsWith(fallback)) { img.src = fallback; } }} />`; } render() { const active = this.activeResolved; const c = active.cosmetic; const priced = c as Pattern | Skin | Flag | Pack | null; const priceHard = priced?.priceHard; const priceSoft = priced?.priceSoft; const artist = priced?.artist; const isPurchasable = active.relationship === "purchasable"; const type = active.type; const isPattern = type === "pattern"; const isSkin = type === "skin"; const isOwnedSubscription = type === "subscription" && active.relationship === "owned"; const dollarLabelKey = type === "subscription" ? this.userHasSubscription ? "store.switch_button" : "store.subscribe_button" : DEFAULT_DOLLAR_LABEL_KEY; const priceSuffix = type === "subscription" ? translateText("store.price_per_month") : ""; const sizeClass = type === "flag" ? "gap-1 p-1.5 w-36" : "gap-2 p-3 w-48"; const crazygamesClass = isPattern || isSkin ? "no-crazygames " : ""; // Colour-row tiles top-align so the skin box, swatches and price buttons // line up across the grid; other tiles fill height with justify-between. const hasColorRow = this.hasColorRow; return html` this.onPurchase?.(this.activeResolved, "dollar") : undefined} .onPurchaseHard=${isPurchasable && priceHard !== undefined ? () => this.onPurchase?.(this.activeResolved, "hard") : undefined} .onPurchaseSoft=${isPurchasable && priceSoft !== undefined ? () => this.onPurchase?.(this.activeResolved, "soft") : undefined} .name=${this.displayName} > ${this.renderColorSwatches()} ${isOwnedSubscription ? html`
${translateText("store.current_plan")}
` : nothing}
`; } }