Shop: every item is coin-priced; level/rank/achievement only gate the purchase
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:
@@ -312,6 +312,7 @@ const fa: Dict = {
|
||||
"shop.includes": "شامل",
|
||||
"shop.reqLevel": "سطح",
|
||||
"shop.reqRating": "امتیاز",
|
||||
"shop.reqAchv": "دستاورد:",
|
||||
"reward.newTitle": "عنوان جدید",
|
||||
|
||||
"reactions.title": "شکلک",
|
||||
@@ -644,6 +645,7 @@ const en: Dict = {
|
||||
"shop.includes": "Includes",
|
||||
"shop.reqLevel": "Level",
|
||||
"shop.reqRating": "Rating",
|
||||
"shop.reqAchv": "Achievement:",
|
||||
"reward.newTitle": "New title",
|
||||
|
||||
"reactions.title": "Emoji",
|
||||
|
||||
@@ -360,9 +360,9 @@ export function evaluateAchievements(profile: UserProfile): {
|
||||
};
|
||||
}
|
||||
|
||||
/** The sticker pack (if any) that unlocking this achievement grants. */
|
||||
/** The sticker pack (if any) that this achievement unlocks for purchase. */
|
||||
export function stickerPackForAchievement(achId: string): StickerPackDef | undefined {
|
||||
return STICKER_PACKS.find((p) => p.unlockAchievement === achId);
|
||||
return STICKER_PACKS.find((p) => p.reqAchievement === achId);
|
||||
}
|
||||
|
||||
/* ------------------------------ Titles ------------------------------- */
|
||||
@@ -466,13 +466,13 @@ export const CARD_BACKS: CardBackDef[] = [
|
||||
{ id: "emerald", nameFa: "زمرد", nameEn: "Emerald", c1: "#0d6b5e", c2: "#062420", accent: "#2dd4bf", price: 1000, pattern: "argyle" },
|
||||
{ id: "jade", nameFa: "یشم", nameEn: "Jade", c1: "#136f63", c2: "#08221e", accent: "#7fe3c0", price: 2000, pattern: "scales" },
|
||||
{ id: "onyx", nameFa: "اونیکس", nameEn: "Onyx", c1: "#26262b", c2: "#0c0c10", accent: "#b0b0c0", price: 1500, pattern: "crosshatch" },
|
||||
// earned by rank / wins — the higher the rank, the rarer the back
|
||||
{ id: "crimson", nameFa: "ارغوانی", nameEn: "Crimson", c1: "#7a1322", c2: "#2a0710", accent: "#ff8a9c", price: 0, unlockWins: 25, pattern: "rays" },
|
||||
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 0, unlockRating: 1300, pattern: "argyle", motif: "♦" },
|
||||
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 0, unlockWins: 50, pattern: "royal", motif: "♛" },
|
||||
{ id: "aurora", nameFa: "شفق", nameEn: "Aurora", c1: "#1d4e6e", c2: "#0a2230", accent: "#5be0c8", price: 0, unlockRating: 1500, pattern: "rays" },
|
||||
{ id: "obsidian", nameFa: "ابسیدین", nameEn: "Obsidian", c1: "#101018", c2: "#000005", accent: "#7c5cff", price: 0, unlockRating: 1700, pattern: "crosshatch", motif: "✦" },
|
||||
{ id: "imperial", nameFa: "شاهنشاهی", nameEn: "Imperial", c1: "#5a3c0a", c2: "#241704", accent: "#ffd76a", price: 0, unlockRating: 1900, pattern: "royal", motif: "♔" },
|
||||
// Rank/achievement gated — always buyable with coins once the gate is met.
|
||||
{ id: "crimson", nameFa: "ارغوانی", nameEn: "Crimson", c1: "#7a1322", c2: "#2a0710", accent: "#ff8a9c", price: 1000, reqAchievement: "wins_25", pattern: "rays" },
|
||||
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 1600, reqRating: 1300, pattern: "argyle", motif: "♦" },
|
||||
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 2000, reqAchievement: "wins_50", pattern: "royal", motif: "♛" },
|
||||
{ id: "aurora", nameFa: "شفق", nameEn: "Aurora", c1: "#1d4e6e", c2: "#0a2230", accent: "#5be0c8", price: 2200, reqRating: 1500, pattern: "rays" },
|
||||
{ id: "obsidian", nameFa: "ابسیدین", nameEn: "Obsidian", c1: "#101018", c2: "#000005", accent: "#7c5cff", price: 2600, reqRating: 1700, pattern: "crosshatch", motif: "✦" },
|
||||
{ id: "imperial", nameFa: "شاهنشاهی", nameEn: "Imperial", c1: "#5a3c0a", c2: "#241704", accent: "#ffd76a", price: 3200, reqRating: 1900, reqAchievement: "hakem_7", pattern: "royal", motif: "♔" },
|
||||
// ✨ Luxury card backs — premium purchasable, each a distinct fancy motif
|
||||
{ id: "diamond", nameFa: "الماس", nameEn: "Diamond", c1: "#1a3a55", c2: "#0a1a2e", accent: "#9fe6ff", price: 2800, pattern: "gem", motif: "◆" },
|
||||
{ id: "blackgold", nameFa: "طلای سیاه", nameEn: "Black Gold", c1: "#1a1407", c2: "#000000", accent: "#ffd76a", price: 3500, pattern: "filigree", motif: "♠" },
|
||||
@@ -489,12 +489,12 @@ export const CARD_FRONTS: CardFrontDef[] = [
|
||||
{ id: "rosegold", nameFa: "رزگلد", nameEn: "Rose Gold", bg1: "#fff1ee", bg2: "#f6d9cf", border: "#d98a72", price: 900 },
|
||||
{ id: "velvet", nameFa: "مخمل", nameEn: "Velvet", bg1: "#f4ecff", bg2: "#dcc9f5", border: "#9a6fd0", price: 1800 },
|
||||
{ id: "onyx-face", nameFa: "شبرنگ", nameEn: "Onyx", bg1: "#2a2a31", bg2: "#16161c", border: "#5a5a6a", price: 1200 },
|
||||
// earned by rank / wins
|
||||
{ id: "parchment", nameFa: "پوستنوشت", nameEn: "Parchment", bg1: "#fbf2d8", bg2: "#efd9a3", border: "#caa84a", price: 0, unlockRating: 1300 },
|
||||
{ id: "mint", nameFa: "نعنایی", nameEn: "Mint", bg1: "#f0fff8", bg2: "#d3f3e3", border: "#57c79a", price: 0, unlockWins: 50 },
|
||||
{ id: "goldleaf", nameFa: "زرورق", nameEn: "Gold Leaf", bg1: "#fff7df", bg2: "#f2dd9b", border: "#caa53a", price: 0, unlockRating: 1500 },
|
||||
{ id: "crystal", nameFa: "بلور", nameEn: "Crystal", bg1: "#eefcff", bg2: "#cdeefa", border: "#5fb6d6", price: 0, unlockRating: 1700 },
|
||||
{ id: "imperial-face", nameFa: "شاهانه", nameEn: "Imperial", bg1: "#fff4cf", bg2: "#ecc873", border: "#b8862a", price: 0, unlockWins: 100 },
|
||||
// Rank/achievement gated — always buyable with coins once the gate is met.
|
||||
{ id: "parchment", nameFa: "پوستنوشت", nameEn: "Parchment", bg1: "#fbf2d8", bg2: "#efd9a3", border: "#caa84a", price: 1400, reqRating: 1300 },
|
||||
{ id: "mint", nameFa: "نعنایی", nameEn: "Mint", bg1: "#f0fff8", bg2: "#d3f3e3", border: "#57c79a", price: 1600, reqAchievement: "wins_50" },
|
||||
{ id: "goldleaf", nameFa: "زرورق", nameEn: "Gold Leaf", bg1: "#fff7df", bg2: "#f2dd9b", border: "#caa53a", price: 2000, reqRating: 1500 },
|
||||
{ id: "crystal", nameFa: "بلور", nameEn: "Crystal", bg1: "#eefcff", bg2: "#cdeefa", border: "#5fb6d6", price: 2400, reqRating: 1700 },
|
||||
{ id: "imperial-face", nameFa: "شاهانه", nameEn: "Imperial", bg1: "#fff4cf", bg2: "#ecc873", border: "#b8862a", price: 2800, reqAchievement: "wins_100" },
|
||||
// ✨ Luxury card fronts — premium purchasable
|
||||
{ id: "diamond-face", nameFa: "الماس", nameEn: "Diamond", bg1: "#f4fdff", bg2: "#d7f0fb", border: "#7fc6e6", price: 2500 },
|
||||
{ id: "blackgold-face", nameFa: "طلای سیاه", nameEn: "Black Gold", bg1: "#2a2410", bg2: "#14110a", border: "#caa53a", price: 3200 },
|
||||
@@ -563,17 +563,16 @@ export function cardFrontById(id: string): CardFrontDef {
|
||||
return CARD_FRONTS.find((c) => c.id === id) ?? CARD_FRONTS[0];
|
||||
}
|
||||
|
||||
// Ownership = default items + what the player has bought. Everything else must be
|
||||
// purchased with coins (req* gates the purchase, it never auto-grants).
|
||||
function ownedCosmeticIds(
|
||||
defs: { id: string; price: number; default?: boolean; unlockRating?: number; unlockWins?: number }[],
|
||||
profile: UserProfile,
|
||||
defs: { id: string; default?: boolean }[],
|
||||
_profile: UserProfile,
|
||||
purchased: string[]
|
||||
): string[] {
|
||||
const ids = new Set<string>();
|
||||
for (const d of defs) {
|
||||
const earned =
|
||||
(d.unlockRating != null && profile.rating >= d.unlockRating) ||
|
||||
(d.unlockWins != null && profile.stats.wins >= d.unlockWins);
|
||||
if (d.default || earned || purchased.includes(d.id)) ids.add(d.id);
|
||||
if (d.default || purchased.includes(d.id)) ids.add(d.id);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
@@ -582,15 +581,12 @@ export function ownedCardBackIds(profile: UserProfile): string[] {
|
||||
return ownedCosmeticIds(CARD_BACKS, profile, profile.ownedCardBacks ?? []);
|
||||
}
|
||||
|
||||
/** Avatars the player owns (default + rank/wins-earned + purchased). */
|
||||
/** Avatars the player owns (default + purchased). */
|
||||
export function ownedAvatarIds(profile: UserProfile): string[] {
|
||||
const purchased = profile.ownedAvatars ?? [];
|
||||
const ids = new Set<string>();
|
||||
for (const a of AVATARS) {
|
||||
const earned =
|
||||
(a.unlockRating != null && profile.rating >= a.unlockRating) ||
|
||||
(a.unlockWins != null && profile.stats.wins >= a.unlockWins);
|
||||
if (a.default || earned || purchased.includes(a.id)) ids.add(a.id);
|
||||
if (a.default || purchased.includes(a.id)) ids.add(a.id);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
@@ -604,23 +600,20 @@ export const REACTION_PACKS: ReactionPackDef[] = [
|
||||
{ id: "starter", nameFa: "پایه", nameEn: "Starter", reactions: ["👍", "👏", "😂", "😮"], price: 0, default: true },
|
||||
{ id: "emotions", nameFa: "احساسات", nameEn: "Emotions", reactions: ["😎", "😭", "🤯", "🥳", "😍"], price: 600 },
|
||||
{ id: "taunt", nameFa: "طعنه", nameEn: "Taunts", reactions: ["😏", "🤡", "🙄", "😴", "🥱"], price: 900 },
|
||||
{ id: "champion", nameFa: "قهرمان", nameEn: "Champion", reactions: ["👑", "🏆", "💪", "🔥"], price: 0, unlockRating: 1300 },
|
||||
{ id: "legend", nameFa: "اسطوره", nameEn: "Legend", reactions: ["💎", "⚡", "🐐", "🎯"], price: 0, unlockWins: 100 },
|
||||
{ id: "champion", nameFa: "قهرمان", nameEn: "Champion", reactions: ["👑", "🏆", "💪", "🔥"], price: 1200, reqRating: 1300 },
|
||||
{ id: "legend", nameFa: "اسطوره", nameEn: "Legend", reactions: ["💎", "⚡", "🐐", "🎯"], price: 1800, reqAchievement: "wins_100" },
|
||||
];
|
||||
|
||||
export function reactionPackById(id: string): ReactionPackDef | undefined {
|
||||
return REACTION_PACKS.find((p) => p.id === id);
|
||||
}
|
||||
|
||||
/** Which packs the player currently owns (default + earned + purchased). */
|
||||
/** Which packs the player currently owns (default + purchased). */
|
||||
export function ownedReactionPackIds(profile: UserProfile): string[] {
|
||||
const purchased = profile.ownedReactionPacks ?? [];
|
||||
const ids = new Set<string>();
|
||||
for (const p of REACTION_PACKS) {
|
||||
const earned =
|
||||
(p.unlockRating != null && profile.rating >= p.unlockRating) ||
|
||||
(p.unlockWins != null && profile.stats.wins >= p.unlockWins);
|
||||
if (p.default || earned || purchased.includes(p.id)) ids.add(p.id);
|
||||
if (p.default || purchased.includes(p.id)) ids.add(p.id);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
@@ -635,17 +628,17 @@ export function ownedReactions(profile: UserProfile): string[] {
|
||||
|
||||
export const STICKER_PACKS: StickerPackDef[] = [
|
||||
{ id: "faces", nameFa: "شکلکها", nameEn: "Faces", stickers: ["happy", "sad", "cool", "love", "angry"], price: 0, default: true },
|
||||
// Earned by the "Seven–Zip" (7–0 sweep) achievement.
|
||||
{ id: "hokm", nameFa: "حکم", nameEn: "Hokm", stickers: ["hokm-badge", "kot-stamp", "crown", "ace-spade"], price: 0, unlockAchievement: "shutout_1" },
|
||||
// Earned by the "100 Wins" achievement.
|
||||
{ id: "persian", nameFa: "ایرانی", nameEn: "Persian", stickers: ["chai", "afarin", "rose"], price: 700, unlockAchievement: "wins_100" },
|
||||
// Earned by the "25 Kots" achievement.
|
||||
{ id: "taunt", nameFa: "طعنه", nameEn: "Taunts", stickers: ["clown", "sleep", "weak"], price: 900, unlockAchievement: "kot_25" },
|
||||
// Achievement-gated — buyable with coins once the "Seven–Zip" (7–0 sweep) is unlocked.
|
||||
{ id: "hokm", nameFa: "حکم", nameEn: "Hokm", stickers: ["hokm-badge", "kot-stamp", "crown", "ace-spade"], price: 1200, reqAchievement: "shutout_1" },
|
||||
// Achievement-gated — "100 Wins".
|
||||
{ id: "persian", nameFa: "ایرانی", nameEn: "Persian", stickers: ["chai", "afarin", "rose"], price: 700, reqAchievement: "wins_100" },
|
||||
// Achievement-gated — "25 Kots".
|
||||
{ id: "taunt", nameFa: "طعنه", nameEn: "Taunts", stickers: ["clown", "sleep", "weak"], price: 900, reqAchievement: "kot_25" },
|
||||
// Persian-text stamps (کوت! / دمت گرم / باریکلا / آخه؟) — purchasable.
|
||||
{ id: "persian-text", nameFa: "متن فارسی", nameEn: "Persian Text", stickers: ["kot-text", "damet-garm", "barikalla", "akhe"], price: 1100 },
|
||||
// Custom packs earned only via achievements / rank.
|
||||
{ id: "rulership", nameFa: "حاکمیت", nameEn: "Rulership", stickers: ["crown-gold", "seven-zip"], price: 0, unlockAchievement: "hakem_7" },
|
||||
{ id: "firestorm", nameFa: "آتشین", nameEn: "Firestorm", stickers: ["streak-fire"], price: 0, unlockAchievement: "streak_10" },
|
||||
// Achievement-gated premium packs (coin + achievement).
|
||||
{ id: "rulership", nameFa: "حاکمیت", nameEn: "Rulership", stickers: ["crown-gold", "seven-zip"], price: 1500, reqAchievement: "hakem_7" },
|
||||
{ id: "firestorm", nameFa: "آتشین", nameEn: "Firestorm", stickers: ["streak-fire"], price: 1500, reqAchievement: "streak_10" },
|
||||
|
||||
/* ---- New themed packs: کلکل (banter), Persian trends, Hokm/game ---- */
|
||||
// کلکل / تیکه — trash-talk you fling at the table
|
||||
@@ -657,11 +650,11 @@ export const STICKER_PACKS: StickerPackDef[] = [
|
||||
{ id: "tashvigh", nameFa: "تشویق", nameEn: "Cheers", stickers: ["damet-garm-2", "nush-jan", "be-be", "ghorbunet"], price: 700 },
|
||||
// Hokm / card-game themed
|
||||
{ id: "khanevadeh", nameFa: "خانواده خال", nameEn: "Court Cards", stickers: ["tak-khal", "as-del", "shah-khesht", "bibi-gesht"], price: 1200 },
|
||||
{ id: "victory", nameFa: "پیروزی", nameEn: "Victory", stickers: ["bardim", "hokm-text", "jam-kon", "kish-mat"], price: 0, unlockRating: 1500 },
|
||||
{ id: "victory", nameFa: "پیروزی", nameEn: "Victory", stickers: ["bardim", "hokm-text", "jam-kon", "kish-mat"], price: 1800, reqRating: 1500 },
|
||||
// Extra emotions
|
||||
{ id: "ehsasat", nameFa: "احساسات", nameEn: "Moods", stickers: ["laugh", "shocked", "cry", "smug"], price: 600 },
|
||||
// Mega banter bundle (earned, not sold) — the spicy stuff for rivals
|
||||
{ id: "raghib", nameFa: "رقیب", nameEn: "Rivalry", stickers: ["khdahafez", "weak", "clown", "sleep"], price: 0, unlockAchievement: "kot_10" },
|
||||
// Spicy rival banter — coin + achievement gated.
|
||||
{ id: "raghib", nameFa: "رقیب", nameEn: "Rivalry", stickers: ["khdahafez", "weak", "clown", "sleep"], price: 1300, reqAchievement: "kot_10" },
|
||||
];
|
||||
|
||||
export function stickerPackById(id: string): StickerPackDef | undefined {
|
||||
@@ -670,14 +663,9 @@ export function stickerPackById(id: string): StickerPackDef | undefined {
|
||||
|
||||
export function ownedStickerPackIds(profile: UserProfile): string[] {
|
||||
const purchased = profile.ownedStickerPacks ?? [];
|
||||
const unlocked = profile.unlocked ?? [];
|
||||
const ids = new Set<string>();
|
||||
for (const p of STICKER_PACKS) {
|
||||
const earned =
|
||||
(p.unlockRating != null && profile.rating >= p.unlockRating) ||
|
||||
(p.unlockWins != null && profile.stats.wins >= p.unlockWins) ||
|
||||
(p.unlockAchievement != null && unlocked.includes(p.unlockAchievement));
|
||||
if (p.default || earned || purchased.includes(p.id)) ids.add(p.id);
|
||||
if (p.default || purchased.includes(p.id)) ids.add(p.id);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+26
-25
@@ -255,11 +255,10 @@ export interface CardBackDef {
|
||||
/** optional centered emblem glyph (luxury backs) */
|
||||
motif?: string;
|
||||
default?: boolean;
|
||||
unlockRating?: number;
|
||||
unlockWins?: number;
|
||||
/** purchase gate: locked until this level / rating is reached */
|
||||
/** purchase gate: locked (shown, not buyable) until met. Always priced. */
|
||||
reqLevel?: number;
|
||||
reqRating?: number;
|
||||
reqAchievement?: string;
|
||||
}
|
||||
|
||||
export interface CardFrontDef {
|
||||
@@ -271,8 +270,9 @@ export interface CardFrontDef {
|
||||
border: string; // face border color
|
||||
price: number;
|
||||
default?: boolean;
|
||||
unlockRating?: number;
|
||||
unlockWins?: number;
|
||||
reqLevel?: number;
|
||||
reqRating?: number;
|
||||
reqAchievement?: string;
|
||||
}
|
||||
|
||||
/* --------------------- Reactions (Sheklak / شکلک) -------------------- */
|
||||
@@ -282,10 +282,11 @@ export interface ReactionPackDef {
|
||||
nameFa: string;
|
||||
nameEn: string;
|
||||
reactions: string[]; // emoji/sticker chars
|
||||
price: number; // >0 → purchasable in the shop
|
||||
price: number; // coins (always purchasable; req* may gate it)
|
||||
default?: boolean; // owned from the start
|
||||
unlockRating?: number; // earned by reaching this rating
|
||||
unlockWins?: number; // earned by total wins
|
||||
reqLevel?: number;
|
||||
reqRating?: number;
|
||||
reqAchievement?: string;
|
||||
}
|
||||
|
||||
export interface StickerPackDef {
|
||||
@@ -293,11 +294,11 @@ export interface StickerPackDef {
|
||||
nameFa: string;
|
||||
nameEn: string;
|
||||
stickers: string[]; // Sticker artwork ids (see components/online/Sticker.tsx)
|
||||
price: number; // >0 → purchasable in the shop
|
||||
price: number; // coins (always purchasable; req* may gate it)
|
||||
default?: boolean;
|
||||
unlockRating?: number;
|
||||
unlockWins?: number;
|
||||
unlockAchievement?: string; // earned when this achievement id is unlocked
|
||||
reqLevel?: number;
|
||||
reqRating?: number;
|
||||
reqAchievement?: string; // gate: must have unlocked this achievement to buy
|
||||
}
|
||||
|
||||
/* ------------------------------ Friends ------------------------------ */
|
||||
@@ -471,9 +472,10 @@ export interface ShopItem {
|
||||
/** short fa/en description of what the item is/does */
|
||||
descFa?: string;
|
||||
descEn?: string;
|
||||
/** purchase gate: locked (shown, not buyable) until met. Server-enforced via the tier in the id. */
|
||||
/** purchase gate: locked (shown, not buyable) until met. Server-enforced. */
|
||||
reqLevel?: number;
|
||||
reqRating?: number;
|
||||
reqAchievement?: string;
|
||||
}
|
||||
|
||||
/* ------------------------------ Coin packs --------------------------- */
|
||||
@@ -611,13 +613,12 @@ export interface AppNotification {
|
||||
export interface AvatarDef {
|
||||
id: string;
|
||||
emoji: string;
|
||||
price?: number; // >0 → purchasable in the shop
|
||||
price?: number; // coins (always purchasable; req* may gate it)
|
||||
default?: boolean; // owned from the start
|
||||
unlockRating?: number; // earned at this rating (better avatars = higher rank)
|
||||
unlockWins?: number;
|
||||
/** purchase gate: locked until this level / rating is reached */
|
||||
/** purchase gate: locked (shown, not buyable) until met. Always priced. */
|
||||
reqLevel?: number;
|
||||
reqRating?: number;
|
||||
reqAchievement?: string;
|
||||
}
|
||||
|
||||
export const AVATARS: AvatarDef[] = [
|
||||
@@ -640,14 +641,14 @@ export const AVATARS: AvatarDef[] = [
|
||||
{ id: "a-diamond", emoji: "💎", price: 3000 },
|
||||
{ id: "a-moneybag", emoji: "💰", price: 3500 },
|
||||
{ id: "a-trophy", emoji: "🏆", price: 4000 },
|
||||
// earned by rank / wins — the rarer faces sit behind higher ranks
|
||||
{ id: "a-robot", emoji: "🤖", unlockWins: 50 },
|
||||
{ id: "a-wizard", emoji: "🧙", unlockRating: 1300 },
|
||||
{ id: "a-ninja", emoji: "🥷", unlockWins: 100 },
|
||||
{ id: "a-king", emoji: "🤴", unlockRating: 1500 },
|
||||
{ id: "a-genie", emoji: "🧞", unlockRating: 1700 },
|
||||
{ id: "a-crown", emoji: "👑", unlockRating: 1900 },
|
||||
{ id: "a-gem", emoji: "💠", unlockRating: 2100 },
|
||||
// Rank/achievement gated — always buyable with coins once the gate is met.
|
||||
{ id: "a-robot", emoji: "🤖", price: 1200, reqAchievement: "wins_50" },
|
||||
{ id: "a-wizard", emoji: "🧙", price: 2500, reqRating: 1300 },
|
||||
{ id: "a-ninja", emoji: "🥷", price: 1800, reqAchievement: "wins_100" },
|
||||
{ id: "a-king", emoji: "🤴", price: 3000, reqRating: 1500 },
|
||||
{ id: "a-genie", emoji: "🧞", price: 3800, reqRating: 1700 },
|
||||
{ id: "a-crown", emoji: "👑", price: 5000, reqRating: 1900, reqAchievement: "hakem_7" },
|
||||
{ id: "a-gem", emoji: "💠", price: 6500, reqRating: 2100, reqAchievement: "shutout_10" },
|
||||
];
|
||||
|
||||
/* ----------------------- Gated gift catalogue ------------------------ */
|
||||
|
||||
Reference in New Issue
Block a user