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
+27 -4
View File
@@ -2,7 +2,13 @@
// Simulates remote players, friends presence, room invites and matchmaking
// with timers, and computes rewards via gamification.ts.
import { CARD_STYLES, REACTION_PACKS, applyMatchResult, dailyRewardFor } from "./gamification";
import {
CARD_STYLES,
REACTION_PACKS,
STICKER_PACKS,
applyMatchResult,
dailyRewardFor,
} from "./gamification";
import {
CreateRoomOptions,
MatchmakingOptions,
@@ -113,6 +119,7 @@ function defaultProfile(session: AuthSession): UserProfile {
ownedCardStyles: ["classic"],
ownedTitles: ["novice"],
ownedReactionPacks: [],
ownedStickerPacks: [],
title: "novice",
cardStyle: "classic",
achievements: {},
@@ -130,6 +137,7 @@ function migrateProfile(p: UserProfile): UserProfile {
ownedCardStyles: p.ownedCardStyles ?? ["classic"],
ownedTitles: p.ownedTitles ?? ["novice"],
ownedReactionPacks: p.ownedReactionPacks ?? [],
ownedStickerPacks: p.ownedStickerPacks ?? [],
title: p.title ?? "novice",
cardStyle: p.cardStyle ?? "classic",
};
@@ -452,7 +460,10 @@ export class MockOnlineService implements OnlineService {
onReaction(cb: (seat: number, reaction: string) => void): Unsubscribe {
this.reactionCbs.add(cb);
if (this.reactionTimer == null) {
const pool = ["👍", "😂", "🔥", "😮", "👏", "😎", "🙄", "😭"];
const pool = [
"👍", "😂", "🔥", "😮", "👏", "🙄",
"sticker:happy", "sticker:cool", "sticker:kot-stamp", "sticker:crown",
];
this.reactionTimer = setInterval(() => {
if (this.reactionCbs.size === 0) return;
const seat = randInt(1, 3);
@@ -761,7 +772,15 @@ export class MockOnlineService implements OnlineService {
price: r.price,
preview: r.reactions[0],
}));
return [...avatarItems, ...cardItems, ...reactionItems];
const stickerItems: ShopItem[] = STICKER_PACKS.filter((p) => p.price > 0).map((p) => ({
id: p.id,
kind: "stickerpack",
nameFa: p.nameFa,
nameEn: p.nameEn,
price: p.price,
preview: p.stickers[0], // sticker id; ShopScreen renders via <Sticker>
}));
return [...avatarItems, ...cardItems, ...reactionItems, ...stickerItems];
}
async buyItem(id: string) {
@@ -774,7 +793,9 @@ export class MockOnlineService implements OnlineService {
? p.ownedAvatars.includes(id)
: item.kind === "cardstyle"
? p.ownedCardStyles.includes(id)
: p.ownedReactionPacks.includes(id);
: item.kind === "reactionpack"
? p.ownedReactionPacks.includes(id)
: p.ownedStickerPacks.includes(id);
if (owned) return { ok: false, messageFa: "قبلاً خریداری شده", messageEn: "Already owned" };
if (p.coins < item.price)
return { ok: false, messageFa: "سکه کافی نیست", messageEn: "Not enough coins" };
@@ -787,6 +808,8 @@ export class MockOnlineService implements OnlineService {
item.kind === "cardstyle" ? [...p.ownedCardStyles, id] : p.ownedCardStyles,
ownedReactionPacks:
item.kind === "reactionpack" ? [...p.ownedReactionPacks, id] : p.ownedReactionPacks,
ownedStickerPacks:
item.kind === "stickerpack" ? [...p.ownedStickerPacks, id] : p.ownedStickerPacks,
};
this.saveProfile();
return { ok: true, profile: this.profile, messageFa: "خرید انجام شد", messageEn: "Purchased" };