mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:32:21 +00:00
Add Rarity to cosmetic items (#3587)
## Description: https://github.com/user-attachments/assets/f2216dec-72aa-497a-89cc-169c2a40021e * Fortnite-style rarity system for cosmetics: New CosmeticContainer component applies tier-based visual styling (gradient backgrounds, glowing borders, hover effects) to flag and pattern cards across 5 rarity tiers: Common, Uncommon, Rare, Epic, and Legendary * Legendary hover effects: Scale-up animation, pulsing orange glow, shimmer sweep, rotating border sweep, corner sparkles, and screen dimming backdrop * Epic hover effects: Purple shimmer sweep glint on hover * Purchase button overhaul: Green ember particles on container hover (non-common only), 40-particle burst stream on button hover, pulsating green glow, shimmer streak animation, and loading spinner on click * Clickable cosmetic cards: Clicking anywhere on a purchasable card (not just the purchase button) triggers the purchase flow * Refactored components: ArtistInfo renamed to CosmeticInfo (now shows rarity and color palette in tooltip), * Forward-compatible rarity schema: rarity field uses .or(z.string()) so unknown backend values won't break the client ## 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:
@@ -920,7 +920,13 @@
|
||||
"selected": "selected"
|
||||
},
|
||||
"cosmetics": {
|
||||
"artist_label": "Artist:"
|
||||
"artist_label": "Artist:",
|
||||
"color_label": "Color:",
|
||||
"common": "Common",
|
||||
"uncommon": "Uncommon",
|
||||
"rare": "Rare",
|
||||
"epic": "Epic",
|
||||
"legendary": "Legendary"
|
||||
},
|
||||
"flag_input": {
|
||||
"title": "Select Flag",
|
||||
|
||||
@@ -40,12 +40,7 @@ export class FlagInputModal extends BaseModal {
|
||||
.map(
|
||||
([key, flag]) => html`
|
||||
<flag-button
|
||||
.flag=${{
|
||||
key: `flag:${key}`,
|
||||
name: flag.name,
|
||||
url: flag.url,
|
||||
artist: flag.artist,
|
||||
}}
|
||||
.flag=${{ ...flag, key: `flag:${key}` }}
|
||||
.selected=${selectedFlag === `flag:${key}`}
|
||||
.onSelect=${onSelect}
|
||||
></flag-button>
|
||||
@@ -87,7 +82,7 @@ export class FlagInputModal extends BaseModal {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="pt-1 flex flex-wrap gap-1.5 justify-center items-stretch content-start"
|
||||
class="flex flex-wrap gap-4 p-8 justify-center items-stretch content-start"
|
||||
>
|
||||
${noFlag} ${cosmeticFlags} ${countryFlags}
|
||||
</div>
|
||||
|
||||
+3
-9
@@ -159,7 +159,7 @@ export class StoreModal extends BaseModal {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-wrap gap-4 p-2 justify-center items-stretch content-start"
|
||||
class="flex flex-wrap gap-4 p-8 justify-center items-stretch content-start"
|
||||
>
|
||||
${buttons}
|
||||
</div>
|
||||
@@ -179,13 +179,7 @@ export class StoreModal extends BaseModal {
|
||||
const selectedFlag = new UserSettings().getFlag() ?? "";
|
||||
buttons.push(html`
|
||||
<flag-button
|
||||
.flag=${{
|
||||
key: `flag:${key}`,
|
||||
name: flag.name,
|
||||
url: flag.url,
|
||||
product: flag.product,
|
||||
artist: flag.artist,
|
||||
}}
|
||||
.flag=${{ ...flag, key: `flag:${key}` }}
|
||||
.selected=${selectedFlag === `flag:${key}`}
|
||||
.requiresPurchase=${rel === "purchasable"}
|
||||
.onPurchase=${() => handlePurchase(flag.product!)}
|
||||
@@ -203,7 +197,7 @@ export class StoreModal extends BaseModal {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-wrap gap-4 p-2 justify-center items-stretch content-start"
|
||||
class="flex flex-wrap gap-4 p-8 justify-center items-stretch content-start"
|
||||
>
|
||||
${buttons}
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ export class TerritoryPatternsModal extends BaseModal {
|
||||
return html`
|
||||
<div class="flex flex-col">
|
||||
<div
|
||||
class="flex flex-wrap gap-4 p-2 justify-center items-stretch content-start"
|
||||
class="flex flex-wrap gap-4 p-8 justify-center items-stretch content-start"
|
||||
>
|
||||
${buttons}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
@customElement("artist-info")
|
||||
export class ArtistInfo extends LitElement {
|
||||
@property({ type: String })
|
||||
artist?: string;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.artist) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="absolute -top-1 -right-1 z-10 group/artist"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-white/20 hover:bg-white/40 flex items-center justify-center cursor-help transition-colors duration-150"
|
||||
>
|
||||
<span class="text-xs font-bold text-white/70">?</span>
|
||||
</div>
|
||||
<div
|
||||
class="hidden group-hover/artist:block absolute top-7 right-0 bg-zinc-800 text-white text-xs px-2.5 py-1.5 rounded shadow-lg whitespace-nowrap z-20 border border-white/10"
|
||||
>
|
||||
${translateText("cosmetics.artist_label")} ${this.artist}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Product } from "../../core/CosmeticSchemas";
|
||||
import "./PurchaseButton";
|
||||
|
||||
type Rarity = "common" | "uncommon" | "rare" | "epic" | "legendary" | string;
|
||||
|
||||
interface RarityConfig {
|
||||
gradient: string;
|
||||
border: string;
|
||||
glow: string;
|
||||
hoverGlowSize: string;
|
||||
nameColor: string;
|
||||
legendary?: boolean;
|
||||
shimmer?: boolean;
|
||||
shimmerColor?: string; // rgb triplet e.g. "255,200,80"
|
||||
borderSweep?: boolean;
|
||||
borderSweepColor?: string; // rgb triplet e.g. "192,132,252"
|
||||
}
|
||||
|
||||
const rarityConfig: Record<string, RarityConfig> = {
|
||||
common: {
|
||||
gradient: "rgba(80,80,80,0.55)",
|
||||
border: "rgba(255,255,255,0.15)",
|
||||
glow: "rgba(255,255,255,0.5)",
|
||||
hoverGlowSize: "10px",
|
||||
nameColor: "rgba(255,255,255,0.7)",
|
||||
},
|
||||
uncommon: {
|
||||
gradient: "rgba(30,100,30,0.65)",
|
||||
border: "rgba(74,222,128,0.45)",
|
||||
glow: "rgba(74,222,128,0.6)",
|
||||
hoverGlowSize: "12px",
|
||||
nameColor: "rgba(255,255,255,1)",
|
||||
},
|
||||
rare: {
|
||||
gradient: "rgba(20,60,160,0.70)",
|
||||
border: "rgba(96,165,250,0.50)",
|
||||
glow: "rgba(96,165,250,0.7)",
|
||||
hoverGlowSize: "14px",
|
||||
nameColor: "rgba(255,255,255,1)",
|
||||
},
|
||||
epic: {
|
||||
gradient: "rgba(90,20,160,0.75)",
|
||||
border: "rgba(192,132,252,0.60)",
|
||||
glow: "rgba(192,132,252,0.85)",
|
||||
hoverGlowSize: "14px",
|
||||
nameColor: "rgba(255,255,255,1)",
|
||||
shimmer: true,
|
||||
shimmerColor: "192,132,252",
|
||||
},
|
||||
legendary: {
|
||||
gradient: "rgba(180,80,0,0.75)",
|
||||
border: "rgba(251,146,60,0.65)",
|
||||
glow: "rgba(251,146,60,0.95)",
|
||||
hoverGlowSize: "25px",
|
||||
nameColor: "rgba(255,255,255,1)",
|
||||
legendary: true,
|
||||
shimmer: true,
|
||||
shimmerColor: "255,200,80",
|
||||
borderSweep: true,
|
||||
borderSweepColor: "255,200,80",
|
||||
},
|
||||
};
|
||||
|
||||
const fallback = rarityConfig["common"];
|
||||
|
||||
const STYLE_ID = "cosmetic-container-styles";
|
||||
if (!document.getElementById(STYLE_ID)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ID;
|
||||
style.textContent = `
|
||||
@keyframes legendary-pulse {
|
||||
0% { box-shadow: 0 0 15px rgba(251,146,60,0.8), 0 0 30px rgba(251,146,60,0.4); }
|
||||
50% { box-shadow: 0 0 25px rgba(251,146,60,0.9), 0 0 45px rgba(251,146,60,0.5); }
|
||||
100% { box-shadow: 0 0 15px rgba(251,146,60,0.8), 0 0 30px rgba(251,146,60,0.4); }
|
||||
}
|
||||
@keyframes legendary-shimmer {
|
||||
0% { left: -60%; }
|
||||
100% { left: 160%; }
|
||||
}
|
||||
@keyframes legendary-border-sweep {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
@keyframes sparkle-twinkle-0 {
|
||||
0%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); }
|
||||
40%, 60% { opacity: 1; transform: scale(1.2) rotate(20deg); }
|
||||
}
|
||||
@keyframes sparkle-twinkle-1 {
|
||||
0%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); }
|
||||
30%, 55% { opacity: 1; transform: scale(1.1) rotate(-15deg); }
|
||||
}
|
||||
@keyframes sparkle-twinkle-2 {
|
||||
0%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); }
|
||||
45%, 65% { opacity: 1; transform: scale(1.3) rotate(10deg); }
|
||||
}
|
||||
@keyframes sparkle-twinkle-3 {
|
||||
0%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); }
|
||||
35%, 58% { opacity: 1; transform: scale(1.0) rotate(-20deg); }
|
||||
}
|
||||
.legendary-hovered {
|
||||
animation: legendary-pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
.legendary-shimmer.active {
|
||||
animation: legendary-shimmer 0.8s ease-in-out;
|
||||
}
|
||||
.legendary-border-sweep {
|
||||
animation: legendary-border-sweep 8s linear infinite;
|
||||
}
|
||||
.legendary-sparkle-0 { animation: sparkle-twinkle-0 1.6s ease-in-out infinite; }
|
||||
.legendary-sparkle-1 { animation: sparkle-twinkle-1 1.9s ease-in-out infinite 0.3s; }
|
||||
.legendary-sparkle-2 { animation: sparkle-twinkle-2 1.7s ease-in-out infinite 0.7s; }
|
||||
.legendary-sparkle-3 { animation: sparkle-twinkle-3 2.0s ease-in-out infinite 0.1s; }
|
||||
@keyframes cosmetic-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
.cosmetic-loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0,0,0,0.6);
|
||||
border-radius: 0.75rem;
|
||||
z-index: 20;
|
||||
}
|
||||
.cosmetic-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255,255,255,0.2);
|
||||
border-top-color: rgb(74,222,128);
|
||||
border-radius: 50%;
|
||||
animation: cosmetic-spin 0.8s linear infinite;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
@customElement("cosmetic-container")
|
||||
export class CosmeticContainer extends LitElement {
|
||||
@property({ type: String })
|
||||
rarity: Rarity = "common";
|
||||
|
||||
@property({ type: Boolean })
|
||||
selected: boolean = false;
|
||||
|
||||
@property({ type: String })
|
||||
name: string = "";
|
||||
|
||||
@property({ type: Object })
|
||||
product: Product | null = null;
|
||||
|
||||
@property({ type: Function })
|
||||
onPurchase?: () => void;
|
||||
|
||||
private static _backdrop: HTMLDivElement | null = null;
|
||||
private static _ensureBackdrop(): HTMLDivElement {
|
||||
if (!CosmeticContainer._backdrop) {
|
||||
const el = document.createElement("div");
|
||||
el.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0);
|
||||
z-index: 9;
|
||||
transition: background 0.3s ease;
|
||||
`;
|
||||
document.body.appendChild(el);
|
||||
CosmeticContainer._backdrop = el;
|
||||
}
|
||||
return CosmeticContainer._backdrop;
|
||||
}
|
||||
|
||||
private _shimmer: HTMLDivElement | null = null;
|
||||
private _borderSweep: HTMLDivElement | null = null;
|
||||
private _sparkles: HTMLDivElement[] = [];
|
||||
private _glowColor = fallback.glow;
|
||||
private _glowSize = fallback.hoverGlowSize;
|
||||
private _isLegendary = false;
|
||||
private _hasGlint = false;
|
||||
private _hasBorderSweep = false;
|
||||
private _loading = false;
|
||||
private _loadingOverlay: HTMLDivElement | null = null;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private applyHostStyles() {
|
||||
const cfg = rarityConfig[this.rarity] ?? fallback;
|
||||
this._glowColor = cfg.glow;
|
||||
this._glowSize = cfg.hoverGlowSize;
|
||||
this._isLegendary = !!cfg.legendary;
|
||||
this._hasGlint = !!cfg.shimmer;
|
||||
this._hasBorderSweep = !!cfg.borderSweep;
|
||||
|
||||
this.style.position = "relative";
|
||||
this.style.overflow = "hidden";
|
||||
this.style.background = `linear-gradient(to top, ${cfg.gradient} 0%, rgba(15,15,20,0.85) 100%)`;
|
||||
this.style.border = `1px solid ${this.selected ? cfg.glow : cfg.border}`;
|
||||
this.style.backdropFilter = "blur(8px)";
|
||||
this.style.borderRadius = "0.75rem";
|
||||
this.style.transition =
|
||||
"border-color 0.2s, background 0.2s, transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s";
|
||||
this.style.zIndex = "0";
|
||||
this.style.cursor = this.product ? "pointer" : "";
|
||||
|
||||
if (this.selected) {
|
||||
this.style.boxShadow = `0 0 18px ${cfg.glow}`;
|
||||
} else if (!this.classList.contains("legendary-hovered")) {
|
||||
this.style.boxShadow = "";
|
||||
}
|
||||
}
|
||||
|
||||
private _ensureLegendaryElements() {
|
||||
if (this._shimmer || this._borderSweep) return;
|
||||
|
||||
// Shimmer sweep — epic and legendary
|
||||
if (this._hasGlint) {
|
||||
const shimmer = document.createElement("div");
|
||||
shimmer.className = "legendary-shimmer";
|
||||
shimmer.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -60%;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(${(rarityConfig[this.rarity] ?? fallback).shimmerColor ?? "255,200,80"},0.45) 50%, transparent 100%);
|
||||
transform: skewX(-15deg);
|
||||
z-index: 10;
|
||||
display: none;
|
||||
`;
|
||||
this.appendChild(shimmer);
|
||||
this._shimmer = shimmer;
|
||||
}
|
||||
|
||||
if (!this._hasBorderSweep) return;
|
||||
const sweepWrap = document.createElement("div");
|
||||
sweepWrap.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
inset: -2px;
|
||||
border-radius: 0.85rem;
|
||||
z-index: -1;
|
||||
overflow: hidden;
|
||||
display: none;
|
||||
`;
|
||||
const sweepInner = document.createElement("div");
|
||||
sweepInner.className = "legendary-border-sweep";
|
||||
const sc =
|
||||
(rarityConfig[this.rarity] ?? fallback).borderSweepColor ?? "255,200,80";
|
||||
sweepInner.style.cssText = `
|
||||
position: absolute;
|
||||
inset: -100%;
|
||||
background: conic-gradient(
|
||||
from 0deg,
|
||||
transparent 0deg,
|
||||
rgba(${sc},0.0) 60deg,
|
||||
rgba(${sc},0.9) 120deg,
|
||||
rgba(${sc},1) 180deg,
|
||||
rgba(${sc},0.9) 240deg,
|
||||
rgba(${sc},0.0) 300deg,
|
||||
transparent 360deg
|
||||
);
|
||||
`;
|
||||
// Inner mask to hide center, show only border ring
|
||||
const sweepMask = document.createElement("div");
|
||||
sweepMask.style.cssText = `
|
||||
position: absolute;
|
||||
inset: 2px;
|
||||
border-radius: 0.75rem;
|
||||
background: transparent;
|
||||
`;
|
||||
sweepWrap.appendChild(sweepInner);
|
||||
sweepWrap.appendChild(sweepMask);
|
||||
this.appendChild(sweepWrap);
|
||||
this._borderSweep = sweepWrap;
|
||||
|
||||
// Corner sparkles ✦
|
||||
const corners = [
|
||||
{ top: "4px", left: "4px" },
|
||||
{ top: "4px", right: "4px" },
|
||||
{ bottom: "4px", left: "4px" },
|
||||
{ bottom: "4px", right: "4px" },
|
||||
];
|
||||
this._sparkles = corners.map((pos, i) => {
|
||||
const el = document.createElement("div");
|
||||
el.className = `legendary-sparkle-${i}`;
|
||||
el.textContent = "✦";
|
||||
el.style.cssText = `
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
font-size: 10px;
|
||||
color: rgba(255,220,100,0.9);
|
||||
text-shadow: 0 0 6px rgba(255,200,60,1);
|
||||
z-index: 11;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
line-height: 1;
|
||||
`;
|
||||
Object.assign(el.style, pos);
|
||||
this.appendChild(el);
|
||||
return el;
|
||||
});
|
||||
}
|
||||
|
||||
private _onClick = () => {
|
||||
if (CosmeticContainer._backdrop) {
|
||||
CosmeticContainer._backdrop.style.background = "rgba(0,0,0,0)";
|
||||
}
|
||||
if (this.product && this.onPurchase && !this._loading) {
|
||||
this._loading = true;
|
||||
this._showLoadingOverlay();
|
||||
Promise.resolve(this.onPurchase()).catch(() => {
|
||||
this._hideLoadingOverlay();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private _showLoadingOverlay() {
|
||||
if (this._loadingOverlay) return;
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "cosmetic-loading-overlay";
|
||||
overlay.innerHTML = `<div class="cosmetic-loading-spinner"></div>`;
|
||||
this.appendChild(overlay);
|
||||
this._loadingOverlay = overlay;
|
||||
}
|
||||
|
||||
private _hideLoadingOverlay() {
|
||||
this._loadingOverlay?.remove();
|
||||
this._loadingOverlay = null;
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
private _onMouseEnter = () => {
|
||||
if (this._hasGlint || this._hasBorderSweep) {
|
||||
this._ensureLegendaryElements();
|
||||
}
|
||||
if (this._isLegendary) {
|
||||
this.style.transform = "scale(1.12)";
|
||||
this.style.zIndex = "10";
|
||||
this.classList.add("legendary-hovered");
|
||||
this._sparkles.forEach((s) => (s.style.display = "block"));
|
||||
CosmeticContainer._ensureBackdrop().style.background = "rgba(0,0,0,0.6)";
|
||||
}
|
||||
if (this._hasBorderSweep && this._borderSweep) {
|
||||
this._borderSweep.style.display = "block";
|
||||
}
|
||||
if (this._hasGlint && this._shimmer) {
|
||||
this._shimmer.style.display = "block";
|
||||
this._shimmer.classList.remove("active");
|
||||
void this._shimmer.offsetWidth;
|
||||
this._shimmer.classList.add("active");
|
||||
}
|
||||
if (!this._isLegendary && !this.selected) {
|
||||
this.style.boxShadow = `0 0 ${this._glowSize} ${this._glowColor}`;
|
||||
}
|
||||
};
|
||||
|
||||
private _onMouseLeave = () => {
|
||||
if (this._isLegendary) {
|
||||
this.style.transform = "";
|
||||
this.style.zIndex = "0";
|
||||
this.classList.remove("legendary-hovered");
|
||||
this._sparkles.forEach((s) => (s.style.display = "none"));
|
||||
if (CosmeticContainer._backdrop) {
|
||||
CosmeticContainer._backdrop.style.background = "rgba(0,0,0,0)";
|
||||
}
|
||||
}
|
||||
if (this._hasGlint && this._shimmer) this._shimmer.style.display = "none";
|
||||
if (this._hasBorderSweep && this._borderSweep)
|
||||
this._borderSweep.style.display = "none";
|
||||
if (!this.selected) this.style.boxShadow = "";
|
||||
};
|
||||
|
||||
private _nameEl: HTMLDivElement | null = null;
|
||||
|
||||
private _updateNameEl() {
|
||||
if (this.name) {
|
||||
this._nameEl ??= document.createElement("div");
|
||||
const cfg = rarityConfig[this.rarity] ?? fallback;
|
||||
this._nameEl.className = `text-xs font-bold uppercase tracking-wider text-center truncate w-full`;
|
||||
this._nameEl.style.color = cfg.nameColor;
|
||||
this._nameEl.title = this.name;
|
||||
this._nameEl.textContent = this.name;
|
||||
// Always ensure it's the first child
|
||||
if (this.firstChild !== this._nameEl) {
|
||||
this.prepend(this._nameEl);
|
||||
}
|
||||
} else if (this._nameEl) {
|
||||
this._nameEl.remove();
|
||||
this._nameEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.applyHostStyles();
|
||||
this._updateNameEl();
|
||||
this.addEventListener("mouseenter", this._onMouseEnter);
|
||||
this.addEventListener("mouseleave", this._onMouseLeave);
|
||||
this.addEventListener("click", this._onClick);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener("mouseenter", this._onMouseEnter);
|
||||
this.removeEventListener("mouseleave", this._onMouseLeave);
|
||||
this.removeEventListener("click", this._onClick);
|
||||
}
|
||||
|
||||
updated() {
|
||||
this.applyHostStyles();
|
||||
this._updateNameEl();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<slot></slot>
|
||||
${this.product && this.onPurchase
|
||||
? html`<purchase-button
|
||||
.product=${this.product}
|
||||
.rarity=${this.rarity}
|
||||
.onPurchase=${this.onPurchase}
|
||||
></purchase-button>`
|
||||
: null}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateCosmetic } from "../Cosmetics";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
const rarityColors: Record<string, string> = {
|
||||
common: "text-white/60",
|
||||
uncommon: "text-green-400",
|
||||
rare: "text-blue-400",
|
||||
epic: "text-purple-300",
|
||||
legendary: "text-orange-400",
|
||||
};
|
||||
|
||||
@customElement("cosmetic-info")
|
||||
export class CosmeticInfo extends LitElement {
|
||||
@property({ type: String })
|
||||
artist?: string;
|
||||
|
||||
@property({ type: String })
|
||||
rarity?: string;
|
||||
|
||||
@property({ type: String })
|
||||
colorPalette?: string;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.artist && !this.rarity && !this.colorPalette) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const rarityColor = rarityColors[this.rarity ?? ""] ?? "text-white/70";
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="absolute -top-1 -right-1 z-10 group/artist"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
class="w-6 h-6 rounded-full bg-white/20 hover:bg-white/40 flex items-center justify-center cursor-help transition-colors duration-150"
|
||||
>
|
||||
<span class="text-xs font-bold text-white/70">?</span>
|
||||
</div>
|
||||
<div
|
||||
class="hidden group-hover/artist:block absolute top-7 right-0 bg-zinc-800 text-white text-xs px-2.5 py-1.5 rounded shadow-lg whitespace-nowrap z-20 border border-white/10 flex flex-col gap-0.5"
|
||||
>
|
||||
${this.rarity
|
||||
? html`<div
|
||||
class="font-bold uppercase tracking-wider ${rarityColor}"
|
||||
>
|
||||
${translateText(`cosmetics.${this.rarity}`) || this.rarity}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.colorPalette
|
||||
? html`<div>
|
||||
${translateText("cosmetics.color_label")}
|
||||
${translateCosmetic(
|
||||
"territory_patterns.color_palette",
|
||||
this.colorPalette,
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this.artist
|
||||
? html`<div>
|
||||
${translateText("cosmetics.artist_label")} ${this.artist}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { Product } from "../../core/CosmeticSchemas";
|
||||
import { Flag } from "../../core/CosmeticSchemas";
|
||||
import { translateCosmetic } from "../Cosmetics";
|
||||
import "./ArtistInfo";
|
||||
import "./PurchaseButton";
|
||||
import "./CosmeticContainer";
|
||||
import "./CosmeticInfo";
|
||||
|
||||
export interface FlagItem {
|
||||
key: string;
|
||||
name: string;
|
||||
url: string;
|
||||
product?: Product | null;
|
||||
artist?: string;
|
||||
}
|
||||
export type FlagItem = Flag & { key: string };
|
||||
|
||||
@customElement("flag-button")
|
||||
export class FlagButton extends LitElement {
|
||||
@@ -35,35 +29,33 @@ export class FlagButton extends LitElement {
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
if (this.requiresPurchase) {
|
||||
this.onPurchase?.();
|
||||
return;
|
||||
}
|
||||
this.onSelect?.(this.flag.key);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="flex flex-col items-center justify-between gap-1 p-1.5 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"}"
|
||||
<cosmetic-container
|
||||
class="flex flex-col items-center justify-between gap-1 p-1.5 w-36 h-full"
|
||||
.rarity=${this.flag.rarity ?? "common"}
|
||||
.selected=${this.selected}
|
||||
.product=${this.requiresPurchase && this.flag.product
|
||||
? this.flag.product
|
||||
: null}
|
||||
.onPurchase=${() => this.onPurchase?.()}
|
||||
.name=${translateCosmetic("flags", this.flag.name)}
|
||||
>
|
||||
<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}
|
||||
class="group relative flex flex-col items-center w-full gap-1 rounded-lg cursor-pointer transition-all duration-200 flex-1"
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<artist-info .artist=${this.flag.artist}></artist-info>
|
||||
<div
|
||||
class="text-[10px] font-bold text-white uppercase tracking-wider mt-1 ${this
|
||||
.flag.artist
|
||||
? "pr-5"
|
||||
: ""} text-center truncate w-full ${this.requiresPurchase
|
||||
? "opacity-50"
|
||||
: ""}"
|
||||
title="${translateCosmetic("flags", this.flag.name)}"
|
||||
>
|
||||
${translateCosmetic("flags", this.flag.name)}
|
||||
</div>
|
||||
<cosmetic-info
|
||||
.artist=${this.flag.artist}
|
||||
.rarity=${this.flag.rarity}
|
||||
></cosmetic-info>
|
||||
|
||||
<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"
|
||||
@@ -84,16 +76,7 @@ export class FlagButton extends LitElement {
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
${this.requiresPurchase && this.flag.product
|
||||
? html`
|
||||
<purchase-button
|
||||
.product=${this.flag.product}
|
||||
.onPurchase=${() => this.onPurchase?.()}
|
||||
></purchase-button>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
</cosmetic-container>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { PatternDecoder } from "../../core/PatternDecoder";
|
||||
import { PlayerPattern } from "../../core/Schemas";
|
||||
import { translateCosmetic } from "../Cosmetics";
|
||||
import { translateText } from "../Utils";
|
||||
import "./ArtistInfo";
|
||||
import "./PurchaseButton";
|
||||
import "./CosmeticContainer";
|
||||
import "./CosmeticInfo";
|
||||
|
||||
export const BUTTON_WIDTH = 150;
|
||||
|
||||
@@ -40,6 +40,10 @@ export class PatternButton extends LitElement {
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
if (this.requiresPurchase) {
|
||||
this.handlePurchase();
|
||||
return;
|
||||
}
|
||||
if (this.pattern === null) {
|
||||
this.onSelect?.(null);
|
||||
return;
|
||||
@@ -61,57 +65,27 @@ export class PatternButton extends LitElement {
|
||||
const isDefaultPattern = this.pattern === null;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="no-crazygames flex flex-col items-center justify-between gap-2 p-3 bg-white/5 backdrop-blur-sm border rounded-xl w-48 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"}"
|
||||
<cosmetic-container
|
||||
class="no-crazygames flex flex-col items-center justify-between gap-2 p-3 w-48 h-full"
|
||||
.rarity=${this.pattern?.rarity ?? "common"}
|
||||
.selected=${this.selected}
|
||||
.product=${this.requiresPurchase && this.pattern?.product
|
||||
? this.pattern.product
|
||||
: null}
|
||||
.onPurchase=${() => this.handlePurchase()}
|
||||
.name=${isDefaultPattern
|
||||
? translateText("territory_patterns.pattern.default")
|
||||
: translateCosmetic("territory_patterns.pattern", this.pattern!.name)}
|
||||
>
|
||||
<button
|
||||
class="group relative flex flex-col items-center w-full gap-2 rounded-lg cursor-pointer transition-all duration-200
|
||||
disabled:cursor-not-allowed flex-1"
|
||||
?disabled=${this.requiresPurchase}
|
||||
class="group relative flex flex-col items-center w-full gap-2 rounded-lg cursor-pointer transition-all duration-200 flex-1"
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<artist-info .artist=${this.pattern?.artist}></artist-info>
|
||||
<div class="flex flex-col items-center w-full">
|
||||
<div
|
||||
class="text-xs font-bold text-white uppercase tracking-wider mb-1 ${this
|
||||
.pattern?.artist
|
||||
? "pr-5"
|
||||
: ""} text-center truncate w-full ${this.requiresPurchase
|
||||
? "opacity-50"
|
||||
: ""}"
|
||||
title="${isDefaultPattern
|
||||
? translateText("territory_patterns.pattern.default")
|
||||
: translateCosmetic(
|
||||
"territory_patterns.pattern",
|
||||
this.pattern!.name,
|
||||
)}"
|
||||
>
|
||||
${isDefaultPattern
|
||||
? translateText("territory_patterns.pattern.default")
|
||||
: translateCosmetic(
|
||||
"territory_patterns.pattern",
|
||||
this.pattern!.name,
|
||||
)}
|
||||
</div>
|
||||
${this.colorPalette !== null
|
||||
? html`
|
||||
<div
|
||||
class="text-[10px] font-bold text-white/40 uppercase tracking-widest mb-2 text-center truncate w-full ${this
|
||||
.requiresPurchase
|
||||
? "opacity-50"
|
||||
: ""}"
|
||||
>
|
||||
${translateCosmetic(
|
||||
"territory_patterns.color_palette",
|
||||
this.colorPalette!.name,
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: html`<div class="h-[22px] mb-2 w-full"></div>`}
|
||||
</div>
|
||||
<cosmetic-info
|
||||
.artist=${this.pattern?.artist}
|
||||
.rarity=${this.pattern?.rarity}
|
||||
.colorPalette=${this.colorPalette?.name ?? undefined}
|
||||
></cosmetic-info>
|
||||
|
||||
<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"
|
||||
@@ -129,16 +103,7 @@ export class PatternButton extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
${this.requiresPurchase && this.pattern?.product
|
||||
? html`
|
||||
<purchase-button
|
||||
.product=${this.pattern.product}
|
||||
.onPurchase=${() => this.handlePurchase()}
|
||||
></purchase-button>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
</cosmetic-container>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,156 @@ import { customElement, property } from "lit/decorators.js";
|
||||
import { Product } from "../../core/CosmeticSchemas";
|
||||
import { translateText } from "../Utils";
|
||||
|
||||
const PURCHASE_STYLE_ID = "purchase-button-styles";
|
||||
if (!document.getElementById(PURCHASE_STYLE_ID)) {
|
||||
const style = document.createElement("style");
|
||||
style.id = PURCHASE_STYLE_ID;
|
||||
style.textContent = `
|
||||
@keyframes purchase-streak {
|
||||
0% { left: -60%; opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { left: 160%; opacity: 0; }
|
||||
}
|
||||
.purchase-sparkle-streak {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -60%;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent 0%, rgba(134,239,172,0.5) 50%, transparent 100%);
|
||||
transform: skewX(-15deg);
|
||||
opacity: 0;
|
||||
}
|
||||
cosmetic-container:hover .purchase-sparkle-streak {
|
||||
animation: purchase-streak 0.7s ease-in-out;
|
||||
}
|
||||
cosmetic-container:hover .purchase-sparkle-btn {
|
||||
background: rgb(34,197,94);
|
||||
border-color: rgb(74,222,128);
|
||||
color: white;
|
||||
box-shadow: 0 0 20px rgba(74,222,128,0.6);
|
||||
}
|
||||
@keyframes purchase-pulse {
|
||||
0% { box-shadow: 0 0 15px rgba(74,222,128,0.6), 0 0 30px rgba(34,197,94,0.3); }
|
||||
50% { box-shadow: 0 0 25px rgba(74,222,128,0.9), 0 0 50px rgba(34,197,94,0.5); }
|
||||
100% { box-shadow: 0 0 15px rgba(74,222,128,0.6), 0 0 30px rgba(34,197,94,0.3); }
|
||||
}
|
||||
.purchase-sparkle-btn:hover {
|
||||
background: rgb(22,163,74) !important;
|
||||
border-color: rgb(74,222,128) !important;
|
||||
color: white !important;
|
||||
animation: purchase-pulse 1.2s ease-in-out infinite !important;
|
||||
}
|
||||
@keyframes purchase-ember-0 {
|
||||
0% { transform: translateY(0) translateX(0) scale(1); opacity: 0.9; }
|
||||
100% { transform: translateY(-35px) translateX(5px) scale(0.2); opacity: 0; }
|
||||
}
|
||||
@keyframes purchase-ember-1 {
|
||||
0% { transform: translateY(0) translateX(0) scale(1); opacity: 0.9; }
|
||||
100% { transform: translateY(-30px) translateX(-6px) scale(0.3); opacity: 0; }
|
||||
}
|
||||
@keyframes purchase-ember-2 {
|
||||
0% { transform: translateY(0) translateX(0) scale(1); opacity: 0.9; }
|
||||
100% { transform: translateY(-40px) translateX(3px) scale(0.2); opacity: 0; }
|
||||
}
|
||||
@keyframes purchase-ember-3 {
|
||||
0% { transform: translateY(0) translateX(0) scale(1); opacity: 0.9; }
|
||||
100% { transform: translateY(-28px) translateX(-4px) scale(0.3); opacity: 0; }
|
||||
}
|
||||
.purchase-ember {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
border-radius: 50%;
|
||||
background: rgba(74,222,128,0.9);
|
||||
box-shadow: 0 0 4px rgba(74,222,128,0.8);
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
.purchase-ember-0 { left: 20%; animation: purchase-ember-0 1.2s ease-out infinite; }
|
||||
.purchase-ember-1 { left: 40%; animation: purchase-ember-1 1.5s ease-out infinite 0.25s; }
|
||||
.purchase-ember-2 { left: 60%; animation: purchase-ember-2 1.3s ease-out infinite 0.5s; }
|
||||
.purchase-ember-3 { left: 80%; animation: purchase-ember-3 1.6s ease-out infinite 0.15s; }
|
||||
cosmetic-container:hover .purchase-ember {
|
||||
display: block;
|
||||
}
|
||||
@keyframes purchase-burst-a { 0% { transform: translateY(0) translateX(0) scale(1.2); opacity:1; } 100% { transform: translateY(-70px) translateX(14px) scale(0); opacity:0; } }
|
||||
@keyframes purchase-burst-b { 0% { transform: translateY(0) translateX(0) scale(1.2); opacity:1; } 100% { transform: translateY(-60px) translateX(-12px) scale(0); opacity:0; } }
|
||||
@keyframes purchase-burst-c { 0% { transform: translateY(0) translateX(0) scale(1.2); opacity:1; } 100% { transform: translateY(-80px) translateX(8px) scale(0); opacity:0; } }
|
||||
@keyframes purchase-burst-d { 0% { transform: translateY(0) translateX(0) scale(1.2); opacity:1; } 100% { transform: translateY(-55px) translateX(-16px) scale(0); opacity:0; } }
|
||||
@keyframes purchase-burst-e { 0% { transform: translateY(0) translateX(0) scale(1.2); opacity:1; } 100% { transform: translateY(-75px) translateX(18px) scale(0); opacity:0; } }
|
||||
@keyframes purchase-burst-f { 0% { transform: translateY(0) translateX(0) scale(1.2); opacity:1; } 100% { transform: translateY(-65px) translateX(-6px) scale(0); opacity:0; } }
|
||||
.purchase-burst {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: rgba(74,222,128,1);
|
||||
box-shadow: 0 0 6px rgba(74,222,128,0.9), 0 0 2px rgba(255,255,255,0.5);
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
.purchase-burst-0 { left: 3%; animation: purchase-burst-a 0.9s ease-out infinite 0.00s; }
|
||||
.purchase-burst-1 { left: 8%; animation: purchase-burst-d 1.1s ease-out infinite 0.73s; }
|
||||
.purchase-burst-2 { left: 12%; animation: purchase-burst-c 0.95s ease-out infinite 0.41s; }
|
||||
.purchase-burst-3 { left: 16%; animation: purchase-burst-f 1.05s ease-out infinite 0.17s; }
|
||||
.purchase-burst-4 { left: 20%; animation: purchase-burst-b 0.85s ease-out infinite 0.89s; }
|
||||
.purchase-burst-5 { left: 24%; animation: purchase-burst-e 1.0s ease-out infinite 0.53s; }
|
||||
.purchase-burst-6 { left: 28%; animation: purchase-burst-a 1.1s ease-out infinite 0.29s; }
|
||||
.purchase-burst-7 { left: 32%; animation: purchase-burst-c 0.9s ease-out infinite 0.97s; }
|
||||
.purchase-burst-8 { left: 36%; animation: purchase-burst-f 1.05s ease-out infinite 0.61s; }
|
||||
.purchase-burst-9 { left: 40%; animation: purchase-burst-d 0.95s ease-out infinite 0.07s; }
|
||||
.purchase-burst-10 { left: 44%; animation: purchase-burst-b 1.0s ease-out infinite 0.83s; }
|
||||
.purchase-burst-11 { left: 48%; animation: purchase-burst-e 0.85s ease-out infinite 0.37s; }
|
||||
.purchase-burst-12 { left: 52%; animation: purchase-burst-a 1.1s ease-out infinite 0.67s; }
|
||||
.purchase-burst-13 { left: 56%; animation: purchase-burst-f 0.9s ease-out infinite 0.11s; }
|
||||
.purchase-burst-14 { left: 60%; animation: purchase-burst-c 1.05s ease-out infinite 0.79s; }
|
||||
.purchase-burst-15 { left: 64%; animation: purchase-burst-d 0.95s ease-out infinite 0.47s; }
|
||||
.purchase-burst-16 { left: 68%; animation: purchase-burst-b 1.0s ease-out infinite 0.23s; }
|
||||
.purchase-burst-17 { left: 72%; animation: purchase-burst-e 0.85s ease-out infinite 1.03s; }
|
||||
.purchase-burst-18 { left: 76%; animation: purchase-burst-a 1.1s ease-out infinite 0.57s; }
|
||||
.purchase-burst-19 { left: 80%; animation: purchase-burst-f 0.95s ease-out infinite 0.31s; }
|
||||
.purchase-burst-20 { left: 6%; animation: purchase-burst-b 0.92s ease-out infinite 0.15s; }
|
||||
.purchase-burst-21 { left: 14%; animation: purchase-burst-e 1.08s ease-out infinite 0.86s; }
|
||||
.purchase-burst-22 { left: 22%; animation: purchase-burst-a 0.88s ease-out infinite 0.44s; }
|
||||
.purchase-burst-23 { left: 30%; animation: purchase-burst-d 1.02s ease-out infinite 0.71s; }
|
||||
.purchase-burst-24 { left: 38%; animation: purchase-burst-f 0.93s ease-out infinite 0.03s; }
|
||||
.purchase-burst-25 { left: 46%; animation: purchase-burst-c 1.07s ease-out infinite 0.59s; }
|
||||
.purchase-burst-26 { left: 54%; animation: purchase-burst-b 0.87s ease-out infinite 0.92s; }
|
||||
.purchase-burst-27 { left: 62%; animation: purchase-burst-e 0.98s ease-out infinite 0.26s; }
|
||||
.purchase-burst-28 { left: 70%; animation: purchase-burst-a 1.12s ease-out infinite 0.64s; }
|
||||
.purchase-burst-29 { left: 78%; animation: purchase-burst-d 0.91s ease-out infinite 0.38s; }
|
||||
.purchase-burst-30 { left: 84%; animation: purchase-burst-c 1.03s ease-out infinite 0.77s; }
|
||||
.purchase-burst-31 { left: 88%; animation: purchase-burst-f 0.86s ease-out infinite 0.09s; }
|
||||
.purchase-burst-32 { left: 92%; animation: purchase-burst-b 1.06s ease-out infinite 0.52s; }
|
||||
.purchase-burst-33 { left: 96%; animation: purchase-burst-e 0.94s ease-out infinite 0.81s; }
|
||||
.purchase-burst-34 { left: 10%; animation: purchase-burst-d 0.89s ease-out infinite 0.34s; }
|
||||
.purchase-burst-35 { left: 26%; animation: purchase-burst-a 1.04s ease-out infinite 0.96s; }
|
||||
.purchase-burst-36 { left: 42%; animation: purchase-burst-f 0.91s ease-out infinite 0.19s; }
|
||||
.purchase-burst-37 { left: 58%; animation: purchase-burst-c 1.09s ease-out infinite 0.69s; }
|
||||
.purchase-burst-38 { left: 74%; animation: purchase-burst-b 0.87s ease-out infinite 0.46s; }
|
||||
.purchase-burst-39 { left: 90%; animation: purchase-burst-e 1.01s ease-out infinite 0.13s; }
|
||||
.purchase-btn-wrap:hover .purchase-burst {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
@customElement("purchase-button")
|
||||
export class PurchaseButton extends LitElement {
|
||||
@property({ type: Object })
|
||||
product!: Product;
|
||||
|
||||
@property({ type: String })
|
||||
rarity: string = "common";
|
||||
|
||||
@property({ type: Function })
|
||||
onPurchase?: () => void;
|
||||
|
||||
@@ -17,19 +162,42 @@ export class PurchaseButton extends LitElement {
|
||||
|
||||
private handleClick(e: Event) {
|
||||
e.stopPropagation();
|
||||
this.onPurchase?.();
|
||||
const container = this.closest("cosmetic-container") as HTMLElement | null;
|
||||
if (container && !container.querySelector(".cosmetic-loading-overlay")) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "cosmetic-loading-overlay";
|
||||
overlay.innerHTML = `<div class="cosmetic-loading-spinner"></div>`;
|
||||
container.appendChild(overlay);
|
||||
}
|
||||
Promise.resolve(this.onPurchase?.()).catch(() => {
|
||||
container?.querySelector(".cosmetic-loading-overlay")?.remove();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="no-crazygames w-full mt-2">
|
||||
<div class="no-crazygames w-full mt-2 relative purchase-btn-wrap">
|
||||
${this.rarity !== "common"
|
||||
? html`<span class="purchase-ember purchase-ember-0"></span>
|
||||
<span class="purchase-ember purchase-ember-1"></span>
|
||||
<span class="purchase-ember purchase-ember-2"></span>
|
||||
<span class="purchase-ember purchase-ember-3"></span>
|
||||
${Array.from(
|
||||
{ length: 40 },
|
||||
(_, i) =>
|
||||
html`<span
|
||||
class="purchase-burst purchase-burst-${i}"
|
||||
></span>`,
|
||||
)}`
|
||||
: 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)]"
|
||||
class="purchase-sparkle-btn relative overflow-hidden 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 hover:border-green-400 hover:text-white hover:shadow-[0_0_20px_rgba(74,222,128,0.6)]"
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<span class="purchase-sparkle-streak"></span>
|
||||
${translateText("territory_patterns.purchase")}
|
||||
<span class="ml-1 text-white/60">(${this.product.price})</span>
|
||||
<span class="ml-1 text-white/50">(${this.product.price})</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -51,8 +51,17 @@ export const ColorPaletteSchema = z.object({
|
||||
secondaryColor: z.string(),
|
||||
});
|
||||
|
||||
export const PatternSchema = z.object({
|
||||
const CosmeticSchema = z.object({
|
||||
name: CosmeticNameSchema,
|
||||
affiliateCode: z.string().nullable(),
|
||||
product: ProductSchema.nullable(),
|
||||
artist: z.string().optional(),
|
||||
rarity: z
|
||||
.enum(["common", "uncommon", "rare", "epic", "legendary"])
|
||||
.or(z.string()),
|
||||
});
|
||||
|
||||
export const PatternSchema = CosmeticSchema.extend({
|
||||
pattern: PatternDataSchema,
|
||||
colorPalettes: z
|
||||
.object({
|
||||
@@ -61,17 +70,10 @@ export const PatternSchema = z.object({
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
affiliateCode: z.string().nullable(),
|
||||
product: ProductSchema.nullable(),
|
||||
artist: z.string().optional(),
|
||||
});
|
||||
|
||||
export const FlagSchema = z.object({
|
||||
name: CosmeticNameSchema,
|
||||
export const FlagSchema = CosmeticSchema.extend({
|
||||
url: z.string(),
|
||||
affiliateCode: z.string().nullable(),
|
||||
product: ProductSchema.nullable(),
|
||||
artist: z.string().optional(),
|
||||
});
|
||||
|
||||
// Schema for resources/cosmetics/cosmetics.json
|
||||
|
||||
@@ -43,6 +43,7 @@ const flagCosmetics = {
|
||||
url: "https://example.com/cool.png",
|
||||
affiliateCode: null,
|
||||
product: { productId: "prod_1", priceId: "price_1", price: "$4.99" },
|
||||
rarity: "common",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user