积分、成就系统
This commit is contained in:
160
lib/services/points_service.dart
Normal file
160
lib/services/points_service.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
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}
|
||||
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
|
||||
List<Map<String, dynamic>> breakdown = [
|
||||
{
|
||||
'label': '专注时长',
|
||||
'value': basePoints,
|
||||
'description': '每专注1分钟获得1积分',
|
||||
},
|
||||
{
|
||||
'label': '诚实奖励',
|
||||
'value': honestyBonus,
|
||||
'description': '记录分心情况获得额外积分',
|
||||
},
|
||||
];
|
||||
|
||||
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
|
||||
Map<String, dynamic> processCheckIn(UserProgress progress) {
|
||||
final now = DateTime.now();
|
||||
|
||||
// Base check-in points
|
||||
int points = 5;
|
||||
List<Map<String, dynamic>> breakdown = [
|
||||
{
|
||||
'label': '每日签到',
|
||||
'value': 5,
|
||||
'description': '每日首次签到获得基础积分',
|
||||
},
|
||||
];
|
||||
|
||||
// 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({
|
||||
'label': '连续签到奖励',
|
||||
'value': weeklyBonus,
|
||||
'description': '连续签到${progress.consecutiveCheckIns}天',
|
||||
});
|
||||
} else if (progress.consecutiveCheckIns % 30 == 0) {
|
||||
int monthlyBonus = 100;
|
||||
points += monthlyBonus;
|
||||
breakdown.add({
|
||||
'label': '连续签到奖励',
|
||||
'value': monthlyBonus,
|
||||
'description': '连续签到${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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user