import 'dart:collection'; import 'package:asciineverdie/src/core/model/equipment_item.dart'; import 'package:asciineverdie/src/core/model/equipment_slot.dart'; import 'package:asciineverdie/src/core/model/monetization_state.dart'; import 'package:asciineverdie/src/core/model/monster_grade.dart'; import 'package:asciineverdie/src/core/util/deterministic_random.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; /// 세이브 파일 버전 /// - v2: 장비 이름만 저장 (레거시) /// - v3: 장비 전체 정보 저장 (level, rarity, stats 포함) /// - v4: MonetizationState 추가, DeathInfo.lostItem 추가 const int kSaveVersion = 4; class GameSave { GameSave({ required this.version, required this.rngState, required this.traits, required this.stats, required this.inventory, required this.equipment, required this.skillBook, required this.progress, required this.queue, this.cheatsEnabled = false, this.monetization, }); factory GameSave.fromState( GameState state, { bool cheatsEnabled = false, MonetizationState? monetization, }) { return GameSave( version: kSaveVersion, rngState: state.rng.state, traits: state.traits, stats: state.stats, inventory: state.inventory, equipment: state.equipment, skillBook: state.skillBook, progress: state.progress, queue: state.queue, cheatsEnabled: cheatsEnabled, monetization: monetization, ); } final int version; final int rngState; final Traits traits; final Stats stats; final Inventory inventory; final Equipment equipment; final SkillBook skillBook; final ProgressState progress; final QueueState queue; final bool cheatsEnabled; /// 수익화 시스템 상태 (v4+) final MonetizationState? monetization; Map toJson() { return { 'version': version, 'cheatsEnabled': cheatsEnabled, 'rng': rngState, 'traits': { 'name': traits.name, 'race': traits.race, 'klass': traits.klass, 'level': traits.level, 'motto': traits.motto, 'guild': traits.guild, 'raceId': traits.raceId, 'classId': traits.classId, }, 'stats': { 'str': stats.str, 'con': stats.con, 'dex': stats.dex, 'int': stats.intelligence, 'wis': stats.wis, 'cha': stats.cha, 'hpMax': stats.hpMax, 'mpMax': stats.mpMax, 'hpCurrent': stats.hpCurrent, 'mpCurrent': stats.mpCurrent, }, 'inventory': { 'gold': inventory.gold, 'items': inventory.items .map((e) => {'name': e.name, 'count': e.count}) .toList(), }, 'equipment': { 'items': equipment.items.map((e) => e.toJson()).toList(), 'bestIndex': equipment.bestIndex, }, 'skills': skillBook.skills .map((e) => {'name': e.name, 'rank': e.rank}) .toList(), 'progress': { 'task': _barToJson(progress.task), 'quest': _barToJson(progress.quest), 'plot': _barToJson(progress.plot), 'exp': _barToJson(progress.exp), 'encumbrance': _barToJson(progress.encumbrance), 'taskInfo': { 'caption': progress.currentTask.caption, 'type': progress.currentTask.type.name, 'monsterBaseName': progress.currentTask.monsterBaseName, 'monsterPart': progress.currentTask.monsterPart, 'monsterLevel': progress.currentTask.monsterLevel, 'monsterGrade': progress.currentTask.monsterGrade?.name, }, 'plotStages': progress.plotStageCount, 'questCount': progress.questCount, 'plotHistory': progress.plotHistory .map((e) => {'caption': e.caption, 'complete': e.isComplete}) .toList(), 'questHistory': progress.questHistory .map((e) => {'caption': e.caption, 'complete': e.isComplete}) .toList(), 'questMonster': progress.currentQuestMonster != null ? { 'data': progress.currentQuestMonster!.monsterData, 'index': progress.currentQuestMonster!.monsterIndex, } : null, 'monstersKilled': progress.monstersKilled, 'deathCount': progress.deathCount, }, 'queue': queue.entries .map( (e) => { 'kind': e.kind.name, 'duration': e.durationMillis, 'caption': e.caption, 'taskType': e.taskType.name, }, ) .toList(), if (monetization != null) 'monetization': monetization!.toJson(), }; } static GameSave fromJson(Map json) { final traitsJson = json['traits'] as Map; final statsJson = json['stats'] as Map; final inventoryJson = json['inventory'] as Map; final equipmentJson = json['equipment'] as Map; final progressJson = json['progress'] as Map; final queueJson = (json['queue'] as List? ?? []).cast(); final skillsJson = (json['skills'] as List? ?? []).cast(); return GameSave( version: json['version'] as int? ?? kSaveVersion, cheatsEnabled: json['cheatsEnabled'] as bool? ?? false, rngState: json['rng'] as int? ?? 0, traits: Traits( name: traitsJson['name'] as String? ?? '', race: traitsJson['race'] as String? ?? '', klass: traitsJson['klass'] as String? ?? '', level: traitsJson['level'] as int? ?? 1, motto: traitsJson['motto'] as String? ?? '', guild: traitsJson['guild'] as String? ?? '', raceId: traitsJson['raceId'] as String? ?? '', classId: traitsJson['classId'] as String? ?? '', ), stats: Stats( str: statsJson['str'] as int? ?? 0, con: statsJson['con'] as int? ?? 0, dex: statsJson['dex'] as int? ?? 0, intelligence: statsJson['int'] as int? ?? 0, wis: statsJson['wis'] as int? ?? 0, cha: statsJson['cha'] as int? ?? 0, hpMax: statsJson['hpMax'] as int? ?? 0, mpMax: statsJson['mpMax'] as int? ?? 0, hpCurrent: statsJson['hpCurrent'] as int?, mpCurrent: statsJson['mpCurrent'] as int?, ), inventory: Inventory( gold: inventoryJson['gold'] as int? ?? 0, items: (inventoryJson['items'] as List? ?? []) .map( (e) => InventoryEntry( name: (e as Map)['name'] as String? ?? '', count: (e)['count'] as int? ?? 0, ), ) .toList(), ), equipment: _equipmentFromJson( equipmentJson, json['version'] as int? ?? 2, ), skillBook: SkillBook( skills: skillsJson .map( (e) => SkillEntry( name: (e as Map)['name'] as String? ?? '', rank: (e)['rank'] as String? ?? 'I', ), ) .toList(), ), progress: ProgressState( task: _barFromJson(progressJson['task'] as Map? ?? {}), quest: _barFromJson( progressJson['quest'] as Map? ?? {}, ), plot: _barFromJson(progressJson['plot'] as Map? ?? {}), exp: _barFromJson(progressJson['exp'] as Map? ?? {}), encumbrance: _barFromJson( progressJson['encumbrance'] as Map? ?? {}, ), currentTask: _taskInfoFromJson( progressJson['taskInfo'] as Map? ?? {}, ), plotStageCount: progressJson['plotStages'] as int? ?? 1, questCount: progressJson['questCount'] as int? ?? 0, plotHistory: _historyListFromJson( progressJson['plotHistory'] as List?, ), questHistory: _historyListFromJson( progressJson['questHistory'] as List?, ), currentQuestMonster: _questMonsterFromJson( progressJson['questMonster'] as Map?, ), monstersKilled: progressJson['monstersKilled'] as int? ?? 0, deathCount: progressJson['deathCount'] as int? ?? 0, ), queue: QueueState( entries: Queue.from( queueJson.map((e) { final m = e as Map; final kind = QueueKind.values.firstWhere( (k) => k.name == m['kind'], orElse: () => QueueKind.task, ); final taskType = TaskType.values.firstWhere( (t) => t.name == m['taskType'], orElse: () => TaskType.neutral, ); return QueueEntry( kind: kind, durationMillis: m['duration'] as int? ?? 0, caption: m['caption'] as String? ?? '', taskType: taskType, ); }), ), ), monetization: _monetizationFromJson( json['monetization'] as Map?, ), ); } GameState toState() { return GameState( rng: DeterministicRandom.fromState(rngState), traits: traits, stats: stats, inventory: inventory, equipment: equipment, skillBook: skillBook, progress: progress, queue: queue, ); } } Map _barToJson(ProgressBarState bar) => { 'pos': bar.position, 'max': bar.max, }; ProgressBarState _barFromJson(Map json) => ProgressBarState( position: json['pos'] as int? ?? 0, max: json['max'] as int? ?? 1, ); TaskInfo _taskInfoFromJson(Map json) { final typeName = json['type'] as String?; final type = TaskType.values.firstWhere( (t) => t.name == typeName, orElse: () => TaskType.neutral, ); // monsterGrade 파싱 final gradeName = json['monsterGrade'] as String?; final monsterGrade = gradeName != null ? MonsterGrade.values.firstWhere( (g) => g.name == gradeName, orElse: () => MonsterGrade.normal, ) : null; return TaskInfo( caption: json['caption'] as String? ?? '', type: type, monsterBaseName: json['monsterBaseName'] as String?, monsterPart: json['monsterPart'] as String?, monsterLevel: json['monsterLevel'] as int?, monsterGrade: monsterGrade, ); } List _historyListFromJson(List? json) { if (json == null || json.isEmpty) return []; return json.map((e) { final m = e as Map; return HistoryEntry( caption: m['caption'] as String? ?? '', isComplete: m['complete'] as bool? ?? false, ); }).toList(); } QuestMonsterInfo? _questMonsterFromJson(Map? json) { if (json == null) return null; return QuestMonsterInfo( monsterData: json['data'] as String? ?? '', monsterIndex: json['index'] as int? ?? -1, ); } /// 장비 데이터 역직렬화 (버전별 분기 처리) /// /// - v2 이하: 레거시 문자열 기반 (이름만 저장) /// - v3 이상: 전체 EquipmentItem 정보 저장 Equipment _equipmentFromJson(Map json, int version) { // v3 이상: 새로운 형식 (items 배열) if (version >= 3 && json['items'] != null) { final itemsList = json['items'] as List; final items = []; for (var i = 0; i < Equipment.slotCount; i++) { if (i < itemsList.length) { final itemJson = itemsList[i] as Map; items.add(EquipmentItem.fromJson(itemJson)); } else { // 누락된 슬롯은 빈 아이템으로 채움 items.add(EquipmentItem.empty(EquipmentSlot.values[i])); } } return Equipment(items: items, bestIndex: json['bestIndex'] as int? ?? 0); } // v2 이하: 레거시 형식 (문자열 기반) return Equipment.fromStrings( weapon: json['weapon'] as String? ?? 'Keyboard', shield: json['shield'] as String? ?? '', helm: json['helm'] as String? ?? '', hauberk: json['hauberk'] as String? ?? '', brassairts: json['brassairts'] as String? ?? '', vambraces: json['vambraces'] as String? ?? '', gauntlets: json['gauntlets'] as String? ?? '', gambeson: json['gambeson'] as String? ?? '', cuisses: json['cuisses'] as String? ?? '', greaves: json['greaves'] as String? ?? '', sollerets: json['sollerets'] as String? ?? '', bestIndex: json['bestIndex'] as int? ?? 0, ); } /// MonetizationState 역직렬화 (v4+ 마이그레이션) /// /// - v3 이하: null (기본값 사용) /// - v4 이상: 저장된 상태 로드 MonetizationState? _monetizationFromJson(Map? json) { if (json == null) return null; return MonetizationState.fromJson(json); }