Store XP packs (expensive), winner 2x XP, premium perks
- XP packs in the store (coin-priced, intentionally expensive): xp1 200/5k, xp2 600/12k, xp3 1500/25k. Consumable (grant XP, can level up) — server ShopBuy handles kind "xp" via an authoritative XpPacks map + Gamification.GrantXp; mock mirrors. New shop section + shop.xp/xpHint i18n. - Every game grants XP and the WINNER earns 2x: matchXp is now base*(won?2:1)*leagueFactor (was a flat +80 win bonus). Mirrored server-side. - Premium (pro) perks: 1.5x XP multiplier (applied in applyMatchResult / ApplyMatch by plan), plus animated shimmering gold chat bubbles for your own messages (premium-chat CSS; ChatScreen gates on plan). Verified: tsc + next + dotnet build clean; sim passes; live server — buying xp2 took L1→L3 and deducted 12k coins under the new curve. Images rebuilt :1500/:1505. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import { ChevronLeft, ChevronRight, Send } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useOnlineStore } from "@/lib/online-store";
|
||||
import { useSessionStore } from "@/lib/session-store";
|
||||
import { useUIStore } from "@/lib/ui-store";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { sound } from "@/lib/sound";
|
||||
@@ -15,6 +16,7 @@ export function ChatScreen() {
|
||||
const messages = useOnlineStore((s) => s.chatMessages);
|
||||
const sendChat = useOnlineStore((s) => s.sendChat);
|
||||
const closeChat = useOnlineStore((s) => s.closeChat);
|
||||
const isPro = useSessionStore((s) => s.profile?.plan === "pro");
|
||||
const navBack = useUIStore((s) => s.back);
|
||||
const [text, setText] = useState("");
|
||||
const endRef = useRef<HTMLDivElement>(null);
|
||||
@@ -77,7 +79,7 @@ export function ChatScreen() {
|
||||
className={cn(
|
||||
"max-w-[78%] rounded-2xl px-3.5 py-2 text-sm",
|
||||
m.fromMe
|
||||
? "ms-auto btn-gold rounded-ee-sm"
|
||||
? cn("ms-auto rounded-ee-sm", isPro ? "premium-chat" : "btn-gold")
|
||||
: "me-auto glass text-cream rounded-es-sm"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -34,6 +34,8 @@ export function ShopScreen() {
|
||||
return profile.ownedCardBacks.includes(item.id);
|
||||
case "reactionpack":
|
||||
return profile.ownedReactionPacks.includes(item.id);
|
||||
case "xp":
|
||||
return false; // consumable — always buyable
|
||||
default:
|
||||
return profile.ownedStickerPacks.includes(item.id);
|
||||
}
|
||||
@@ -55,6 +57,7 @@ export function ShopScreen() {
|
||||
const cardbacks = items.filter((i) => i.kind === "cardback");
|
||||
const reactions = items.filter((i) => i.kind === "reactionpack");
|
||||
const stickers = items.filter((i) => i.kind === "stickerpack");
|
||||
const xp = items.filter((i) => i.kind === "xp");
|
||||
|
||||
return (
|
||||
<ScreenShell>
|
||||
@@ -150,6 +153,21 @@ export function ShopScreen() {
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section title={t("shop.xp")}>
|
||||
<p className="text-[11px] text-cream/45 -mt-2 mb-3">{t("shop.xpHint")}</p>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{xp.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
owned={false}
|
||||
onBuy={() => buy(item)}
|
||||
preview={<span className="text-4xl">⚡</span>}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
</ScreenShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user