LifeFlow/android/lib/models/gamification.dart

276 lines
6.6 KiB
Dart

import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
part 'gamification.freezed.dart';
part 'gamification.g.dart';
@HiveType(typeId: 8)
@freezed
class GamificationStats with _$GamificationStats {
const factory GamificationStats({
@HiveField(0) @Default(0) int totalPoints,
@HiveField(1) @Default(0) int currentStreak,
@HiveField(2) @Default(0) int longestStreak,
@HiveField(3) @Default(1) int level,
@HiveField(4) @Default([]) List<String> unlockedBadges,
@HiveField(5) @Default({}) Map<String, int> categoryStreaks,
@HiveField(6) @Default(0) int routinesCompletedToday,
@HiveField(7) @Default(0) int totalRoutinesCompleted,
@HiveField(8) @Default(0) int perfectDays,
@HiveField(9) required DateTime lastUpdated,
}) = _GamificationStats;
factory GamificationStats.fromJson(Map<String, dynamic> json) =
_$$GamificationStatsImplFromJson;
}
@HiveType(typeId: 9)
class Badge extends HiveObject {
@HiveField(0)
final String id;
@HiveField(1)
final String name;
@HiveField(2)
final String description;
@HiveField(3)
final String icon;
@HiveField(4)
final int pointsRequired;
@HiveField(5)
final BadgeCategory category;
Badge({
required this.id,
required this.name,
required this.description,
required this.icon,
required this.pointsRequired,
required this.category,
});
}
@HiveType(typeId: 10)
enum BadgeCategory {
@HiveField(0)
streak,
@HiveField(1)
completion,
@HiveField(2)
category,
@HiveField(3)
special,
}
// Predefined badges
class Badges {
static final List<Badge> all = [
// Streak badges
Badge(
id: 'streak_3',
name: 'Getting Started',
description: '3 day streak',
icon: '🔥',
pointsRequired: 0,
category: BadgeCategory.streak,
),
Badge(
id: 'streak_7',
name: 'Week Warrior',
description: '7 day streak',
icon: '🔥🔥',
pointsRequired: 0,
category: BadgeCategory.streak,
),
Badge(
id: 'streak_30',
name: 'Monthly Master',
description: '30 day streak',
icon: '📅',
pointsRequired: 0,
category: BadgeCategory.streak,
),
Badge(
id: 'streak_100',
name: 'Centurion',
description: '100 day streak',
icon: '💯',
pointsRequired: 0,
category: BadgeCategory.streak,
),
// Completion badges
Badge(
id: 'first_routine',
name: 'First Step',
description: 'Complete your first routine',
icon: '👟',
pointsRequired: 0,
category: BadgeCategory.completion,
),
Badge(
id: 'routines_10',
name: 'Getting Consistent',
description: 'Complete 10 routines',
icon: '',
pointsRequired: 0,
category: BadgeCategory.completion,
),
Badge(
id: 'routines_100',
name: 'Century Club',
description: 'Complete 100 routines',
icon: '🏆',
pointsRequired: 0,
category: BadgeCategory.completion,
),
Badge(
id: 'perfect_week',
name: 'Perfect Week',
description: 'Complete all routines for 7 days',
icon: '🌟',
pointsRequired: 0,
category: BadgeCategory.completion,
),
// Category badges
Badge(
id: 'hydration_master',
name: 'Hydration Hero',
description: 'Hit water goal 7 days in a row',
icon: '💧',
pointsRequired: 0,
category: BadgeCategory.category,
),
Badge(
id: 'medication_reliable',
name: 'Reliable',
description: 'Take medication on time for 14 days',
icon: '💊',
pointsRequired: 0,
category: BadgeCategory.category,
),
Badge(
id: 'sleep_schedule',
name: 'Sleep Master',
description: 'Maintain sleep schedule for 7 days',
icon: '😴',
pointsRequired: 0,
category: BadgeCategory.category,
),
// Special badges
Badge(
id: 'early_bird',
name: 'Early Bird',
description: 'Complete morning routine before 8 AM',
icon: '🐦',
pointsRequired: 0,
category: BadgeCategory.special,
),
Badge(
id: 'night_owl',
name: 'Night Owl',
description: 'Complete evening routine after 9 PM',
icon: '🦉',
pointsRequired: 0,
category: BadgeCategory.special,
),
];
}
// Level calculation
class LevelCalculator {
static int calculateLevel(int totalPoints) {
// Exponential level curve
// Level 1: 0 points
// Level 2: 100 points
// Level 3: 250 points
// Level 4: 450 points
// etc.
if (totalPoints < 100) return 1;
if (totalPoints < 250) return 2;
if (totalPoints < 450) return 3;
if (totalPoints < 700) return 4;
if (totalPoints < 1000) return 5;
// Beyond level 5: every 500 points
return 5 + ((totalPoints - 1000) ~/ 500);
}
static int pointsToNextLevel(int currentLevel, int totalPoints) {
final nextLevelThreshold = _levelThreshold(currentLevel + 1);
return nextLevelThreshold - totalPoints;
}
static double progressToNextLevel(int currentLevel, int totalPoints) {
final currentThreshold = _levelThreshold(currentLevel);
final nextThreshold = _levelThreshold(currentLevel + 1);
final progress = totalPoints - currentThreshold;
final needed = nextThreshold - currentThreshold;
return (progress / needed).clamp(0.0, 1.0);
}
static int _levelThreshold(int level) {
switch (level) {
case 1:
return 0;
case 2:
return 100;
case 3:
return 250;
case 4:
return 450;
case 5:
return 700;
default:
return 1000 + ((level - 6) * 500);
}
}
}
class BadgeChecker {
static List<Badge> checkUnlockedBadges(
GamificationStats stats,
List<String> alreadyUnlocked,
) {
final newlyUnlocked = <Badge>[];
for (final badge in Badges.all) {
if (alreadyUnlocked.contains(badge.id)) continue;
if (_meetsCriteria(badge, stats)) {
newlyUnlocked.add(badge);
}
}
return newlyUnlocked;
}
static bool _meetsCriteria(Badge badge, GamificationStats stats) {
switch (badge.id) {
case 'streak_3':
return stats.currentStreak >= 3;
case 'streak_7':
return stats.currentStreak >= 7;
case 'streak_30':
return stats.currentStreak >= 30;
case 'streak_100':
return stats.currentStreak >= 100;
case 'routines_10':
return stats.totalRoutinesCompleted >= 10;
case 'routines_100':
return stats.totalRoutinesCompleted >= 100;
case 'perfect_week':
return stats.perfectDays >= 7;
default:
return false;
}
}
}