import type { TemplateResult } from "lit"; import { html } from "lit"; import { customElement } from "lit/decorators.js"; import { UserMeResponse } from "../core/ApiSchemas"; import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas"; import { Difficulty, GameMapSize, GameMapType, GameMode, GameType, UnitType, } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; import { BaseModal } from "./components/BaseModal"; import "./components/CosmeticButton"; import "./components/NotLoggedInWarning"; import { modalHeader } from "./components/ui/ModalHeader"; import { fetchCosmetics, purchaseCosmetic, resolveCosmetics, ResolvedCosmetic, } from "./Cosmetics"; import { translateText } from "./Utils"; type StoreTab = "patterns" | "flags" | "packs" | "subscriptions"; @customElement("store-modal") export class StoreModal extends BaseModal { protected routerName = "store"; private cosmetics: Cosmetics | null = null; private affiliateCode: string | null = null; private userMeResponse: UserMeResponse | false = false; protected modalConfig() { if (this.affiliateCode) { // Affiliate mode: hide tabs, show only items associated with the code. return {}; } return { tabs: [ { key: "packs", label: translateText("store.packs") }, { key: "subscriptions", label: translateText("store.subscriptions") }, { key: "patterns", label: translateText("store.patterns") }, { key: "flags", label: translateText("store.flags") }, ], }; } connectedCallback() { super.connectedCallback(); document.addEventListener( "userMeResponse", (event: CustomEvent) => { this.onUserMe(event.detail); }, ); } async onUserMe(userMeResponse: UserMeResponse | false) { this.userMeResponse = userMeResponse; this.cosmetics = await fetchCosmetics(); this.refresh(); } private startTestGame(resolved: ResolvedCosmetic) { if (!this.userMeResponse || resolved.type !== "pattern") return; const pattern = resolved.cosmetic as Pattern; const colorPalette = resolved.colorPalette as ColorPalette | null; const clientID = this.userMeResponse.player.publicId; const gameID = pattern.name; const selectedPattern = { name: pattern.name, patternData: pattern.pattern, colorPalette: colorPalette ?? undefined, }; const translation = translateText( `territory_patterns.pattern.${pattern.name}`, ); const displayName = translation.startsWith("territory_patterns.pattern.") ? pattern.name .split("_") .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" ") : translation; this.dispatchEvent( new CustomEvent("join-lobby", { detail: { clientID, gameID, isSkinTest: true, source: "singleplayer", gameStartInfo: { gameID, players: [ { clientID, username: displayName, cosmetics: { pattern: selectedPattern, }, }, ], config: { gameMap: GameMapType.Iceland, gameMapSize: GameMapSize.Compact, gameType: GameType.Singleplayer, gameMode: GameMode.FFA, playerTeams: 1, bots: 0, difficulty: Difficulty.Easy, donateGold: false, donateTroops: false, instantBuild: false, randomSpawn: true, disableNations: true, infiniteGold: true, infiniteTroops: true, startingTroops: 10_000_000, percentageTilesOwnedToWin: 99, disabledUnits: [ UnitType.City, UnitType.Factory, UnitType.Port, UnitType.MissileSilo, UnitType.DefensePost, UnitType.SAMLauncher, UnitType.AtomBomb, UnitType.HydrogenBomb, UnitType.MIRV, UnitType.Warship, ], }, lobbyCreatedAt: Date.now(), }, }, bubbles: true, composed: true, }), ); } private renderHeader(): TemplateResult { return modalHeader({ title: translateText("store.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), rightContent: html``, }); } private renderPatternGrid(): TemplateResult { const items = resolveCosmetics( this.cosmetics, this.userMeResponse, this.affiliateCode, ).filter( (r) => r.type === "pattern" && r.relationship !== "blocked" && r.relationship !== "owned", ); if (items.length === 0) { return html`
${translateText("store.no_skins")}
`; } return html`
${items.map( (r) => html` this.startTestGame(resolved) : undefined} > `, )}
`; } private renderFlagGrid(): TemplateResult { const items = resolveCosmetics( this.cosmetics, this.userMeResponse, this.affiliateCode, ).filter( (r) => r.type === "flag" && r.relationship !== "blocked" && r.relationship !== "owned", ); if (items.length === 0) { return html`
${translateText("store.no_flags")}
`; } const selectedFlag = new UserSettings().getFlag() ?? ""; return html`
${items.map( (r) => html` `, )}
`; } private renderPackGrid(): TemplateResult { const items = resolveCosmetics( this.cosmetics, this.userMeResponse, this.affiliateCode, ).filter((r) => r.type === "pack" && r.relationship === "purchasable"); if (items.length === 0) { return html`
${translateText("store.no_packs")}
`; } return html`
${items.map( (r) => html` `, )}
`; } private renderSubscriptionGrid(): TemplateResult { const items = resolveCosmetics( this.cosmetics, this.userMeResponse, this.affiliateCode, ).filter( (r) => r.type === "subscription" && (r.relationship === "purchasable" || r.relationship === "owned"), ); if (items.length === 0) { return html`
${translateText("store.no_subscriptions")}
`; } const userHasSubscription = this.userMeResponse !== false && this.userMeResponse.player.subscription !== null; return html`
${items.map( (r) => html` `, )}
`; } protected renderHeaderSlot() { return this.renderHeader(); } protected renderBody(key: string): TemplateResult { if (this.affiliateCode) { return this.renderAffiliateGrid(); } switch (key as StoreTab) { case "patterns": return this.renderPatternGrid(); case "flags": return this.renderFlagGrid(); case "subscriptions": return this.renderSubscriptionGrid(); case "packs": default: return this.renderPackGrid(); } } private renderAffiliateGrid(): TemplateResult { const items = resolveCosmetics( this.cosmetics, this.userMeResponse, this.affiliateCode, ).filter( (r) => (r.type === "pattern" || r.type === "flag" || r.type === "pack") && r.relationship === "purchasable", ); if (items.length === 0) { return html`
${translateText("store.no_skins")}
`; } return html`
${items.map( (r) => html` `, )}
`; } protected async onOpen(args?: Record) { const affiliate = typeof args?.affiliateCode === "string" ? args.affiliateCode : null; this.affiliateCode = affiliate; this.cosmetics ??= await fetchCosmetics(); await this.refresh(); } protected onClose(): void { this.affiliateCode = null; } public async refresh() { this.requestUpdate(); } }