Add designed sticker packs (SVG art) to the reactions system

- 15 hand-designed inline-SVG stickers (faces, Hokm: حکم/کُت/crown/ace,
  Persian: chai/آفرین/rose, taunts: clown/zzz/ضعیف) in components/online/Sticker.tsx
- Sticker packs: faces (free), hokm (earned @rating 1300), persian & taunt (buy)
- In-game tray now tabbed Emoji | Stickers; stickers broadcast as "sticker:<id>"
  and render as large animated bubbles per seat
- Shop sells sticker packs; profile.ownedStickerPacks; gamification helpers
  ownedStickers/ownedStickerPackIds; mock opponents send stickers too

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 11:15:28 +03:30
parent f9425dea01
commit db4eade619
8 changed files with 359 additions and 20 deletions
+32
View File
@@ -12,6 +12,7 @@ import {
RankTierId,
ReactionPackDef,
RewardResult,
StickerPackDef,
TitleDef,
TitleUnlock,
UserProfile,
@@ -262,6 +263,37 @@ export function ownedReactions(profile: UserProfile): string[] {
return REACTION_PACKS.filter((p) => ids.has(p.id)).flatMap((p) => p.reactions);
}
/* ------------------------- Sticker packs ----------------------------- */
export const STICKER_PACKS: StickerPackDef[] = [
{ id: "faces", nameFa: "شکلک‌ها", nameEn: "Faces", stickers: ["happy", "sad", "cool", "love", "angry"], price: 0, default: true },
{ id: "hokm", nameFa: "حکم", nameEn: "Hokm", stickers: ["hokm-badge", "kot-stamp", "crown", "ace-spade"], price: 0, unlockRating: 1300 },
{ id: "persian", nameFa: "ایرانی", nameEn: "Persian", stickers: ["chai", "afarin", "rose"], price: 700 },
{ id: "taunt", nameFa: "طعنه", nameEn: "Taunts", stickers: ["clown", "sleep", "weak"], price: 900 },
];
export function stickerPackById(id: string): StickerPackDef | undefined {
return STICKER_PACKS.find((p) => p.id === id);
}
export function ownedStickerPackIds(profile: UserProfile): string[] {
const purchased = profile.ownedStickerPacks ?? [];
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);
if (p.default || earned || purchased.includes(p.id)) ids.add(p.id);
}
return [...ids];
}
/** Flattened sticker-id list the player can send. */
export function ownedStickers(profile: UserProfile): string[] {
const ids = new Set(ownedStickerPackIds(profile));
return STICKER_PACKS.filter((p) => ids.has(p.id)).flatMap((p) => p.stickers);
}
/* ---------------------- Apply a match result ------------------------- */
function applyStats(stats: PlayerStats, summary: MatchSummary): PlayerStats {