247 lines
6.7 KiB
Dart
247 lines
6.7 KiB
Dart
import 'dart:convert';
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'app_theme.dart';
|
||
|
||
// --- Achievement System ---
|
||
class AchievementDef {
|
||
final String id;
|
||
final String title;
|
||
final String description;
|
||
final int rewardStars;
|
||
|
||
const AchievementDef({
|
||
required this.id,
|
||
required this.title,
|
||
required this.description,
|
||
required this.rewardStars,
|
||
});
|
||
}
|
||
|
||
// --- Level Progress Manager ---
|
||
class LevelManager {
|
||
static final LevelManager instance = LevelManager._internal();
|
||
LevelManager._internal();
|
||
|
||
final Map<int, int> _bestStars = {};
|
||
int highestUnlockedLevel = 1;
|
||
int totalStars = 0; // 当前可用星星(用于兑换)
|
||
String selectedSkinId = 'classic';
|
||
final Set<String> ownedSkins = {'classic'};
|
||
final Map<String, bool> _unlockedAchievements = {};
|
||
|
||
static const _bestStarsKey = 'bestStars';
|
||
static const _highestLevelKey = 'highestUnlockedLevel';
|
||
static const _totalStarsKey = 'totalStars';
|
||
static const _ownedSkinsKey = 'ownedSkins';
|
||
static const _selectedSkinKey = 'selectedSkin';
|
||
static const _achievementsKey = 'achievements';
|
||
|
||
static const List<AchievementDef> achievementsCatalog = [
|
||
AchievementDef(
|
||
id: 'first_clear',
|
||
title: 'First Steps',
|
||
description: 'Clear any level for the first time.',
|
||
rewardStars: 3,
|
||
),
|
||
AchievementDef(
|
||
id: 'perfect_run',
|
||
title: 'Perfect Ink',
|
||
description: 'Earn 5 stars in a single level.',
|
||
rewardStars: 5,
|
||
),
|
||
AchievementDef(
|
||
id: 'speed_runner',
|
||
title: 'Speed Runner',
|
||
description: 'Clear a level within 30 seconds.',
|
||
rewardStars: 5,
|
||
),
|
||
AchievementDef(
|
||
id: 'slope_master',
|
||
title: 'Slope Master',
|
||
description: 'Clear a slope level (1–3).',
|
||
rewardStars: 2,
|
||
),
|
||
AchievementDef(
|
||
id: 'bounce_master',
|
||
title: 'Bounce Master',
|
||
description: 'Clear a bounce level (4–6).',
|
||
rewardStars: 2,
|
||
),
|
||
AchievementDef(
|
||
id: 'maze_master',
|
||
title: 'Maze Navigator',
|
||
description: 'Clear a multi-layer maze level (7–9).',
|
||
rewardStars: 2,
|
||
),
|
||
AchievementDef(
|
||
id: 'gap_master',
|
||
title: 'Gap Sniper',
|
||
description: 'Clear a narrow-gap level (10–12).',
|
||
rewardStars: 2,
|
||
),
|
||
];
|
||
|
||
Future<void> load() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final bestStr = prefs.getString(_bestStarsKey);
|
||
if (bestStr != null) {
|
||
final Map<String, dynamic> decoded = jsonDecode(bestStr);
|
||
_bestStars
|
||
..clear()
|
||
..addEntries(
|
||
decoded.entries.map(
|
||
(e) => MapEntry(int.parse(e.key), e.value as int),
|
||
),
|
||
);
|
||
}
|
||
|
||
highestUnlockedLevel = prefs.getInt(_highestLevelKey) ?? 1;
|
||
if (highestUnlockedLevel < 1) highestUnlockedLevel = 1;
|
||
|
||
totalStars = prefs.getInt(_totalStarsKey) ?? 0;
|
||
|
||
final owned = prefs.getStringList(_ownedSkinsKey);
|
||
ownedSkins
|
||
..clear()
|
||
..addAll(owned?.toSet() ?? {'classic'});
|
||
|
||
selectedSkinId = prefs.getString(_selectedSkinKey) ?? 'classic';
|
||
if (!ownedSkins.contains(selectedSkinId)) {
|
||
selectedSkinId = 'classic';
|
||
}
|
||
|
||
final unlockedList = prefs.getStringList(_achievementsKey);
|
||
_unlockedAchievements
|
||
..clear()
|
||
..addEntries(
|
||
(unlockedList ?? const <String>[]).map((id) => MapEntry(id, true)),
|
||
);
|
||
}
|
||
|
||
Future<void> _persist() async {
|
||
final prefs = await SharedPreferences.getInstance();
|
||
final mapAsStringInt = _bestStars.map(
|
||
(key, value) => MapEntry(key.toString(), value),
|
||
);
|
||
await prefs.setString(_bestStarsKey, jsonEncode(mapAsStringInt));
|
||
await prefs.setInt(_highestLevelKey, highestUnlockedLevel);
|
||
await prefs.setInt(_totalStarsKey, totalStars);
|
||
await prefs.setStringList(_ownedSkinsKey, ownedSkins.toList());
|
||
await prefs.setString(_selectedSkinKey, selectedSkinId);
|
||
await prefs.setStringList(
|
||
_achievementsKey,
|
||
_unlockedAchievements.entries
|
||
.where((e) => e.value)
|
||
.map((e) => e.key)
|
||
.toList(),
|
||
);
|
||
}
|
||
|
||
Future<void> clearAll() async {
|
||
_bestStars.clear();
|
||
highestUnlockedLevel = 1;
|
||
totalStars = 0;
|
||
ownedSkins
|
||
..clear()
|
||
..add('classic');
|
||
selectedSkinId = 'classic';
|
||
await _persist();
|
||
}
|
||
|
||
void saveProgress(int levelId, int stars, int maxLevel) {
|
||
final oldBest = _bestStars[levelId] ?? 0;
|
||
if (stars > oldBest) {
|
||
_bestStars[levelId] = stars;
|
||
// 只把"新提高"的星星计入总数,避免重复刷
|
||
totalStars += (stars - oldBest);
|
||
}
|
||
|
||
// 通关解锁下一关(至少 1 星)
|
||
if (levelId == highestUnlockedLevel && stars > 0) {
|
||
if (highestUnlockedLevel < maxLevel) {
|
||
highestUnlockedLevel = highestUnlockedLevel + 1;
|
||
}
|
||
}
|
||
|
||
// 异步持久化
|
||
_persist();
|
||
}
|
||
|
||
int getStars(int levelId) => _bestStars[levelId] ?? 0;
|
||
|
||
// 皮肤相关
|
||
bool isSkinOwned(String skinId) => ownedSkins.contains(skinId);
|
||
|
||
bool isSkinSelected(String skinId) => selectedSkinId == skinId;
|
||
|
||
bool canAfford(int cost) => totalStars >= cost;
|
||
|
||
void selectSkin(String skinId) {
|
||
if (!isSkinOwned(skinId)) return;
|
||
selectedSkinId = skinId;
|
||
_persist();
|
||
}
|
||
|
||
bool buySkin(SkinThemeData skin) {
|
||
if (isSkinOwned(skin.id)) return false;
|
||
if (!canAfford(skin.cost)) return false;
|
||
totalStars -= skin.cost;
|
||
ownedSkins.add(skin.id);
|
||
selectedSkinId = skin.id;
|
||
_persist();
|
||
return true;
|
||
}
|
||
|
||
bool isAchievementUnlocked(String id) => _unlockedAchievements[id] == true;
|
||
|
||
List<AchievementDef> unlockAchievementsForWin({
|
||
required int levelId,
|
||
required int stars,
|
||
required Duration? clearTime,
|
||
}) {
|
||
final newlyUnlocked = <AchievementDef>[];
|
||
|
||
void _tryUnlock(String id) {
|
||
if (isAchievementUnlocked(id)) return;
|
||
final def = achievementsCatalog.firstWhere(
|
||
(a) => a.id == id,
|
||
orElse: () => achievementsCatalog.first,
|
||
);
|
||
_unlockedAchievements[id] = true;
|
||
totalStars += def.rewardStars;
|
||
newlyUnlocked.add(def);
|
||
}
|
||
|
||
// 首次通关任意关卡
|
||
if (stars > 0 && _unlockedAchievements.isEmpty) {
|
||
_tryUnlock('first_clear');
|
||
}
|
||
|
||
// 一次性拿满 5 星
|
||
if (stars == 5) {
|
||
_tryUnlock('perfect_run');
|
||
}
|
||
|
||
// 在 30 秒内通关一关
|
||
if (clearTime != null && clearTime.inSeconds <= 30) {
|
||
_tryUnlock('speed_runner');
|
||
}
|
||
|
||
// 主题关成就,基于关卡编号区间
|
||
if (levelId >= 1 && levelId <= 3) {
|
||
_tryUnlock('slope_master');
|
||
} else if (levelId >= 4 && levelId <= 6) {
|
||
_tryUnlock('bounce_master');
|
||
} else if (levelId >= 7 && levelId <= 9) {
|
||
_tryUnlock('maze_master');
|
||
} else if (levelId >= 10 && levelId <= 12) {
|
||
_tryUnlock('gap_master');
|
||
}
|
||
|
||
if (newlyUnlocked.isNotEmpty) {
|
||
_persist();
|
||
}
|
||
return newlyUnlocked;
|
||
}
|
||
}
|