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:
@@ -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 {
|
||||
|
||||
@@ -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" };
|
||||
|
||||
+13
-1
@@ -55,6 +55,7 @@ export interface UserProfile {
|
||||
ownedCardStyles: string[];
|
||||
ownedTitles: string[];
|
||||
ownedReactionPacks: string[]; // purchased reaction packs
|
||||
ownedStickerPacks: string[]; // purchased sticker packs
|
||||
title: string | null; // equipped title id
|
||||
cardStyle: string; // equipped card-back style id
|
||||
|
||||
@@ -146,6 +147,17 @@ export interface ReactionPackDef {
|
||||
unlockWins?: number; // earned by total wins
|
||||
}
|
||||
|
||||
export interface StickerPackDef {
|
||||
id: string;
|
||||
nameFa: string;
|
||||
nameEn: string;
|
||||
stickers: string[]; // Sticker artwork ids (see components/online/Sticker.tsx)
|
||||
price: number; // >0 → purchasable in the shop
|
||||
default?: boolean;
|
||||
unlockRating?: number;
|
||||
unlockWins?: number;
|
||||
}
|
||||
|
||||
/* ------------------------------ Friends ------------------------------ */
|
||||
|
||||
export type PresenceStatus = "online" | "offline" | "in-game";
|
||||
@@ -281,7 +293,7 @@ export interface LeaderboardEntry {
|
||||
|
||||
/* ------------------------------- Shop -------------------------------- */
|
||||
|
||||
export type ShopItemKind = "avatar" | "cardstyle" | "reactionpack";
|
||||
export type ShopItemKind = "avatar" | "cardstyle" | "reactionpack" | "stickerpack";
|
||||
|
||||
export interface ShopItem {
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user