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 })}
|
||||
|
||||
Reference in New Issue
Block a user