import 'dart:math'; import '../models/focus_session.dart'; import '../models/user_progress.dart'; /// Service for calculating and managing points class PointsService { /// Calculate points earned from a focus session /// Returns a map with breakdown: {basePoints, honestyBonus, total, breakdown} /// Note: breakdown contains labelKey and descriptionKey for localization Map calculateSessionPoints(FocusSession session) { // Base points = actual minutes focused int basePoints = session.actualMinutes; // Honesty bonus: reward for recording distractions (with cap to prevent abuse) int honestyBonus = _calculateHonestyBonus( session.distractionCount, session.actualMinutes, ); int total = basePoints + honestyBonus; // Detailed breakdown for UI display (using localization keys) List> breakdown = [ { 'labelKey': 'focusTimePoints', 'value': basePoints, 'descriptionKey': 'focusTimePointsDesc', }, { 'labelKey': 'honestyBonusLabel', 'value': honestyBonus, 'descriptionKey': 'honestyBonusDesc', }, ]; return { 'basePoints': basePoints, 'honestyBonus': honestyBonus, 'total': total, 'breakdown': breakdown, }; } /// Calculate honesty bonus with anti-abuse cap /// Strategy: Max 1 rewarded distraction per 10 minutes int _calculateHonestyBonus(int distractionCount, int minutes) { if (distractionCount == 0) return 0; // Cap: 1 rewarded distraction per 10 minutes // 15 min → max 2 distractions // 25 min → max 3 distractions // 45 min → max 5 distractions int maxBonusDistraction = max(1, (minutes / 10).ceil()); int rewardedCount = min(distractionCount, maxBonusDistraction); return rewardedCount; // 1 point per recorded distraction (up to cap) } /// Process daily check-in and return points earned with detailed breakdown /// Note: breakdown contains labelKey and descriptionKey for localization Map processCheckIn(UserProgress progress) { final now = DateTime.now(); // Base check-in points int points = 5; List> breakdown = [ { 'labelKey': 'checkInPoints', 'value': 5, 'descriptionKey': 'checkInPointsDesc', }, ]; // Update check-in streak if (_isConsecutiveDay(progress.lastCheckInDate, now)) { progress.consecutiveCheckIns++; // Bonus for streak milestones if (progress.consecutiveCheckIns % 7 == 0) { int weeklyBonus = 30; points += weeklyBonus; breakdown.add({ 'labelKey': 'streakBonus', 'value': weeklyBonus, 'descriptionKey': 'streakBonusDesc', 'descriptionParams': {'days': progress.consecutiveCheckIns}, }); } else if (progress.consecutiveCheckIns % 30 == 0) { int monthlyBonus = 100; points += monthlyBonus; breakdown.add({ 'labelKey': 'streakBonus', 'value': monthlyBonus, 'descriptionKey': 'streakBonusDesc', 'descriptionParams': {'days': progress.consecutiveCheckIns}, }); } } else { progress.consecutiveCheckIns = 1; } // Update last check-in date progress.lastCheckInDate = now; // Add to check-in history (store date only, not time) final dateOnly = DateTime(now.year, now.month, now.day); if (!progress.checkInHistory.any((date) => date.year == dateOnly.year && date.month == dateOnly.month && date.day == dateOnly.day)) { progress.checkInHistory.add(dateOnly); } return { 'points': points, 'consecutiveDays': progress.consecutiveCheckIns, 'breakdown': breakdown, }; } /// Check if two dates are consecutive days bool _isConsecutiveDay(DateTime? lastDate, DateTime currentDate) { if (lastDate == null) return false; final lastDateOnly = DateTime(lastDate.year, lastDate.month, lastDate.day); final currentDateOnly = DateTime(currentDate.year, currentDate.month, currentDate.day); final diff = currentDateOnly.difference(lastDateOnly).inDays; return diff == 1; } /// Calculate level based on total points Map calculateLevel(int totalPoints) { // Simple level calculation: each level requires 100 points int level = (totalPoints / 100).floor() + 1; int pointsForCurrentLevel = (level - 1) * 100; int pointsForNextLevel = level * 100; int pointsInCurrentLevel = totalPoints - pointsForCurrentLevel; double progress = pointsInCurrentLevel / 100; return { 'level': level, 'pointsForCurrentLevel': pointsForCurrentLevel, 'pointsForNextLevel': pointsForNextLevel, 'pointsInCurrentLevel': pointsInCurrentLevel, 'progress': progress, }; } /// Get points balance summary Map getPointsSummary(UserProgress progress) { final levelInfo = calculateLevel(progress.totalPoints); return { 'currentPoints': progress.currentPoints, 'totalPoints': progress.totalPoints, 'level': levelInfo['level'], 'levelProgress': levelInfo['progress'], 'consecutiveCheckIns': progress.consecutiveCheckIns, 'totalCheckIns': progress.checkInHistory.length, }; } }