Files
FocusBuddy/lib/services/points_service.dart
ytc1012 86a368e1e3 优化
2025-12-02 16:38:58 +08:00

165 lines
5.3 KiB
Dart

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<String, dynamic> 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<Map<String, dynamic>> 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<String, dynamic> processCheckIn(UserProgress progress) {
final now = DateTime.now();
// Base check-in points
int points = 5;
List<Map<String, dynamic>> 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<String, dynamic> 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<String, dynamic> 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,
};
}
}