100 gated gifts (level/rating-locked) + requirement system
CI/CD / CI - API (dotnet build + engine sim) (push) Has been cancelled
CI/CD / CI - Web (tsc + next build) (push) Has been cancelled
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled

Adds ~100 new purchasable gifts that are LOCKED until a level/rating gate is met,
then buyable with coins — value scales with the gate:
- 45 gift avatars (types.ts), 35 gift titles + 20 gift card backs (gamification.ts),
  all reusing existing renderers. Tier (1-5) encoded in the id (-t<n>-).
- Gate model: GIFT_TIERS (shared) → reqLevel/reqRating on AvatarDef/TitleDef/
  CardBackDef + ShopItem. Tiers: t1 free, t2 Lv10, t3 Lv20, t4 Lv35, t5 Rating1700.
- Shop UI: locked cards dim + show the requirement (Lock + "Level 20"), buy
  disabled until met; mock buyItem enforces it offline.
- Server enforces generically — ProfileService parses the tier from the id and
  checks the player's level/rating (no 100-entry mirror). Mirrors GIFT_TIERS.
- i18n shop.reqLevel/reqRating (fa+en).

Verified: tsc + sim + next build + dotnet build all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-07 00:02:28 +03:30
parent e49df07c0f
commit 38ac8b06d1
6 changed files with 166 additions and 6 deletions
+10
View File
@@ -1031,6 +1031,8 @@ export class MockOnlineService implements OnlineService {
preview: a.emoji,
descFa: "آواتار نمایه شما در بازی و جدول",
descEn: "Your profile avatar in games & leaderboard",
reqLevel: a.reqLevel,
reqRating: a.reqRating,
}));
const backItems: ShopItem[] = CARD_BACKS.filter((c) => c.price > 0).map((c) => ({
id: c.id,
@@ -1041,6 +1043,8 @@ export class MockOnlineService implements OnlineService {
preview: c.accent,
descFa: "طرح پشت کارت‌ها روی میز",
descEn: "The pattern on the back of your cards",
reqLevel: c.reqLevel,
reqRating: c.reqRating,
}));
const frontItems: ShopItem[] = CARD_FRONTS.filter((c) => c.price > 0).map((c) => ({
id: c.id,
@@ -1083,6 +1087,8 @@ export class MockOnlineService implements OnlineService {
preview: "🏷️",
descFa: "عنوان نمایه که زیر نام شما در بازی و لیست‌ها نشان داده می‌شود",
descEn: "A profile title shown under your name in games & lists",
reqLevel: tt.reqLevel,
reqRating: tt.reqRating,
}));
const xpItems: ShopItem[] = XP_PACKS.map((x) => ({
id: x.id,
@@ -1104,6 +1110,10 @@ 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))
return { ok: false, messageFa: "هنوز باز نشده است", messageEn: "Locked — requirement not met" };
// XP packs are consumable — grant XP instead of adding to an owned list.
if (item.kind === "xp") {
if (p.coins < item.price)