Celebration animations for purchases, XP gains & achievement unlocks
CI/CD / CI - API (dotnet build + engine sim) (push) Successful in 28s
CI/CD / CI - Web (tsc + next build) (push) Successful in 1m1s
CI/CD / Deploy - local stack (db + server + web) (push) Failing after 0s

- 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:
soroush.asadi
2026-06-05 09:52:28 +03:30
parent be8c758425
commit b661385a00
9 changed files with 306 additions and 3 deletions
+34
View File
@@ -298,6 +298,40 @@ export function achievementById(id: string): AchievementDef | undefined {
return ACHIEVEMENTS.find((a) => a.id === id);
}
/**
* Re-evaluate all achievements against the profile's current state (used outside
* matches, e.g. after an XP-pack purchase crosses a level milestone). Unlocks new
* ones, grants their coin rewards, and returns the newly-unlocked list.
*/
export function evaluateAchievements(profile: UserProfile): {
profile: UserProfile;
newAchievements: AchievementUnlock[];
} {
const achievements = { ...profile.achievements };
const unlocked = [...profile.unlocked];
const newAchievements: AchievementUnlock[] = [];
let coins = 0;
for (const def of ACHIEVEMENTS) {
const prog = achievementProgress(def, profile.stats, profile.rating, profile.level);
achievements[def.id] = prog;
if (prog >= def.goal && !unlocked.includes(def.id)) {
unlocked.push(def.id);
coins += def.coinReward;
newAchievements.push({
id: def.id,
nameFa: def.nameFa,
nameEn: def.nameEn,
icon: def.icon,
coinReward: def.coinReward,
});
}
}
return {
profile: { ...profile, achievements, unlocked, coins: profile.coins + coins },
newAchievements,
};
}
/** The sticker pack (if any) that unlocking this achievement grants. */
export function stickerPackForAchievement(achId: string): StickerPackDef | undefined {
return STICKER_PACKS.find((p) => p.unlockAchievement === achId);