Celebration animations for purchases, XP gains & achievement unlocks
- New global celebration system: celebration-store (queue) + CelebrationOverlay (animated: count-up XP, filling bar, level-up pop, achievement cards; plays levelUp/award sounds; tap or auto-dismiss). Rendered in page.tsx. - Shop: every purchase now celebrates — XP packs animate XP gain + level-up, cosmetics show a "purchased!" pop. Newly-unlocked achievements (diffed from the profile before/after) animate too. - XP purchases now actually evaluate achievements: gamification.evaluateAchievements (client) + Gamification.EvaluateAchievements (server, called in ShopBuy xp path) unlock level milestones + grant their coins. Verified live: buying XP took L1→L5, unlocked level_5 server-side and credited its reward. tsc + dotnet + next build clean; images rebuilt :1500/:1505. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,9 @@ import { useSessionStore } from "@/lib/session-store";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { getService } from "@/lib/online/service";
|
||||
import { sound } from "@/lib/sound";
|
||||
import { ShopItem } from "@/lib/online/types";
|
||||
import { achievementById } from "@/lib/online/gamification";
|
||||
import { celebrate } from "@/lib/celebration-store";
|
||||
import { AchievementUnlock, ShopItem } from "@/lib/online/types";
|
||||
import { cn } from "@/lib/cn";
|
||||
|
||||
/** The product artwork, used on the card and (bigger) in the detail sheet. */
|
||||
@@ -75,11 +77,40 @@ export function ShopScreen() {
|
||||
};
|
||||
|
||||
const buy = async (item: ShopItem) => {
|
||||
const before = profile;
|
||||
const res = await getService().buyItem(item.id);
|
||||
if (res.ok && res.profile) {
|
||||
setProfile(res.profile);
|
||||
const after = res.profile;
|
||||
setProfile(after);
|
||||
sound.play("purchase");
|
||||
setDetail(null);
|
||||
|
||||
// newly-unlocked achievements (e.g. an XP pack crossing a level milestone)
|
||||
const newAch: AchievementUnlock[] = after.unlocked
|
||||
.filter((id) => !before.unlocked.includes(id))
|
||||
.map((id) => achievementById(id))
|
||||
.filter((d): d is NonNullable<typeof d> => !!d)
|
||||
.map((d) => ({ id: d.id, nameFa: d.nameFa, nameEn: d.nameEn, icon: d.icon, coinReward: d.coinReward }));
|
||||
|
||||
if (item.kind === "xp") {
|
||||
celebrate({
|
||||
variant: "xp",
|
||||
icon: "⚡",
|
||||
title: locale === "fa" ? "امتیاز تجربه" : "Experience",
|
||||
xpGained: item.xp ?? 0,
|
||||
levelBefore: before.level,
|
||||
levelAfter: after.level,
|
||||
achievements: newAch.length ? newAch : undefined,
|
||||
});
|
||||
} else {
|
||||
celebrate({
|
||||
variant: "purchase",
|
||||
// avatar/reaction previews are emojis; others fall back to the default glyph
|
||||
icon: item.kind === "avatar" || item.kind === "reactionpack" ? item.preview : undefined,
|
||||
title: locale === "fa" ? item.nameFa : item.nameEn,
|
||||
achievements: newAch.length ? newAch : undefined,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setMsg(locale === "fa" ? res.messageFa : res.messageEn);
|
||||
setTimeout(() => setMsg(""), 1800);
|
||||
|
||||
Reference in New Issue
Block a user