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:
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user