import { deriveLevel, getBadgeDefinition, getCollectionKey, getChallengeDefinition, seedChallengeDefinitions } from '@fan-passport/shared'; import type { AchievementEvent, BadgeAward, ChallengeDefinition, ChallengeProgress, PassportMemory, Prediction, TriviaAnswer, UserCollectionEntry, UserProfile } from '@fan-passport/shared'; import { randomUUID } from 'node:crypto'; export interface GamificationRecord { user: UserProfile; collections: UserCollectionEntry[]; triviaAnswers: TriviaAnswer[]; predictions: Prediction[]; memories: PassportMemory[]; badges: BadgeAward[]; achievements: AchievementEvent[]; challengeCompletions: Record; } function eventId(prefix: string): string { return `${prefix}-${randomUUID()}`; } function collectionCountForChallenge(record: GamificationRecord, challenge: ChallengeDefinition): number { return record.collections.filter((entry) => { const typeMatches = challenge.itemType ? entry.itemType === challenge.itemType : true; const contentMatches = challenge.contentIds ? challenge.contentIds.includes(entry.contentId) : true; return typeMatches && contentMatches; }).length; } function completedContentCountForChallenge(record: GamificationRecord, challenge: ChallengeDefinition): number { if (!challenge.itemType || !challenge.contentIds) { return 0; } const collectedKeys = new Set(record.collections.map((entry) => getCollectionKey(entry.itemType, entry.contentId))); return challenge.contentIds.filter((contentId) => collectedKeys.has(getCollectionKey(challenge.itemType!, contentId))).length; } function starterActionCount(record: GamificationRecord): number { return [ record.collections.length > 0, record.triviaAnswers.length > 0, record.predictions.length > 0, record.memories.length > 0 ].filter(Boolean).length; } export function buildChallengeProgress(record: GamificationRecord): ChallengeProgress[] { return seedChallengeDefinitions.map((challenge) => { let current = 0; switch (challenge.metric) { case 'collect_any': current = collectionCountForChallenge(record, challenge); break; case 'collect_all_content_ids': current = completedContentCountForChallenge(record, challenge); break; case 'answer_trivia_count': current = record.triviaAnswers.length; break; case 'submit_giant_killing_prediction': current = record.predictions.some((prediction) => prediction.giantKilling) ? 1 : 0; break; case 'add_memory_count': current = record.memories.length; break; case 'complete_starter_actions': current = starterActionCount(record); break; } const complete = current >= challenge.target; const completedAt = record.challengeCompletions[challenge.id]; return { challengeId: challenge.id, current: Math.min(current, challenge.target), target: challenge.target, complete, completedAt }; }); } function awardBadge(record: GamificationRecord, badgeId: string, reason: string, now: string): AchievementEvent | undefined { const existing = record.badges.some((badge) => badge.badgeId === badgeId); if (existing) { return undefined; } const badge = getBadgeDefinition(badgeId); if (!badge) { return undefined; } record.badges.push({ badgeId, awardedAt: now, reason }); const event: AchievementEvent = { id: eventId('achievement-badge'), type: 'badge', title: `Badge unlocked: ${badge.name}`, description: badge.description, points: 0, createdAt: now }; record.achievements.unshift(event); return event; } export function applyChallengeRewards(record: GamificationRecord, now: string): AchievementEvent[] { const unlockedEvents: AchievementEvent[] = []; for (const progress of buildChallengeProgress(record)) { if (!progress.complete || record.challengeCompletions[progress.challengeId]) { continue; } const challenge = getChallengeDefinition(progress.challengeId); if (!challenge) { continue; } record.challengeCompletions[challenge.id] = now; record.user.points += challenge.points; const challengeEvent: AchievementEvent = { id: eventId('achievement-challenge'), type: 'challenge', title: `Challenge complete: ${challenge.title}`, description: `${challenge.description} +${challenge.points} XP`, points: challenge.points, createdAt: now }; record.achievements.unshift(challengeEvent); unlockedEvents.push(challengeEvent); if (challenge.badgeId) { const badgeEvent = awardBadge(record, challenge.badgeId, `Completed challenge: ${challenge.title}`, now); if (badgeEvent) { unlockedEvents.push(badgeEvent); } } } record.user.level = deriveLevel(record.user.points); return unlockedEvents; }