Grapevine_Disease_Detection/VinEye/src/hooks/useGameProgress.ts
Yanis a964cc3836 add VinEye frontend app + fix hardcoded paths + gitignore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 10:30:05 +02:00

152 lines
4.1 KiB
TypeScript

import { useState, useEffect, useCallback } from 'react';
import { storage } from '@/services/storage';
import type { GameProgress, BadgeId } from '@/types/gamification';
import type { Detection } from '@/types/detection';
import {
createInitialBadges,
checkNewBadges,
getLevelNumber,
XP_REWARDS,
} from '@/utils/achievements';
const INITIAL_PROGRESS: GameProgress = {
xp: 0,
level: 1,
badges: createInitialBadges(),
streak: 0,
lastScanDate: null,
totalScans: 0,
uniqueGrapes: [],
bestStreak: 0,
highConfidenceScans: 0,
};
function isSameDay(a: Date, b: Date): boolean {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function isYesterday(date: Date, reference: Date): boolean {
const yesterday = new Date(reference);
yesterday.setDate(yesterday.getDate() - 1);
return isSameDay(date, yesterday);
}
export function useGameProgress() {
const [progress, setProgress] = useState<GameProgress>(INITIAL_PROGRESS);
const [newlyUnlockedBadges, setNewlyUnlockedBadges] = useState<BadgeId[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
loadProgress();
}, []);
async function loadProgress() {
setIsLoading(true);
const saved = await storage.get<GameProgress>(storage.KEYS.GAME_PROGRESS);
setProgress(saved ?? INITIAL_PROGRESS);
setIsLoading(false);
}
const processDetection = useCallback(async (detection: Detection): Promise<number> => {
let xpEarned = 0;
setProgress((prev) => {
const now = new Date();
const lastDate = prev.lastScanDate ? new Date(prev.lastScanDate) : null;
// Update streak
let newStreak = prev.streak;
if (detection.result === 'vine') {
if (!lastDate || isYesterday(lastDate, now)) {
newStreak = prev.streak + 1;
} else if (!lastDate || !isSameDay(lastDate, now)) {
newStreak = 1;
}
// Same day — no streak change
}
// Calculate XP
if (detection.result === 'vine') {
xpEarned += XP_REWARDS.SCAN_SUCCESS;
if (detection.cepageId && !prev.uniqueGrapes.includes(detection.cepageId)) {
xpEarned += XP_REWARDS.NEW_CEEPAGE;
}
if (newStreak > prev.streak) {
xpEarned += XP_REWARDS.DAILY_STREAK_BONUS;
}
if (detection.confidence > 90) {
xpEarned += XP_REWARDS.HIGH_CONFIDENCE_BONUS;
}
}
const newXP = prev.xp + xpEarned;
const newLevel = getLevelNumber(newXP);
const updatedUniqueGrapes =
detection.cepageId && !prev.uniqueGrapes.includes(detection.cepageId)
? [...prev.uniqueGrapes, detection.cepageId]
: prev.uniqueGrapes;
const updatedTotalScans = prev.totalScans + 1;
const updatedHighConf =
detection.result === 'vine' && detection.confidence > 95
? prev.highConfidenceScans + 1
: prev.highConfidenceScans;
const nextProgress: GameProgress = {
...prev,
xp: newXP,
level: newLevel,
streak: newStreak,
bestStreak: Math.max(prev.bestStreak, newStreak),
lastScanDate: new Date().toISOString(),
totalScans: updatedTotalScans,
uniqueGrapes: updatedUniqueGrapes,
highConfidenceScans: updatedHighConf,
badges: prev.badges,
};
// Check badges
const { badges, newlyUnlocked } = checkNewBadges(nextProgress, prev.badges);
nextProgress.badges = badges;
if (newlyUnlocked.length > 0) {
setNewlyUnlockedBadges(newlyUnlocked);
}
// Persist
storage.set(storage.KEYS.GAME_PROGRESS, nextProgress);
return nextProgress;
});
return xpEarned;
}, []);
const clearNewlyUnlocked = useCallback(() => {
setNewlyUnlockedBadges([]);
}, []);
const resetProgress = useCallback(async () => {
await storage.remove(storage.KEYS.GAME_PROGRESS);
setProgress(INITIAL_PROGRESS);
}, []);
return {
progress,
isLoading,
processDetection,
newlyUnlockedBadges,
clearNewlyUnlocked,
resetProgress,
reload: loadProgress,
};
}