积分、成就系统

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,190 @@
/// Achievement types for tracking progress
enum AchievementType {
sessionCount, // Total number of completed sessions
distractionCount, // Total number of recorded distractions
totalMinutes, // Total minutes of focus time
consecutiveDays, // Consecutive check-in days
}
/// Configuration for a single achievement
class AchievementConfig {
final String id;
final String nameKey; // Localization key for name
final String descKey; // Localization key for description
final String icon; // Emoji icon
final AchievementType type;
final int requiredValue;
final int bonusPoints; // Points awarded when unlocked
const AchievementConfig({
required this.id,
required this.nameKey,
required this.descKey,
required this.icon,
required this.type,
required this.requiredValue,
required this.bonusPoints,
});
/// All available achievements in the app
static List<AchievementConfig> get all => [
// First session
const AchievementConfig(
id: 'first_session',
nameKey: 'achievement_first_session_name',
descKey: 'achievement_first_session_desc',
icon: '🎖️',
type: AchievementType.sessionCount,
requiredValue: 1,
bonusPoints: 10,
),
// Session milestones
const AchievementConfig(
id: 'sessions_10',
nameKey: 'achievement_sessions_10_name',
descKey: 'achievement_sessions_10_desc',
icon: '',
type: AchievementType.sessionCount,
requiredValue: 10,
bonusPoints: 50,
),
const AchievementConfig(
id: 'sessions_50',
nameKey: 'achievement_sessions_50_name',
descKey: 'achievement_sessions_50_desc',
icon: '🌟',
type: AchievementType.sessionCount,
requiredValue: 50,
bonusPoints: 200,
),
const AchievementConfig(
id: 'sessions_100',
nameKey: 'achievement_sessions_100_name',
descKey: 'achievement_sessions_100_desc',
icon: '💫',
type: AchievementType.sessionCount,
requiredValue: 100,
bonusPoints: 500,
),
// Honesty tracking series (KEY INNOVATION)
const AchievementConfig(
id: 'honest_bronze',
nameKey: 'achievement_honest_bronze_name',
descKey: 'achievement_honest_bronze_desc',
icon: '🧠',
type: AchievementType.distractionCount,
requiredValue: 50,
bonusPoints: 50,
),
const AchievementConfig(
id: 'honest_silver',
nameKey: 'achievement_honest_silver_name',
descKey: 'achievement_honest_silver_desc',
icon: '🧠',
type: AchievementType.distractionCount,
requiredValue: 200,
bonusPoints: 100,
),
const AchievementConfig(
id: 'honest_gold',
nameKey: 'achievement_honest_gold_name',
descKey: 'achievement_honest_gold_desc',
icon: '🧠',
type: AchievementType.distractionCount,
requiredValue: 500,
bonusPoints: 300,
),
// Focus time milestones
const AchievementConfig(
id: 'focus_5h',
nameKey: 'achievement_focus_5h_name',
descKey: 'achievement_focus_5h_desc',
icon: '⏱️',
type: AchievementType.totalMinutes,
requiredValue: 300, // 5 hours
bonusPoints: 100,
),
const AchievementConfig(
id: 'focus_25h',
nameKey: 'achievement_focus_25h_name',
descKey: 'achievement_focus_25h_desc',
icon: '',
type: AchievementType.totalMinutes,
requiredValue: 1500, // 25 hours
bonusPoints: 300,
),
const AchievementConfig(
id: 'focus_100h',
nameKey: 'achievement_focus_100h_name',
descKey: 'achievement_focus_100h_desc',
icon: '👑',
type: AchievementType.totalMinutes,
requiredValue: 6000, // 100 hours
bonusPoints: 1000,
),
// Check-in streaks
const AchievementConfig(
id: 'streak_3',
nameKey: 'achievement_streak_3_name',
descKey: 'achievement_streak_3_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 3,
bonusPoints: 20,
),
const AchievementConfig(
id: 'streak_7',
nameKey: 'achievement_streak_7_name',
descKey: 'achievement_streak_7_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 7,
bonusPoints: 50,
),
const AchievementConfig(
id: 'streak_30',
nameKey: 'achievement_streak_30_name',
descKey: 'achievement_streak_30_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 30,
bonusPoints: 200,
),
const AchievementConfig(
id: 'streak_100',
nameKey: 'achievement_streak_100_name',
descKey: 'achievement_streak_100_desc',
icon: '🔥',
type: AchievementType.consecutiveDays,
requiredValue: 100,
bonusPoints: 1000,
),
];
/// Get achievement by ID
static AchievementConfig? getById(String id) {
try {
return all.firstWhere((achievement) => achievement.id == id);
} catch (e) {
return null;
}
}
/// Get all achievements of a specific type
static List<AchievementConfig> getByType(AchievementType type) {
return all.where((achievement) => achievement.type == type).toList();
}
}

View File

