Store XP packs (expensive), winner 2x XP, premium perks
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped

- 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:
soroush.asadi
2026-06-05 00:08:19 +03:30
parent 4199a82c9d
commit fd33f85e9c
9 changed files with 116 additions and 14 deletions
+23 -1
View File
@@ -7,8 +7,11 @@ import {
CARD_FRONTS,
REACTION_PACKS,
STICKER_PACKS,
XP_PACKS,
addXp,
applyMatchResult,
dailyRewardFor,
faNum,
xpNeededForLevel,
} from "./gamification";
import {
@@ -879,7 +882,15 @@ export class MockOnlineService implements OnlineService {
price: p.price,
preview: p.stickers[0], // sticker id; ShopScreen renders via <Sticker>
}));
return [...avatarItems, ...frontItems, ...backItems, ...reactionItems, ...stickerItems];
const xpItems: ShopItem[] = XP_PACKS.map((x) => ({
id: x.id,
kind: "xp",
nameFa: `${faNum(x.xp)} امتیاز تجربه`,
nameEn: `${x.xp} XP`,
price: x.price,
preview: "⚡",
}));
return [...avatarItems, ...frontItems, ...backItems, ...reactionItems, ...stickerItems, ...xpItems];
}
async buyItem(id: string) {
@@ -887,6 +898,17 @@ export class MockOnlineService implements OnlineService {
const items = await this.getShopItems();
const item = items.find((i) => i.id === id);
if (!item) return { ok: false, messageFa: "آیتم یافت نشد", messageEn: "Item not found" };
// XP packs are consumable — grant XP instead of adding to an owned list.
if (item.kind === "xp") {
if (p.coins < item.price)
return { ok: false, messageFa: "سکه کافی نیست", messageEn: "Not enough coins" };
const pack = XP_PACKS.find((x) => x.id === id)!;
const lvl = addXp(p.level, p.xp, pack.xp);
this.profile = { ...p, coins: p.coins - item.price, level: lvl.level, xp: lvl.xp };
this.saveProfile();
return { ok: true, profile: this.profile, messageFa: "امتیاز اضافه شد", messageEn: "XP added" };
}
const ownedMap: Record<string, string[]> = {
avatar: p.ownedAvatars,
cardfront: p.ownedCardFronts,