LineInkGuide/lib/managers.dart

247 lines
6.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (13).',
rewardStars: 2,
),
AchievementDef(
id: 'bounce_master',
title: 'Bounce Master',
description: 'Clear a bounce level (46).',
rewardStars: 2,
),
AchievementDef(
id: 'maze_master',
title: 'Maze Navigator',
description: 'Clear a multi-layer maze level (79).',
rewardStars: 2,
),
AchievementDef(
id: 'gap_master',
title: 'Gap Sniper',
description: 'Clear a narrow-gap level (1012).',
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;
}
}