@@ -0,0 +1,144 @@
import 'package:hive/hive.dart';
part 'user_progress.g.dart';
@HiveType(typeId: 1)
class UserProgress extends HiveObject {
@HiveField(0)
int totalPoints;
@HiveField(1)
int currentPoints;
@HiveField(2)
DateTime? lastCheckInDate;
@HiveField(3)
int consecutiveCheckIns;
@HiveField(4)
Map<String, DateTime> unlockedAchievements;
@HiveField(5)
int totalFocusMinutes;
@HiveField(6)
int totalDistractions;
@HiveField(7)
int totalSessions;
@HiveField(8)
List<DateTime> checkInHistory;
UserProgress({
this.totalPoints = 0,
this.currentPoints = 0,
this.lastCheckInDate,
this.consecutiveCheckIns = 0,
Map<String, DateTime>? unlockedAchievements,
this.totalFocusMinutes = 0,
this.totalDistractions = 0,
this.totalSessions = 0,
List<DateTime>? checkInHistory,
}) : unlockedAchievements = unlockedAchievements ?? {},
checkInHistory = checkInHistory ?? [];
/// Get current level based on total points
int get level {
return LevelSystem.getLevel(totalPoints);
}
/// Get progress to next level (0.0 - 1.0)
double get levelProgress {
return LevelSystem.getLevelProgress(totalPoints);
}
/// Get points needed to reach next level
int get pointsToNextLevel {
return LevelSystem.getPointsToNextLevel(totalPoints);
}
/// Check if checked in today
bool get hasCheckedInToday {
if (lastCheckInDate == null) return false;
final now = DateTime.now();
return lastCheckInDate!.year == now.year &&
lastCheckInDate!.month == now.month &&
lastCheckInDate!.day == now.day;
}
/// Get longest check-in streak from history
int get longestCheckInStreak {
if (checkInHistory.isEmpty) return 0;
int maxStreak = 1;
int currentStreak = 1;
// Sort dates
final sortedDates = List<DateTime>.from(checkInHistory)
..sort((a, b) => a.compareTo(b));
for (int i = 1; i < sortedDates.length; i++) {
final diff = sortedDates[i].difference(sortedDates[i - 1]).inDays;
if (diff == 1) {
currentStreak++;
maxStreak = currentStreak > maxStreak ? currentStreak : maxStreak;
} else {
currentStreak = 1;
}
}
return maxStreak;
}
}
/// Level system configuration
class LevelSystem {
static const List<int> levelThresholds = [
0, // Level 0 → 1: 0 points
50, // Level 1 → 2: 50 points
150, // Level 2 → 3: 150 points
300, // Level 3 → 4: 300 points
500, // Level 4 → 5: 500 points
800, // Level 5 → 6: 800 points
1200, // Level 6 → 7: 1200 points
1800, // Level 7 → 8: 1800 points
2500, // Level 8 → 9: 2500 points
3500, // Level 9 → 10: 3500 points
];
static int getLevel(int points) {
for (int i = levelThresholds.length - 1; i >= 0; i--) {
if (points >= levelThresholds[i]) {
return i;
}
}
return 0;
}
static double getLevelProgress(int points) {
int currentLevel = getLevel(points);
if (currentLevel >= levelThresholds.length - 1) return 1.0;
int currentThreshold = levelThresholds[currentLevel];
int nextThreshold = levelThresholds[currentLevel + 1];
return (points - currentThreshold) / (nextThreshold - currentThreshold);
}
static int getPointsToNextLevel(int points) {
int currentLevel = getLevel(points);
if (currentLevel >= levelThresholds.length - 1) return 0;
return levelThresholds[currentLevel + 1] - points;
}
static int getNextLevelThreshold(int points) {
int currentLevel = getLevel(points);
if (currentLevel >= levelThresholds.length - 1) {
return levelThresholds.last;
}
return levelThresholds[currentLevel + 1];
}
}

View File

@@ -0,0 +1,65 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_progress.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class UserProgressAdapter extends TypeAdapter<UserProgress> {
@override
final int typeId = 1;
@override
UserProgress read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return UserProgress(
totalPoints: fields[0] as int,
currentPoints: fields[1] as int,
lastCheckInDate: fields[2] as DateTime?,
consecutiveCheckIns: fields[3] as int,
unlockedAchievements: (fields[4] as Map?)?.cast<String, DateTime>(),
totalFocusMinutes: fields[5] as int,
totalDistractions: fields[6] as int,
totalSessions: fields[7] as int,
checkInHistory: (fields[8] as List?)?.cast<DateTime>(),
);
}
@override
void write(BinaryWriter writer, UserProgress obj) {
writer
..writeByte(9)
..writeByte(0)
..write(obj.totalPoints)
..writeByte(1)
..write(obj.currentPoints)
..writeByte(2)
..write(obj.lastCheckInDate)
..writeByte(3)
..write(obj.consecutiveCheckIns)
..writeByte(4)
..write(obj.unlockedAchievements)
..writeByte(5)
..write(obj.totalFocusMinutes)
..writeByte(6)
..write(obj.totalDistractions)
..writeByte(7)
..write(obj.totalSessions)
..writeByte(8)
..write(obj.checkInHistory);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserProgressAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}