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
+57
View File
@@ -3,6 +3,7 @@
import {
AVATARS,
GIFT_TIERS,
AchievementCategoryDef,
AchievementCategoryId,
AchievementDef,
@@ -499,6 +500,62 @@ export const CARD_FRONTS: CardFrontDef[] = [
{ id: "blackgold-face", nameFa: "طلای سیاه", nameEn: "Black Gold", bg1: "#2a2410", bg2: "#14110a", border: "#caa53a", price: 3200 },
];
/* ------------------- Gated gift catalogue (titles + backs) -------------------
* Purchasable gifts that are LOCKED until a level/rating gate is met (the tier is
* encoded in the id `-t<n>-` so the server enforces it generically). Gift avatars
* live in types.ts (same scheme). ~100 new gifts total across the three kinds. */
function giftReq(tier: number) {
const t = GIFT_TIERS[tier] ?? GIFT_TIERS[1];
return { price: t.price, reqLevel: t.level || undefined, reqRating: t.rating || undefined };
}
// 35 gift titles (fa, en), spread across the 5 tiers.
const GIFT_TITLE_WORDS: [string, string][] = [
["سردار", "Commander"], ["یل", "Brave"], ["پهلوان", "Hero"], ["شیردل", "Lionheart"], ["تیزهوش", "Sharp"], ["زبده", "Ace"], ["کارکشته", "Veteran"],
["نخبه", "Elite"], ["بی‌رقیب", "Unrivaled"], ["شکست‌ناپذیر", "Invincible"], ["آتشین", "Fiery"], ["طوفان", "Storm"], ["صاعقه", "Thunder"], ["کولاک", "Blizzard"],
["تاجدار", "Crowned"], ["فرمانروا", "Ruler"], ["شاهباز", "Royal Falcon"], ["گرگ تنها", "Lone Wolf"], ["عقاب", "Eagle"], ["ققنوس", "Phoenix"], ["محاسب", "Tactician"],
["نقشه‌کش", "Strategist"], ["بازی‌ساز", "Playmaker"], ["پادشاه میز", "Table King"], ["حکم‌ران", "Hokm Lord"], ["برگ‌برنده", "Trump Card"], ["دست‌مریزاد", "Masterstroke"], ["افسانه‌ساز", "Legend-Maker"],
["جواهر", "Gem"], ["الماس", "Diamond"], ["زرین", "Golden"], ["شاهنشاه", "Emperor"], ["اسطوره زنده", "Living Legend"], ["استاد اعظم", "Grandmaster"], ["تسخیرناپذیر", "Untamed"],
];
TITLES.push(
...GIFT_TITLE_WORDS.map(([fa, en], i) => {
const tier = Math.min(5, Math.floor(i / 7) + 1);
const r = giftReq(tier);
return { id: `t-g-t${tier}-${i + 1}`, nameFa: fa, nameEn: en, hintFa: "گیفت ویژه", hintEn: "Special gift", price: r.price, reqLevel: r.reqLevel, reqRating: r.reqRating };
})
);
// 20 gift card backs (palette × pattern), spread across the 5 tiers.
const GIFT_BACK_PALETTE: { fa: string; en: string; c1: string; c2: string; accent: string; pattern: CardBackDef["pattern"]; motif?: string }[] = [
{ fa: "نیلی", en: "Indigo", c1: "#2b2f7a", c2: "#10112e", accent: "#8aa0ff", pattern: "stripes" },
{ fa: "زمردین", en: "Verdant", c1: "#0d5a44", c2: "#06231a", accent: "#4fe0a8", pattern: "dots" },
{ fa: "غروب", en: "Sunset", c1: "#7a3b12", c2: "#2a1407", accent: "#ffb066", pattern: "rays" },
{ fa: "بنفشه", en: "Violet", c1: "#532a7a", c2: "#1d0e2e", accent: "#c79cff", pattern: "argyle", motif: "✦" },
{ fa: "فیروزه", en: "Turquoise", c1: "#0e5d6e", c2: "#06232a", accent: "#5fe0e0", pattern: "scales" },
{ fa: "گلگون", en: "Rose", c1: "#7a2340", c2: "#2a0c16", accent: "#ff8ab0", pattern: "grid" },
{ fa: "شنی", en: "Sand", c1: "#7a5a1a", c2: "#2a1e08", accent: "#e6c66a", pattern: "crosshatch" },
{ fa: "یخی", en: "Frost", c1: "#3a5a7a", c2: "#13202e", accent: "#9fd0ff", pattern: "dots" },
{ fa: "ارغوان", en: "Magenta", c1: "#7a1f5a", c2: "#2a0a1e", accent: "#ff8ae0", pattern: "rays" },
{ fa: "جنگلی", en: "Forest", c1: "#2e5a1a", c2: "#0e2008", accent: "#9ee06a", pattern: "stripes" },
{ fa: "نقره‌ای", en: "Silver", c1: "#4a4f5a", c2: "#16181d", accent: "#cfd6e6", pattern: "argyle", motif: "♢" },
{ fa: "مسی", en: "Copper", c1: "#7a3f1a", c2: "#2a1508", accent: "#e6975a", pattern: "scales" },
{ fa: "یاقوتی", en: "Garnet", c1: "#6a1326", c2: "#240710", accent: "#ff7a90", pattern: "filigree", motif: "♦" },
{ fa: "کبریتی", en: "Sulfur", c1: "#6a6010", c2: "#242008", accent: "#ffe46a", pattern: "grid" },
{ fa: "اطلسی", en: "Petunia", c1: "#3a2a7a", c2: "#12102e", accent: "#9a8aff", pattern: "royal", motif: "♛" },
{ fa: "زمستانی", en: "Winter", c1: "#1f4a6a", c2: "#0a1a26", accent: "#7fc6ff", pattern: "filigree", motif: "❄" },
{ fa: "آتشفشان", en: "Volcano", c1: "#6a1f10", c2: "#240a06", accent: "#ff7a4a", pattern: "rays", motif: "✦" },
{ fa: "کهکشان", en: "Galaxy", c1: "#241a4a", c2: "#0a0820", accent: "#a98aff", pattern: "gem", motif: "✧" },
{ fa: "زرافشان", en: "Goldspark", c1: "#5a4410", c2: "#241a06", accent: "#ffd76a", pattern: "royal", motif: "♔" },
{ fa: "الماسی", en: "Brilliant", c1: "#103a4a", c2: "#06181f", accent: "#7fe6ff", pattern: "gem", motif: "♦" },
];
CARD_BACKS.push(
...GIFT_BACK_PALETTE.map((b, i) => {
const tier = Math.min(5, Math.floor(i / 4) + 1);
const r = giftReq(tier);
return { id: `cb-g-t${tier}-${i + 1}`, nameFa: b.fa, nameEn: b.en, c1: b.c1, c2: b.c2, accent: b.accent, pattern: b.pattern, motif: b.motif, price: r.price, reqLevel: r.reqLevel, reqRating: r.reqRating };
})
);
export function cardBackById(id: string): CardBackDef {
return CARD_BACKS.find((c) => c.id === id) ?? CARD_BACKS[0];
}