import type { TemplateResult } from "lit"; import { html } from "lit"; import { customElement, state } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas"; import { PATTERN_KEY, USER_SETTINGS_CHANGED_EVENT, UserSettings, } from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; import { BaseModal } from "./components/BaseModal"; import "./components/FlagButton"; import "./components/NotLoggedInWarning"; import "./components/PatternButton"; import { modalHeader } from "./components/ui/ModalHeader"; import { fetchCosmetics, flagRelationship, getPlayerCosmetics, handlePurchase, patternRelationship, } from "./Cosmetics"; import { translateText } from "./Utils"; @customElement("store-modal") export class StoreModal extends BaseModal { @state() private selectedPattern: PlayerPattern | null; @state() private selectedColor: string | null = null; @state() private activeTab: "patterns" | "flags" = "patterns"; private cosmetics: Cosmetics | null = null; private userSettings: UserSettings = new UserSettings(); private isActive = false; private affiliateCode: string | null = null; private userMeResponse: UserMeResponse | false = false; private _onPatternSelected = async () => { await this.updateFromSettings(); this.refresh(); }; connectedCallback() { super.connectedCallback(); document.addEventListener( "userMeResponse", (event: CustomEvent) => { this.onUserMe(event.detail); }, ); window.addEventListener( `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, ); } disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener( `${USER_SETTINGS_CHANGED_EVENT}:${PATTERN_KEY}`, this._onPatternSelected, ); } private async updateFromSettings() { const cosmetics = await getPlayerCosmetics(); this.selectedPattern = cosmetics.pattern ?? null; this.selectedColor = cosmetics.color?.color ?? null; } async onUserMe(userMeResponse: UserMeResponse | false) { this.userMeResponse = userMeResponse; this.cosmetics = await fetchCosmetics(); await this.updateFromSettings(); this.refresh(); } private renderHeader(): TemplateResult { return html` ${modalHeader({ title: translateText("store.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), rightContent: html``, })}
`; } private renderPatternGrid(): TemplateResult { const buttons: TemplateResult[] = []; const patterns: (Pattern | null)[] = [ null, ...Object.values(this.cosmetics?.patterns ?? {}), ]; for (const pattern of patterns) { const colorPalettes = pattern ? [...(pattern.colorPalettes ?? []), null] : [null]; for (const colorPalette of colorPalettes) { let rel = "owned"; if (pattern) { rel = patternRelationship( pattern, colorPalette, this.userMeResponse, this.affiliateCode, ); } if (rel === "blocked" || rel === "owned") { continue; } const isDefaultPattern = pattern === null; const isSelected = (isDefaultPattern && this.selectedPattern === null) || (!isDefaultPattern && this.selectedPattern && this.selectedPattern.name === pattern?.name && (this.selectedPattern.colorPalette?.name ?? null) === (colorPalette?.name ?? null)); buttons.push(html` this.selectPattern(p)} .onPurchase=${(p: Pattern, cp: ColorPalette | null) => handlePurchase(p.product!, cp?.name)} > `); } } if (buttons.length === 0) { return html`
${translateText("store.no_skins")}
`; } return html`
${buttons}
`; } private renderFlagGrid(): TemplateResult { const buttons: TemplateResult[] = []; const flags = Object.entries(this.cosmetics?.flags ?? {}); for (const [key, flag] of flags) { const rel = flagRelationship( flag, this.userMeResponse, this.affiliateCode, ); if (rel === "blocked" || rel === "owned") continue; const selectedFlag = new UserSettings().getFlag() ?? ""; buttons.push(html` handlePurchase(flag.product!)} > `); } if (buttons.length === 0) { return html`
${translateText("store.no_flags")}
`; } return html`
${buttons}
`; } render() { if (!this.isActive && !this.inline) return html``; const content = html`
${this.renderHeader()}
${this.activeTab === "patterns" ? this.renderPatternGrid() : this.renderFlagGrid()}
`; if (this.inline) { return content; } return html` ${content} `; } public async open(options?: string | { affiliateCode?: string }) { if (this.isModalOpen) return; this.isActive = true; if (typeof options === "string") { this.affiliateCode = options; } else if ( options !== null && typeof options === "object" && !Array.isArray(options) ) { this.affiliateCode = options.affiliateCode ?? null; } else { this.affiliateCode = null; } this.cosmetics ??= await fetchCosmetics(); await this.refresh(); super.open(); } public close() { this.isActive = false; this.affiliateCode = null; super.close(); } private selectPattern(pattern: PlayerPattern | null) { this.selectedColor = null; this.userSettings.setSelectedColor(undefined); if (pattern === null) { this.userSettings.setSelectedPatternName(undefined); } else { const name = pattern.colorPalette?.name === undefined ? pattern.name : `${pattern.name}:${pattern.colorPalette.name}`; this.userSettings.setSelectedPatternName(`pattern:${name}`); } this.selectedPattern = pattern; this.refresh(); this.showSelectedPopup(pattern); this.close(); } private showSelectedPopup(pattern: PlayerPattern | null) { let skinName = translateText("territory_patterns.pattern.default"); if (pattern && pattern.name) { skinName = pattern.name .split("_") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); if (pattern.colorPalette && pattern.colorPalette.name) { skinName += ` (${pattern.colorPalette.name})`; } } window.dispatchEvent( new CustomEvent("show-message", { detail: { message: `${skinName} ${translateText("territory_patterns.selected")}`, duration: 2000, }, }), ); } public async refresh() { this.requestUpdate(); } }