Shop: every item is coin-priced; level/rank/achievement only gate the purchase
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 6m29s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m8s
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled

No more earned-only (rank/wins) cosmetics — every avatar, card back/front,
reaction & sticker pack now has a coin price. Rank/wins/achievement become
purchase requirements (coin · coin+rank · coin+rank+achievement), enforced
client (mock + ShopScreen lock label) and server (ProfileService.ItemGate,
keyed by kind:id). Ownership = default + purchased only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 21:27:25 +03:30
parent ccfc9b0536
commit 72efc03e2d
6 changed files with 137 additions and 78 deletions
+17 -2
View File
@@ -1036,6 +1036,7 @@ export class MockOnlineService implements OnlineService {
descEn: "A legendary profile avatar shown in games & the leaderboard",
reqLevel: a.reqLevel,
reqRating: a.reqRating,
reqAchievement: a.reqAchievement,
};
});
const backItems: ShopItem[] = CARD_BACKS.filter((c) => c.price > 0).map((c) => ({
@@ -1049,6 +1050,7 @@ export class MockOnlineService implements OnlineService {
descEn: "The pattern on the back of your cards",
reqLevel: c.reqLevel,
reqRating: c.reqRating,
reqAchievement: c.reqAchievement,
}));
const frontItems: ShopItem[] = CARD_FRONTS.filter((c) => c.price > 0).map((c) => ({
id: c.id,
@@ -1059,6 +1061,9 @@ export class MockOnlineService implements OnlineService {
preview: c.bg2,
descFa: "ظاهر روی کارت‌های شما",
descEn: "The face style of your cards",
reqLevel: c.reqLevel,
reqRating: c.reqRating,
reqAchievement: c.reqAchievement,
}));
const reactionItems: ShopItem[] = REACTION_PACKS.filter((r) => r.price > 0).map((r) => ({
id: r.id,
@@ -1070,6 +1075,9 @@ export class MockOnlineService implements OnlineService {
contents: r.reactions,
descFa: `${faNum(r.reactions.length)} ایموجی برای استفاده در بازی`,
descEn: `${r.reactions.length} in-game emotes`,
reqLevel: r.reqLevel,
reqRating: r.reqRating,
reqAchievement: r.reqAchievement,
}));
const stickerItems: ShopItem[] = STICKER_PACKS.filter((p) => p.price > 0).map((p) => ({
id: p.id,
@@ -1081,6 +1089,9 @@ export class MockOnlineService implements OnlineService {
contents: p.stickers,
descFa: `${faNum(p.stickers.length)} استیکر برای استفاده در بازی`,
descEn: `${p.stickers.length} in-game stickers`,
reqLevel: p.reqLevel,
reqRating: p.reqRating,
reqAchievement: p.reqAchievement,
}));
const titleItems: ShopItem[] = TITLES.filter((tt) => (tt.price ?? 0) > 0).map((tt) => ({
id: tt.id,
@@ -1114,8 +1125,12 @@ export class MockOnlineService implements OnlineService {
const item = items.find((i) => i.id === id);
if (!item) return { ok: false, messageFa: "آیتم یافت نشد", messageEn: "Item not found" };
// Purchase gate: locked until the level/rating requirement is met.
if ((item.reqLevel && p.level < item.reqLevel) || (item.reqRating && p.rating < item.reqRating))
// Purchase gate: locked until the level / rating / achievement requirement is met.
if (
(item.reqLevel && p.level < item.reqLevel) ||
(item.reqRating && p.rating < item.reqRating) ||
(item.reqAchievement && !(p.unlocked ?? []).includes(item.reqAchievement))
)
return { ok: false, messageFa: "هنوز باز نشده است", messageEn: "Locked — requirement not met" };
// XP packs are consumable — grant XP instead of adding to an owned list.