积分、成就系统

This commit is contained in:
ytc1012
2025-11-27 13:37:10 +08:00
parent 0195cdf54b
commit 58f6ec39b7
35 changed files with 7786 additions and 199 deletions

View File

@@ -0,0 +1,111 @@
import '../models/user_progress.dart';
import '../models/achievement_config.dart';
/// Service for managing achievements
class AchievementService {
/// Check for newly unlocked achievements asynchronously
/// Returns list of newly unlocked achievement IDs
Future<List<String>> checkAchievementsAsync(UserProgress progress) async {
List<String> newlyUnlocked = [];
for (var achievement in AchievementConfig.all) {
// Skip if already unlocked
if (progress.unlockedAchievements.containsKey(achievement.id)) {
continue;
}
// Check if requirement is met
bool unlocked = false;
switch (achievement.type) {
case AchievementType.sessionCount:
unlocked = progress.totalSessions >= achievement.requiredValue;
break;
case AchievementType.distractionCount:
unlocked = progress.totalDistractions >= achievement.requiredValue;
break;
case AchievementType.totalMinutes:
unlocked = progress.totalFocusMinutes >= achievement.requiredValue;
break;
case AchievementType.consecutiveDays:
unlocked = progress.consecutiveCheckIns >= achievement.requiredValue;
break;
}
if (unlocked) {
// Mark as unlocked with timestamp
progress.unlockedAchievements[achievement.id] = DateTime.now();
// Award bonus points
progress.totalPoints += achievement.bonusPoints;
progress.currentPoints += achievement.bonusPoints;
newlyUnlocked.add(achievement.id);
}
}
return newlyUnlocked;
}
/// Get progress towards a specific achievement (0.0 - 1.0)
double getAchievementProgress(
UserProgress progress,
AchievementConfig achievement,
) {
int currentValue = 0;
switch (achievement.type) {
case AchievementType.sessionCount:
currentValue = progress.totalSessions;
break;
case AchievementType.distractionCount:
currentValue = progress.totalDistractions;
break;
case AchievementType.totalMinutes:
currentValue = progress.totalFocusMinutes;
break;
case AchievementType.consecutiveDays:
currentValue = progress.consecutiveCheckIns;
break;
}
return (currentValue / achievement.requiredValue).clamp(0.0, 1.0);
}
/// Get current value for achievement type
int getAchievementCurrentValue(
UserProgress progress,
AchievementConfig achievement,
) {
switch (achievement.type) {
case AchievementType.sessionCount:
return progress.totalSessions;
case AchievementType.distractionCount:
return progress.totalDistractions;
case AchievementType.totalMinutes:
return progress.totalFocusMinutes;
case AchievementType.consecutiveDays:
return progress.consecutiveCheckIns;
}
}
/// Get all achievements with their current progress
Map<AchievementConfig, double> getAllAchievementsWithProgress(
UserProgress progress,
) {
final result = <AchievementConfig, double>{};
for (var achievement in AchievementConfig.all) {
result[achievement] = getAchievementProgress(progress, achievement);
}
return result;
}
/// Get newly unlocked achievements since last check
/// This can be used to show notifications for achievements unlocked in background
List<String> getNewlyUnlockedAchievements(
UserProgress progress,
Set<String> previouslyUnlocked,
) {
final currentlyUnlocked = progress.unlockedAchievements.keys.toSet();
return currentlyUnlocked.difference(previouslyUnlocked).toList();
}
}

View File

@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import 'storage_service.dart';
import 'notification_service.dart';
import 'encouragement_service.dart';
import 'points_service.dart';
import 'achievement_service.dart';
/// GetIt instance for dependency injection
final getIt = GetIt.instance;
@@ -31,6 +33,10 @@ Future<void> initializeDI() async {
return service;
});
// Register synchronous services
getIt.registerSingleton<PointsService>(PointsService());
getIt.registerSingleton<AchievementService>(AchievementService());
// Wait for all services to be initialized
await getIt.allReady();

View 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,
};
}
}

View File

