diff --git a/resources/images/BottleCapIcon.svg b/resources/images/BottleCapIcon.svg new file mode 100644 index 000000000..7a4066f0b --- /dev/null +++ b/resources/images/BottleCapIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/images/PlutoniumIcon.svg b/resources/images/PlutoniumIcon.svg new file mode 100644 index 000000000..ea3e0ac2a --- /dev/null +++ b/resources/images/PlutoniumIcon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/lang/en.json b/resources/lang/en.json index fd977f7c3..987133225 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -942,8 +942,11 @@ "title": "Store", "patterns": "Skins", "flags": "Flags", + "packs": "Packs", "no_flags": "No flags available. Check back later for new items.", - "no_skins": "No skins available. Check back later for new items." + "no_skins": "No skins available. Check back later for new items.", + "no_packs": "No packs available. Check back later for new items.", + "currency_pack_purchase_success": "Currency pack purchase successful!" }, "territory_patterns": { "title": "Skins", @@ -963,7 +966,9 @@ "rare": "Rare", "epic": "Epic", "legendary": "Legendary", - "adfree": "ad-free for life!" + "adfree": "ad-free for life!", + "hard": "Plutonium", + "soft": "Caps" }, "flag_input": { "title": "Select Flag", diff --git a/src/client/AccountModal.ts b/src/client/AccountModal.ts index fe76dfd6b..a1375f12d 100644 --- a/src/client/AccountModal.ts +++ b/src/client/AccountModal.ts @@ -15,6 +15,7 @@ import "./components/baseComponents/stats/PlayerStatsTable"; import "./components/baseComponents/stats/PlayerStatsTree"; import { BaseModal } from "./components/BaseModal"; import "./components/CopyButton"; +import "./components/CurrencyDisplay"; import "./components/Difficulties"; import { modalHeader } from "./components/ui/ModalHeader"; import { translateText } from "./Utils"; @@ -191,12 +192,24 @@ export class AccountModal extends BaseModal { `; } + private renderCurrency(): TemplateResult { + const currency = this.userMeResponse?.player?.currency; + if (!currency) return html``; + + return html` + + `; + } + private renderLoggedInAs(): TemplateResult { const me = this.userMeResponse?.user; if (me?.discord) { return html`
- ${this.renderLogoutButton()} + ${this.renderCurrency()} ${this.renderLogoutButton()}
`; } else if (me?.email) { @@ -207,7 +220,7 @@ export class AccountModal extends BaseModal { account_name: me.email, })} - ${this.renderLogoutButton()} + ${this.renderCurrency()} ${this.renderLogoutButton()} `; } @@ -265,6 +278,7 @@ export class AccountModal extends BaseModal {

${translateText("account_modal.sign_in_desc")}

+ ${this.renderCurrency()}
diff --git a/src/client/Cosmetics.ts b/src/client/Cosmetics.ts index 32c6fbcd0..963538a5d 100644 --- a/src/client/Cosmetics.ts +++ b/src/client/Cosmetics.ts @@ -5,6 +5,7 @@ import { Cosmetics, CosmeticsSchema, Flag, + Pack, Pattern, Product, } from "../core/CosmeticSchemas"; @@ -190,8 +191,8 @@ export function flagRelationship( } export type ResolvedCosmetic = { - type: "pattern" | "flag"; - cosmetic: Pattern | Flag | null; + type: "pattern" | "flag" | "pack"; + cosmetic: Pattern | Flag | Pack | null; colorPalette: ColorPalette | null; relationship: "owned" | "purchasable" | "blocked"; /** Unique key for selection/identity, e.g. "pattern:hearts:red" or "flag:cool_flag" */ @@ -257,6 +258,18 @@ export function resolveCosmetics( }); } + // Packs + for (const [packKey, pack] of Object.entries(cosmetics.currencyPacks ?? {})) { + const rel = pack.product ? "purchasable" : "blocked"; + result.push({ + type: "pack", + cosmetic: pack, + colorPalette: null, + relationship: rel, + key: `pack:${packKey}`, + }); + } + return result; } diff --git a/src/client/Main.ts b/src/client/Main.ts index 4c15d5684..f4fb076e9 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -636,6 +636,12 @@ class Client { return; } + const type = params.get("type"); + if (type === "currency_pack") { + alertAndStrip(translateText("store.currency_pack_purchase_success")); + return; + } + const cosmeticName = params.get("cosmetic"); if (!cosmeticName) { alert("Something went wrong. Please contact support."); diff --git a/src/client/Store.ts b/src/client/Store.ts index 32af6d2f8..2c48fae75 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -18,10 +18,9 @@ import { translateText } from "./Utils"; @customElement("store-modal") export class StoreModal extends BaseModal { - @state() private activeTab: "patterns" | "flags" = "patterns"; + @state() private activeTab: "patterns" | "flags" | "packs" = "patterns"; private cosmetics: Cosmetics | null = null; - private userSettings: UserSettings = new UserSettings(); private isActive = false; private affiliateCode: string | null = null; private userMeResponse: UserMeResponse | false = false; @@ -51,6 +50,15 @@ export class StoreModal extends BaseModal { rightContent: html``, })}
+
`; diff --git a/src/client/components/CapIcon.ts b/src/client/components/CapIcon.ts new file mode 100644 index 000000000..ac6d84603 --- /dev/null +++ b/src/client/components/CapIcon.ts @@ -0,0 +1,29 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { assetUrl } from "../../core/AssetUrls"; + +@customElement("cap-icon") +export class CapIcon extends LitElement { + @property({ type: Number }) + size: number = 48; + + createRenderRoot() { + return this; + } + + render() { + return html` +
+ Caps +
+ `; + } +} diff --git a/src/client/components/CosmeticButton.ts b/src/client/components/CosmeticButton.ts index 33284c50f..9b1c3274b 100644 --- a/src/client/components/CosmeticButton.ts +++ b/src/client/components/CosmeticButton.ts @@ -1,12 +1,14 @@ import { html, LitElement, nothing, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators.js"; -import { Flag, Pattern } from "../../core/CosmeticSchemas"; +import { Flag, Pack, Pattern } from "../../core/CosmeticSchemas"; import { PlayerPattern } from "../../core/Schemas"; import { ResolvedCosmetic, translateCosmetic } from "../Cosmetics"; import { translateText } from "../Utils"; +import "./CapIcon"; import "./CosmeticContainer"; import "./CosmeticInfo"; import { renderPatternPreview } from "./PatternPreview"; +import "./PlutoniumIcon"; @customElement("cosmetic-button") export class CosmeticButton extends LitElement { @@ -38,6 +40,9 @@ export class CosmeticButton extends LitElement { if (this.resolved.type === "pattern") { return translateCosmetic("territory_patterns.pattern", c.name); } + if (this.resolved.type === "pack") { + return (c as Pack).displayName; + } return translateCosmetic("flags", c.name); } @@ -55,6 +60,33 @@ export class CosmeticButton extends LitElement { return renderPatternPreview(playerPattern, 150, 150); } + if (this.resolved.type === "pack") { + const pack = this.resolved.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)} +
`; + } + const c = this.resolved.cosmetic as Flag; return html` +
+ + ${this.hard.toLocaleString()} +
+
+ + ${this.soft.toLocaleString()} +
+
+ `; + } +} diff --git a/src/client/components/PlutoniumIcon.ts b/src/client/components/PlutoniumIcon.ts new file mode 100644 index 000000000..17cf3a242 --- /dev/null +++ b/src/client/components/PlutoniumIcon.ts @@ -0,0 +1,55 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { assetUrl } from "../../core/AssetUrls"; + +const STYLE_ID = "plutonium-icon-styles"; +if (!document.getElementById(STYLE_ID)) { + const style = document.createElement("style"); + style.id = STYLE_ID; + style.textContent = ` + @keyframes plutonium-pulse { + 0% { filter: drop-shadow(0 0 4px rgba(34,197,94,0.6)) drop-shadow(0 0 8px rgba(34,197,94,0.3)); scale: 1; } + 50% { filter: drop-shadow(0 0 10px rgba(34,197,94,0.9)) drop-shadow(0 0 20px rgba(34,197,94,0.5)) drop-shadow(0 0 30px rgba(34,197,94,0.2)); scale: 1.04; } + 100% { filter: drop-shadow(0 0 4px rgba(34,197,94,0.6)) drop-shadow(0 0 8px rgba(34,197,94,0.3)); scale: 1; } + } + @keyframes plutonium-rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + @keyframes plutonium-jiggle { + 0%, 100% { translate: 0 0; } + 25% { translate: -0.4px 0.3px; } + 50% { translate: 0.3px -0.4px; } + 75% { translate: -0.3px -0.3px; } + } + `; + document.head.appendChild(style); +} + +@customElement("plutonium-icon") +export class PlutoniumIcon extends LitElement { + @property({ type: Number }) + size: number = 48; + + createRenderRoot() { + return this; + } + + render() { + return html` +
+ Plutonium +
+ `; + } +} diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 74e7bf8c3..cb968b576 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -81,6 +81,12 @@ export const UserMeResponseSchema = z.object({ .optional(), }) .optional(), + currency: z + .object({ + soft: z.coerce.number(), + hard: z.coerce.number(), + }) + .optional(), }), }); export type UserMeResponse = z.infer; diff --git a/src/core/CosmeticSchemas.ts b/src/core/CosmeticSchemas.ts index bd808cdcc..ee6121cb0 100644 --- a/src/core/CosmeticSchemas.ts +++ b/src/core/CosmeticSchemas.ts @@ -6,6 +6,7 @@ import { PlayerPattern } from "./Schemas"; export type Cosmetics = z.infer; export type Pattern = z.infer; export type Flag = z.infer; +export type Pack = z.infer; export type PatternName = z.infer; export type Product = z.infer; export type ColorPalette = z.infer; @@ -76,11 +77,18 @@ export const FlagSchema = CosmeticSchema.extend({ url: z.string(), }); +export const PackSchema = CosmeticSchema.extend({ + displayName: z.string(), + currency: z.enum(["hard", "soft"]), + amount: z.number().int().positive(), +}); + // Schema for resources/cosmetics/cosmetics.json export const CosmeticsSchema = z.object({ colorPalettes: z.record(z.string(), ColorPaletteSchema).optional(), patterns: z.record(z.string(), PatternSchema), flags: z.record(z.string(), FlagSchema), + currencyPacks: z.record(z.string(), PackSchema).optional(), }); export const DefaultPattern = {