More cosmetics (rank-gated) + steeper level curve capped at 100
Cosmetics — many new variants, the rarer ones gated behind higher ranks: - Card backs: +midnight/jade/onyx (buy) + crimson/aurora/obsidian/imperial (earned by wins/rating up to Master). Card fronts: +sunset/velvet/onyx (buy) + goldleaf/crystal/imperial (earned). - Titles: +marksman, untouchable, sweeper, ruler, platinum_star, diamond_ace, immortal, the_one (gated by kots/streak/shutouts/hakem/rating/level/wins), mirrored on the server so live games grant them. - Avatars: list expanded + rank/wins-earned tier (robot/wizard/ninja/king/ genie/crown) via new ownedAvatarIds(); profile picker shows earned ones, shop sells the priced ones. - Stickers: new Persian-text stamp pack (کوت! / دمت گرم / باریکلا / آخه؟) plus a rank-earned Victory pack (بردیم!/حکم) — new inline-SVG art. Leveling: XP per level now grows (100*l + 15*l²) so each level is harder; higher leagues grant more XP (×1.5 at 500 stake, ×2 at 1000) so you progress by playing up. Hard cap at level 100. Mirrored in server Gamification (XpForLevel/MatchXp/ AddXp). Sim now tops out lower (level 20 vs 35 over 500 matches) as intended. Verified: tsc + next build + dotnet build clean; sim passes; images rebuilt :1500/:1505. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -224,6 +224,44 @@ const STICKERS: Record<string, React.ReactNode> = {
|
||||
<path d="M50 18 C58 34 70 38 66 56 C64 70 54 78 50 78 C46 78 34 72 34 56 C34 46 42 44 44 36 C50 42 48 50 52 52 C58 50 54 38 50 18 Z" fill="url(#sf)" />
|
||||
</>
|
||||
),
|
||||
|
||||
/* ---------------------- Persian-text stamps ------------------------- */
|
||||
"kot-text": (
|
||||
<>
|
||||
<rect x="6" y="26" width="88" height="48" rx="10" fill="#7a0f1a" stroke="#ff6b81" strokeWidth="3" transform="rotate(-8 50 50)" />
|
||||
<text x="50" y="61" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="34" fill="#ffd9de" transform="rotate(-8 50 50)">کوت!</text>
|
||||
</>
|
||||
),
|
||||
"hokm-text": (
|
||||
<>
|
||||
<circle cx="50" cy="50" r="42" fill="#13314d" stroke="#d4af37" strokeWidth="3" />
|
||||
<text x="50" y="62" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="30" fill="#ffe488">حکم</text>
|
||||
</>
|
||||
),
|
||||
"damet-garm": (
|
||||
<>
|
||||
<rect x="8" y="30" width="84" height="40" rx="20" fill="#0d6b5e" stroke="#2dd4bf" strokeWidth="3" />
|
||||
<text x="50" y="57" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="19" fill="#d8fff5">دمت گرم</text>
|
||||
</>
|
||||
),
|
||||
barikalla: (
|
||||
<>
|
||||
<circle cx="50" cy="50" r="42" fill="#5a3c0a" stroke="#ffd76a" strokeWidth="3" />
|
||||
<text x="50" y="58" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="20" fill="#ffe9a8">باریکلا</text>
|
||||
</>
|
||||
),
|
||||
akhe: (
|
||||
<>
|
||||
<circle cx="50" cy="50" r="42" fill="#3a2a4d" stroke="#c77dff" strokeWidth="3" />
|
||||
<text x="50" y="60" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="24" fill="#e7d4ff">آخه؟!</text>
|
||||
</>
|
||||
),
|
||||
bardim: (
|
||||
<>
|
||||
<rect x="6" y="28" width="88" height="44" rx="10" fill="#136f3a" stroke="#7fe3a0" strokeWidth="3" transform="rotate(6 50 50)" />
|
||||
<text x="50" y="59" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="26" fill="#daffe4" transform="rotate(6 50 50)">بردیم!</text>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
export const STICKER_IDS = Object.keys(STICKERS);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
CARD_FRONTS,
|
||||
TITLES,
|
||||
achievementProgress,
|
||||
ownedAvatarIds,
|
||||
ownedCardBackIds,
|
||||
ownedCardFrontIds,
|
||||
} from "@/lib/online/gamification";
|
||||
@@ -43,6 +44,7 @@ export function ProfileScreen() {
|
||||
const titleName = titleDef ? (locale === "fa" ? titleDef.nameFa : titleDef.nameEn) : null;
|
||||
const ownedFronts = new Set(ownedCardFrontIds(profile));
|
||||
const ownedBacks = new Set(ownedCardBackIds(profile));
|
||||
const ownedAvatars = new Set(ownedAvatarIds(profile));
|
||||
|
||||
const saveName = async () => {
|
||||
if (name.trim()) await updateProfile({ displayName: name.trim() });
|
||||
@@ -142,7 +144,7 @@ export function ProfileScreen() {
|
||||
<div className="glass rounded-2xl p-4 mt-4">
|
||||
<h3 className="text-sm font-bold text-cream/80 mb-3">{t("profile.chooseAvatar")}</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{AVATARS.filter((a) => profile.ownedAvatars.includes(a.id)).map((a) => (
|
||||
{AVATARS.filter((a) => ownedAvatars.has(a.id)).map((a) => (
|
||||
<button
|
||||
key={a.id}
|
||||
onClick={() => updateProfile({ avatar: a.id })}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// daily rewards, achievements. No side effects, no storage — unit-testable.
|
||||
|
||||
import {
|
||||
AVATARS,
|
||||
AchievementCategoryDef,
|
||||
AchievementCategoryId,
|
||||
AchievementDef,
|
||||
@@ -131,18 +132,30 @@ export function leagueById(id: string): MatchLeague {
|
||||
|
||||
/* ------------------------------- XP ---------------------------------- */
|
||||
|
||||
/** XP required to advance from `level` to `level + 1`. */
|
||||
/** Hard level cap. */
|
||||
export const MAX_LEVEL = 100;
|
||||
|
||||
/** XP required to advance from `level` to `level + 1` — grows with level, so
|
||||
* each level is harder than the last. */
|
||||
export function xpNeededForLevel(level: number): number {
|
||||
return 100 * level;
|
||||
return 100 * level + 15 * level * level;
|
||||
}
|
||||
|
||||
/** Higher leagues (bigger stake) grant more XP, so high-level players progress
|
||||
* by playing up rather than grinding the starter league. */
|
||||
export function leagueXpFactor(stake: number): number {
|
||||
if (stake >= 1000) return 2;
|
||||
if (stake >= 500) return 1.5;
|
||||
return 1;
|
||||
}
|
||||
|
||||
export function matchXp(summary: MatchSummary): number {
|
||||
return (
|
||||
const base =
|
||||
40 +
|
||||
(summary.won ? 80 : 0) +
|
||||
summary.tricksWon * 5 +
|
||||
(summary.kotFor ? 30 : 0)
|
||||
);
|
||||
(summary.kotFor ? 30 : 0);
|
||||
return Math.round(base * leagueXpFactor(summary.stake));
|
||||
}
|
||||
|
||||
export interface LevelProgress {
|
||||
@@ -155,11 +168,16 @@ export function addXp(level: number, xpInLevel: number, gained: number): LevelPr
|
||||
let lvl = level;
|
||||
let xp = xpInLevel + gained;
|
||||
let leveledUp = false;
|
||||
while (xp >= xpNeededForLevel(lvl)) {
|
||||
while (lvl < MAX_LEVEL && xp >= xpNeededForLevel(lvl)) {
|
||||
xp -= xpNeededForLevel(lvl);
|
||||
lvl += 1;
|
||||
leveledUp = true;
|
||||
}
|
||||
// At the cap, don't let XP overflow the bar.
|
||||
if (lvl >= MAX_LEVEL) {
|
||||
lvl = MAX_LEVEL;
|
||||
xp = Math.min(xp, xpNeededForLevel(MAX_LEVEL));
|
||||
}
|
||||
return { level: lvl, xp, leveledUp };
|
||||
}
|
||||
|
||||
@@ -288,8 +306,16 @@ export const TITLES: TitleDef[] = [
|
||||
{ id: "professional", nameFa: "حرفهای", nameEn: "Professional", hintFa: "۵۰ برد", hintEn: "50 wins" },
|
||||
{ id: "veteran", nameFa: "کهنهکار", nameEn: "Veteran", hintFa: "سطح ۳۰", hintEn: "Level 30" },
|
||||
{ id: "captain", nameFa: "کاپیتان", nameEn: "Captain", hintFa: "۱۰۰ برد", hintEn: "100 wins" },
|
||||
{ id: "marksman", nameFa: "کماندار", nameEn: "Marksman", hintFa: "۵۰ کُت", hintEn: "50 kots" },
|
||||
{ id: "untouchable", nameFa: "شکستناپذیر", nameEn: "Untouchable", hintFa: "۱۰ برد پیاپی", hintEn: "10 win streak" },
|
||||
{ id: "sweeper", nameFa: "جاروکش", nameEn: "Sweeper", hintFa: "۱۰ هفت–هیچ", hintEn: "10 sweeps" },
|
||||
{ id: "ruler", nameFa: "فرمانروا", nameEn: "Ruler", hintFa: "۵۰ بار حاکم", hintEn: "50× hakem" },
|
||||
{ id: "champion", nameFa: "قهرمان", nameEn: "Champion", hintFa: "لیگ طلا", hintEn: "Gold league" },
|
||||
{ id: "platinum_star", nameFa: "ستاره پلاتین", nameEn: "Platinum Star", hintFa: "لیگ پلاتین", hintEn: "Platinum league" },
|
||||
{ id: "leader", nameFa: "فرمانده", nameEn: "Leader", hintFa: "۲۵۰ برد", hintEn: "250 wins" },
|
||||
{ id: "diamond_ace", nameFa: "آس الماس", nameEn: "Diamond Ace", hintFa: "لیگ الماس", hintEn: "Diamond league" },
|
||||
{ id: "immortal", nameFa: "جاودانه", nameEn: "Immortal", hintFa: "سطح ۵۰", hintEn: "Level 50" },
|
||||
{ id: "the_one", nameFa: "یگانه", nameEn: "The One", hintFa: "۵۰۰ برد", hintEn: "500 wins" },
|
||||
{ id: "legend", nameFa: "اسطوره", nameEn: "Legend", hintFa: "لیگ استاد", hintEn: "Master league" },
|
||||
];
|
||||
|
||||
@@ -314,10 +340,26 @@ export function titleUnlocked(
|
||||
return level >= 30;
|
||||
case "captain":
|
||||
return stats.wins >= 100;
|
||||
case "marksman":
|
||||
return stats.kotsFor >= 50;
|
||||
case "untouchable":
|
||||
return stats.bestWinStreak >= 10;
|
||||
case "sweeper":
|
||||
return (stats.shutoutWins ?? 0) >= 10;
|
||||
case "ruler":
|
||||
return (stats.hakemRounds ?? 0) >= 50;
|
||||
case "champion":
|
||||
return rating >= tierById("gold").floor;
|
||||
case "platinum_star":
|
||||
return rating >= tierById("platinum").floor;
|
||||
case "leader":
|
||||
return stats.wins >= 250;
|
||||
case "diamond_ace":
|
||||
return rating >= tierById("diamond").floor;
|
||||
case "immortal":
|
||||
return level >= 50;
|
||||
case "the_one":
|
||||
return stats.wins >= 500;
|
||||
case "legend":
|
||||
return rating >= tierById("master").floor;
|
||||
default:
|
||||
@@ -330,19 +372,34 @@ export function titleUnlocked(
|
||||
// Card BACKS (pattern on the reverse of every card).
|
||||
export const CARD_BACKS: CardBackDef[] = [
|
||||
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", c1: "#14274f", c2: "#0a142e", accent: "#d4af37", price: 0, default: true },
|
||||
{ id: "midnight", nameFa: "نیمهشب", nameEn: "Midnight", c1: "#1b2540", c2: "#0a0f1f", accent: "#8aa0c8", price: 1200 },
|
||||
{ id: "sapphire", nameFa: "یاقوت کبود", nameEn: "Sapphire", c1: "#0b3a82", c2: "#06173a", accent: "#6aa6ff", price: 800 },
|
||||
{ id: "emerald", nameFa: "زمرد", nameEn: "Emerald", c1: "#0d6b5e", c2: "#062420", accent: "#2dd4bf", price: 1000 },
|
||||
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 0, unlockRating: 1300 }, // earned
|
||||
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 0, unlockWins: 50 }, // earned
|
||||
{ id: "jade", nameFa: "یشم", nameEn: "Jade", c1: "#136f63", c2: "#08221e", accent: "#7fe3c0", price: 2000 },
|
||||
{ id: "onyx", nameFa: "اونیکس", nameEn: "Onyx", c1: "#26262b", c2: "#0c0c10", accent: "#b0b0c0", price: 1500 },
|
||||
// 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 },
|
||||
{ id: "ruby", nameFa: "یاقوت", nameEn: "Ruby", c1: "#7f1d2e", c2: "#2b0a12", accent: "#ff7a90", price: 0, unlockRating: 1300 },
|
||||
{ id: "royal", nameFa: "سلطنتی", nameEn: "Royal", c1: "#4a1d7f", c2: "#1a0a2e", accent: "#c77dff", price: 0, unlockWins: 50 },
|
||||
{ id: "aurora", nameFa: "شفق", nameEn: "Aurora", c1: "#1d4e6e", c2: "#0a2230", accent: "#5be0c8", price: 0, unlockRating: 1500 },
|
||||
{ id: "obsidian", nameFa: "ابسیدین", nameEn: "Obsidian", c1: "#101018", c2: "#000005", accent: "#7c5cff", price: 0, unlockRating: 1700 },
|
||||
{ id: "imperial", nameFa: "شاهنشاهی", nameEn: "Imperial", c1: "#5a3c0a", c2: "#241704", accent: "#ffd76a", price: 0, unlockRating: 1900 },
|
||||
];
|
||||
|
||||
// Card FRONTS (the face background/border behind the suit + rank).
|
||||
export const CARD_FRONTS: CardFrontDef[] = [
|
||||
{ id: "classic", nameFa: "کلاسیک", nameEn: "Classic", bg1: "#fffdf7", bg2: "#f3ead2", border: "rgba(0,0,0,0.12)", price: 0, default: true },
|
||||
{ id: "ivory", nameFa: "عاج", nameEn: "Ivory", bg1: "#ffffff", bg2: "#eef2f8", border: "#c9ccd6", price: 600 },
|
||||
{ id: "sunset", nameFa: "غروب", nameEn: "Sunset", bg1: "#fff3e6", bg2: "#ffd9b0", border: "#e0915a", price: 1000 },
|
||||
{ id: "rosegold", nameFa: "رزگلد", nameEn: "Rose Gold", bg1: "#fff1ee", bg2: "#f6d9cf", border: "#d98a72", price: 900 },
|
||||
{ id: "parchment", nameFa: "پوستنوشت", nameEn: "Parchment", bg1: "#fbf2d8", bg2: "#efd9a3", border: "#caa84a", price: 0, unlockRating: 1300 }, // earned
|
||||
{ id: "mint", nameFa: "نعنایی", nameEn: "Mint", bg1: "#f0fff8", bg2: "#d3f3e3", border: "#57c79a", price: 0, unlockWins: 50 }, // earned
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
export function cardBackById(id: string): CardBackDef {
|
||||
@@ -370,6 +427,19 @@ function ownedCosmeticIds(
|
||||
export function ownedCardBackIds(profile: UserProfile): string[] {
|
||||
return ownedCosmeticIds(CARD_BACKS, profile, profile.ownedCardBacks ?? []);
|
||||
}
|
||||
|
||||
/** Avatars the player owns (default + rank/wins-earned + 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);
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
export function ownedCardFrontIds(profile: UserProfile): string[] {
|
||||
return ownedCosmeticIds(CARD_FRONTS, profile, profile.ownedCardFronts ?? []);
|
||||
}
|
||||
@@ -417,9 +487,12 @@ export const STICKER_PACKS: StickerPackDef[] = [
|
||||
{ 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" },
|
||||
// Custom packs earned only via achievements.
|
||||
// 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" },
|
||||
{ id: "victory", nameFa: "پیروزی", nameEn: "Victory", stickers: ["bardim", "hokm-text"], price: 0, unlockRating: 1500 },
|
||||
];
|
||||
|
||||
export function stickerPackById(id: string): StickerPackDef | undefined {
|
||||
|
||||
@@ -839,12 +839,12 @@ export class MockOnlineService implements OnlineService {
|
||||
}
|
||||
|
||||
async getShopItems(): Promise<ShopItem[]> {
|
||||
const avatarItems: ShopItem[] = AVATARS.slice(2).map((a, i) => ({
|
||||
const avatarItems: ShopItem[] = AVATARS.filter((a) => (a.price ?? 0) > 0).map((a) => ({
|
||||
id: a.id,
|
||||
kind: "avatar",
|
||||
nameFa: "آواتار",
|
||||
nameEn: "Avatar",
|
||||
price: 500 + i * 150,
|
||||
price: a.price!,
|
||||
preview: a.emoji,
|
||||
}));
|
||||
const backItems: ShopItem[] = CARD_BACKS.filter((c) => c.price > 0).map((c) => ({
|
||||
|
||||
+30
-11
@@ -499,17 +499,36 @@ export interface AppNotification {
|
||||
|
||||
/* ------------------------------ Avatars ------------------------------ */
|
||||
|
||||
export const AVATARS: { id: string; emoji: string }[] = [
|
||||
{ id: "a-fox", emoji: "🦊" },
|
||||
{ id: "a-lion", emoji: "🦁" },
|
||||
{ id: "a-owl", emoji: "🦉" },
|
||||
{ id: "a-tiger", emoji: "🐯" },
|
||||
{ id: "a-panda", emoji: "🐼" },
|
||||
{ id: "a-eagle", emoji: "🦅" },
|
||||
{ id: "a-wolf", emoji: "🐺" },
|
||||
{ id: "a-cat", emoji: "🐱" },
|
||||
{ id: "a-dragon", emoji: "🐲" },
|
||||
{ id: "a-unicorn", emoji: "🦄" },
|
||||
export interface AvatarDef {
|
||||
id: string;
|
||||
emoji: string;
|
||||
price?: number; // >0 → purchasable in the shop
|
||||
default?: boolean; // owned from the start
|
||||
unlockRating?: number; // earned at this rating (better avatars = higher rank)
|
||||
unlockWins?: number;
|
||||
}
|
||||
|
||||
export const AVATARS: AvatarDef[] = [
|
||||
{ id: "a-fox", emoji: "🦊", default: true },
|
||||
{ id: "a-lion", emoji: "🦁", default: true },
|
||||
{ id: "a-owl", emoji: "🦉", price: 400 },
|
||||
{ id: "a-cat", emoji: "🐱", price: 500 },
|
||||
{ id: "a-tiger", emoji: "🐯", price: 500 },
|
||||
{ id: "a-panda", emoji: "🐼", price: 600 },
|
||||
{ id: "a-bear", emoji: "🐻", price: 600 },
|
||||
{ id: "a-eagle", emoji: "🦅", price: 700 },
|
||||
{ id: "a-wolf", emoji: "🐺", price: 700 },
|
||||
{ id: "a-shark", emoji: "🦈", price: 900 },
|
||||
{ id: "a-dragon", emoji: "🐲", price: 1500 },
|
||||
{ id: "a-unicorn", emoji: "🦄", price: 1500 },
|
||||
{ id: "a-peacock", emoji: "🦚", price: 2000 },
|
||||
// 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 },
|
||||
];
|
||||
|
||||
export function avatarEmoji(id: string): string {
|
||||
|
||||
Reference in New Issue
Block a user