Files
HokmPlay/src/components/online/Sticker.tsx
T
soroush.asadi 4199a82c9d
CI/CD / CI - API (dotnet build + engine sim) (push) Failing after 1m40s
CI/CD / CI - Web (tsc + next build) (push) Failing after 1m20s
CI/CD / Deploy - local stack (db + server + web) (push) Has been skipped
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>
2026-06-04 23:43:21 +03:30

278 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/**
* Hand-designed sticker artwork as inline SVG — no external assets.
* Each sticker is keyed by id; packs (see gamification.ts) reference these ids.
*/
type SvgProps = { className?: string };
function Face({
bg1,
bg2,
children,
}: {
bg1: string;
bg2: string;
children: React.ReactNode;
}) {
return (
<>
<defs>
<radialGradient id="fg" cx="40%" cy="35%" r="75%">
<stop offset="0" stopColor={bg1} />
<stop offset="1" stopColor={bg2} />
</radialGradient>
</defs>
<circle cx="50" cy="50" r="44" fill="url(#fg)" stroke="rgba(0,0,0,0.18)" strokeWidth="2" />
{children}
</>
);
}
const STICKERS: Record<string, React.ReactNode> = {
/* ----------------------------- faces ----------------------------- */
happy: (
<Face bg1="#ffe680" bg2="#f5b301">
<circle cx="36" cy="44" r="5" fill="#3a2a00" />
<circle cx="64" cy="44" r="5" fill="#3a2a00" />
<path d="M32 62 Q50 80 68 62" fill="none" stroke="#3a2a00" strokeWidth="5" strokeLinecap="round" />
</Face>
),
sad: (
<Face bg1="#bfe3ff" bg2="#5aa6e0">
<circle cx="36" cy="44" r="5" fill="#13314d" />
<circle cx="64" cy="44" r="5" fill="#13314d" />
<path d="M32 70 Q50 56 68 70" fill="none" stroke="#13314d" strokeWidth="5" strokeLinecap="round" />
<path d="M64 50 q4 10 0 16 q-4 -6 0 -16" fill="#2f8fd6" />
</Face>
),
cool: (
<Face bg1="#ffe680" bg2="#f5b301">
<rect x="24" y="38" width="22" height="13" rx="5" fill="#1b1b1b" />
<rect x="54" y="38" width="22" height="13" rx="5" fill="#1b1b1b" />
<rect x="46" y="42" width="8" height="3" fill="#1b1b1b" />
<path d="M36 64 Q52 74 66 60" fill="none" stroke="#3a2a00" strokeWidth="5" strokeLinecap="round" />
</Face>
),
love: (
<Face bg1="#ffc1e3" bg2="#ff5fa2">
<path d="M30 40 l6 -6 6 6 -6 8 z" fill="#c1124e" />
<path d="M58 40 l6 -6 6 6 -6 8 z" fill="#c1124e" />
<path d="M34 62 Q50 78 66 62" fill="none" stroke="#7a0b30" strokeWidth="5" strokeLinecap="round" />
</Face>
),
angry: (
<Face bg1="#ff9d7a" bg2="#e23b1e">
<path d="M28 38 l16 6" stroke="#3a0a00" strokeWidth="5" strokeLinecap="round" />
<path d="M72 38 l-16 6" stroke="#3a0a00" strokeWidth="5" strokeLinecap="round" />
<circle cx="37" cy="50" r="4.5" fill="#3a0a00" />
<circle cx="63" cy="50" r="4.5" fill="#3a0a00" />
<path d="M34 72 Q50 60 66 72" fill="none" stroke="#3a0a00" strokeWidth="5" strokeLinecap="round" />
</Face>
),
/* ------------------------------ hokm ----------------------------- */
"hokm-badge": (
<>
<defs>
<linearGradient id="gold" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#f1da8a" />
<stop offset="0.55" stopColor="#d4af37" />
<stop offset="1" stopColor="#b8860b" />
</linearGradient>
</defs>
<circle cx="50" cy="50" r="42" fill="url(#gold)" stroke="#7a5a00" strokeWidth="3" />
<circle cx="50" cy="50" r="34" fill="none" stroke="#7a5a00" strokeWidth="1.5" opacity="0.5" />
<text x="50" y="46" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="26" fill="#3a2a04">
حکم
</text>
<text x="50" y="72" textAnchor="middle" fontSize="20" fill="#3a2a04">
</text>
</>
),
"kot-stamp": (
<g transform="rotate(-14 50 50)">
<circle cx="50" cy="50" r="40" fill="none" stroke="#d11a2a" strokeWidth="5" />
<circle cx="50" cy="50" r="33" fill="none" stroke="#d11a2a" strokeWidth="2" />
<text x="50" y="60" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="34" fill="#d11a2a">
کُت!
</text>
</g>
),
crown: (
<>
<defs>
<linearGradient id="cg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#ffe89a" />
<stop offset="1" stopColor="#d4af37" />
</linearGradient>
</defs>
<path d="M18 70 L24 34 L40 54 L50 26 L60 54 L76 34 L82 70 Z" fill="url(#cg)" stroke="#7a5a00" strokeWidth="2.5" strokeLinejoin="round" />
<rect x="18" y="70" width="64" height="10" rx="3" fill="url(#cg)" stroke="#7a5a00" strokeWidth="2.5" />
<circle cx="50" cy="24" r="5" fill="#ff5d73" />
<circle cx="24" cy="32" r="4" fill="#6aa6ff" />
<circle cx="76" cy="32" r="4" fill="#6aa6ff" />
</>
),
"ace-spade": (
<>
<rect x="24" y="14" width="52" height="72" rx="8" fill="#fffdf7" stroke="#caa84a" strokeWidth="2.5" />
<text x="34" y="32" textAnchor="middle" fontSize="14" fontWeight="800" fill="#1b1b1b">A</text>
<text x="50" y="62" textAnchor="middle" fontSize="34" fill="#1b1b1b"></text>
<text x="66" y="80" textAnchor="middle" fontSize="14" fontWeight="800" fill="#1b1b1b" transform="rotate(180 66 75)">A</text>
</>
),
/* ----------------------------- persian --------------------------- */
chai: (
<>
<path d="M40 24 q4 -8 0 -14 M52 24 q5 -9 0 -16" fill="none" stroke="#cbd5e1" strokeWidth="3" strokeLinecap="round" opacity="0.8" />
<path d="M34 30 H66 L61 78 Q60 84 54 84 H46 Q40 84 39 78 Z" fill="#fff" stroke="#caa84a" strokeWidth="2.5" />
<path d="M37 40 H63 L59 72 Q58 76 53 76 H47 Q42 76 41 72 Z" fill="#a8431a" />
<ellipse cx="50" cy="88" rx="26" ry="6" fill="#e9d9a8" stroke="#caa84a" strokeWidth="2" />
<path d="M66 44 q14 2 12 16 q-2 10 -14 8" fill="none" stroke="#caa84a" strokeWidth="3" />
</>
),
afarin: (
<>
<defs>
<linearGradient id="rib" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#f1da8a" />
<stop offset="1" stopColor="#c9a227" />
</linearGradient>
</defs>
<path d="M14 36 H86 L74 50 L86 64 H14 L26 50 Z" fill="url(#rib)" stroke="#7a5a00" strokeWidth="2" />
<text x="50" y="56" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="20" fill="#3a2a04">
آفرین
</text>
</>
),
rose: (
<>
<path d="M50 60 C40 80 30 84 26 92 M50 60 C60 80 70 84 74 92" stroke="#2f7d32" strokeWidth="4" fill="none" />
<path d="M30 74 q-12 -2 -14 -12 q12 0 16 8 M70 74 q12 -2 14 -12 q-12 0 -16 8" fill="#2f7d32" opacity="0.85" />
<circle cx="50" cy="42" r="22" fill="#d11a2a" />
<path d="M50 24 a18 18 0 0 1 0 36 a12 12 0 0 0 0 -24 a8 8 0 0 1 0 -12" fill="#a30f1f" opacity="0.7" />
<circle cx="50" cy="42" r="7" fill="#7a0b16" />
</>
),
/* ------------------------------ taunt ---------------------------- */
clown: (
<Face bg1="#fff3d6" bg2="#ffd98a">
<circle cx="36" cy="44" r="4.5" fill="#1b1b1b" />
<circle cx="64" cy="44" r="4.5" fill="#1b1b1b" />
<circle cx="50" cy="56" r="7" fill="#e23b1e" />
<path d="M30 66 Q50 84 70 66" fill="none" stroke="#e23b1e" strokeWidth="5" strokeLinecap="round" />
<circle cx="22" cy="40" r="6" fill="#ff5fa2" opacity="0.8" />
<circle cx="78" cy="40" r="6" fill="#ff5fa2" opacity="0.8" />
</Face>
),
sleep: (
<Face bg1="#ffe680" bg2="#f5b301">
<path d="M30 44 q6 -5 12 0 M58 44 q6 -5 12 0" fill="none" stroke="#3a2a00" strokeWidth="4" strokeLinecap="round" />
<circle cx="50" cy="64" r="5" fill="none" stroke="#3a2a00" strokeWidth="4" />
<text x="74" y="34" fontFamily="Vazirmatn, sans-serif" fontSize="16" fontWeight="900" fill="#13314d">z</text>
<text x="82" y="24" fontFamily="Vazirmatn, sans-serif" fontSize="11" fontWeight="900" fill="#13314d">z</text>
</Face>
),
weak: (
<>
<circle cx="50" cy="50" r="42" fill="#1f2b4d" stroke="#d11a2a" strokeWidth="3" />
<path d="M50 30 v26 M50 56 l-9 -9 M50 56 l9 -9" stroke="#ff6b81" strokeWidth="5" fill="none" strokeLinecap="round" />
<text x="50" y="80" textAnchor="middle" fontFamily="Vazirmatn, sans-serif" fontWeight="900" fontSize="16" fill="#ff6b81">
ضعیف!
</text>
</>
),
/* ------------------- custom (achievement-unlocked) ------------------ */
"crown-gold": (
<>
<defs>
<linearGradient id="cg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#ffe488" />
<stop offset="1" stopColor="#d4a017" />
</linearGradient>
</defs>
<circle cx="50" cy="50" r="44" fill="#2a1a4d" stroke="#d4af37" strokeWidth="2" />
<path d="M24 64 L24 40 L36 52 L50 32 L64 52 L76 40 L76 64 Z" fill="url(#cg)" stroke="#7a5a00" strokeWidth="2" strokeLinejoin="round" />
<rect x="24" y="64" width="52" height="8" rx="2" fill="url(#cg)" stroke="#7a5a00" strokeWidth="2" />
<circle cx="50" cy="30" r="4" fill="#ff5fa2" />
<circle cx="24" cy="40" r="3" fill="#6aa6ff" />
<circle cx="76" cy="40" r="3" fill="#6aa6ff" />
</>
),
"seven-zip": (
<>
<circle cx="50" cy="50" r="44" fill="#0d6b5e" stroke="#2dd4bf" strokeWidth="2" />
<text x="50" y="63" textAnchor="middle" fontFamily="Arial, sans-serif" fontWeight="900" fontSize="34" fill="#effdf8">70</text>
</>
),
"streak-fire": (
<>
<defs>
<linearGradient id="sf" x1="0" y1="1" x2="0" y2="0">
<stop offset="0" stopColor="#ff3d00" />
<stop offset="0.6" stopColor="#ff9100" />
<stop offset="1" stopColor="#ffea00" />
</linearGradient>
</defs>
<circle cx="50" cy="50" r="44" fill="#2b0a0a" stroke="#ff6b35" strokeWidth="2" />
<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);
export function Sticker({ id, size = 64, className }: { id: string; size?: number } & SvgProps) {
const art = STICKERS[id];
if (!art) return null;
return (
<svg viewBox="0 0 100 100" width={size} height={size} className={className} role="img">
{art}
</svg>
);
}