@@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'storage_service.dart';
import 'notification_service.dart';
import 'encouragement_service.dart';
import 'points_service.dart';
import 'achievement_service.dart';
/// Service Locator - 统一管理所有服务实例
class ServiceLocator {
@@ -13,6 +15,8 @@ class ServiceLocator {
late StorageService _storageService;
late NotificationService _notificationService;
late EncouragementService _encouragementService;
late PointsService _pointsService;
late AchievementService _achievementService;
bool _isInitialized = false;
/// 初始化所有服务
@@ -33,6 +37,12 @@ class ServiceLocator {
_encouragementService = EncouragementService();
await _encouragementService.loadMessages();
// 初始化积分服务
_pointsService = PointsService();
// 初始化成就服务
_achievementService = AchievementService();
_isInitialized = true;
if (kDebugMode) {
print('ServiceLocator initialized successfully');
@@ -63,6 +73,18 @@ class ServiceLocator {
return _encouragementService;
}
/// 获取积分服务实例
PointsService get pointsService {
_checkInitialized();
return _pointsService;
}
/// 获取成就服务实例
AchievementService get achievementService {
_checkInitialized();
return _achievementService;
}
/// 检查服务是否已初始化
void _checkInitialized() {
if (!_isInitialized) {

View File

@@ -1,17 +1,23 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:flutter/foundation.dart';
import '../models/focus_session.dart';
import '../models/user_progress.dart';
/// Service to manage local storage using Hive
class StorageService {
static const String _focusSessionBox = 'focus_sessions';
static const String _userProgressBox = 'user_progress';
static const String _progressKey = 'user_progress_key';
// Cache for today's sessions to improve performance
List<FocusSession>? _todaySessionsCache;
DateTime? _cacheDate;
// Cache for user progress
UserProgress? _userProgressCache;
/// Initialize Hive storage service
///
///
/// This method initializes Hive, registers adapters, and opens the focus sessions box.
/// It should be called once during app initialization.
Future<void> init() async {
@@ -20,10 +26,12 @@ class StorageService {
// Register adapters
Hive.registerAdapter(FocusSessionAdapter());
Hive.registerAdapter(UserProgressAdapter());
// Open boxes
await Hive.openBox<FocusSession>(_focusSessionBox);
await Hive.openBox<UserProgress>(_userProgressBox);
if (kDebugMode) {
print('StorageService initialized successfully');
}
@@ -37,13 +45,92 @@ class StorageService {
/// Get the focus sessions box
Box<FocusSession> get _sessionsBox => Hive.box<FocusSession>(_focusSessionBox);
/// Get the user progress box
Box<UserProgress> get _progressBox => Hive.box<UserProgress>(_userProgressBox);
/// Invalidate the cache when data changes
void _invalidateCache() {
_todaySessionsCache = null;
_cacheDate = null;
}
/// Invalidate user progress cache
void _invalidateProgressCache() {
_userProgressCache = null;
}
// ==================== User Progress Methods ====================
/// Get user progress (creates new one if doesn't exist)
UserProgress getUserProgress() {
try {
// Return cached progress if available
if (_userProgressCache != null) {
return _userProgressCache!;
}
// Try to get from box
var progress = _progressBox.get(_progressKey);
// Create new progress if doesn't exist
if (progress == null) {
progress = UserProgress();
_progressBox.put(_progressKey, progress);
}
// Cache and return
_userProgressCache = progress;
return progress;
} catch (e) {
if (kDebugMode) {
print('Failed to get user progress: $e');
}
// Return new progress as fallback
return UserProgress();
}
}
/// Save user progress
Future<void> saveUserProgress(UserProgress progress) async {
try {
await _progressBox.put(_progressKey, progress);
_userProgressCache = progress; // Update cache
} catch (e) {
if (kDebugMode) {
print('Failed to save user progress: $e');
}
rethrow;
}
}
/// Update user progress with a function
Future<void> updateUserProgress(Function(UserProgress) updateFn) async {
try {
final progress = getUserProgress();
updateFn(progress);
await saveUserProgress(progress);
} catch (e) {
if (kDebugMode) {
print('Failed to update user progress: $e');
}
rethrow;
}
}
/// Clear user progress (for testing/reset)
Future<void> clearUserProgress() async {
try {
await _progressBox.delete(_progressKey);
_invalidateProgressCache();
} catch (e) {
if (kDebugMode) {
print('Failed to clear user progress: $e');
}
rethrow;
}
}
/// Save a focus session to local storage
///
/// [session] - The focus session to save