积分、成就系统
This commit is contained in:
190
lib/models/achievement_config.dart
Normal file
190
lib/models/achievement_config.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
144
lib/models/user_progress.dart
Normal file
144
lib/models/user_progress.dart
Normal 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];
|
||||
}
|
||||
}
|
||||
65
lib/models/user_progress.g.dart
Normal file
65
lib/models/user_progress.g.dart
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user