Files
asciinevrdie/lib/src/core/model/save_data.dart
JiWoong Sul b6d5cd2abd feat(death): 사망/부활 시스템 개선
- DeathInfo에 lostItem 필드 추가 (광고 부활 시 복구용)
- 세이브 데이터 v4: MonetizationState 포함
- 사망 오버레이 UI 개선
- 부활 서비스 광고 연동
2026-01-16 20:09:52 +09:00

382 lines
13 KiB
Dart

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<String, dynamic> 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<String, dynamic> json) {
final traitsJson = json['traits'] as Map<String, dynamic>;
final statsJson = json['stats'] as Map<String, dynamic>;
final inventoryJson = json['inventory'] as Map<String, dynamic>;
final equipmentJson = json['equipment'] as Map<String, dynamic>;
final progressJson = json['progress'] as Map<String, dynamic>;
final queueJson = (json['queue'] as List<dynamic>? ?? []).cast<dynamic>();
final skillsJson = (json['skills'] as List<dynamic>? ?? []).cast<dynamic>();
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<dynamic>? ?? [])
.map(
(e) => InventoryEntry(
name: (e as Map<String, dynamic>)['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<String, dynamic>)['name'] as String? ?? '',
rank: (e)['rank'] as String? ?? 'I',
),
)
.toList(),
),
progress: ProgressState(
task: _barFromJson(progressJson['task'] as Map<String, dynamic>? ?? {}),
quest: _barFromJson(
progressJson['quest'] as Map<String, dynamic>? ?? {},
),
plot: _barFromJson(progressJson['plot'] as Map<String, dynamic>? ?? {}),
exp: _barFromJson(progressJson['exp'] as Map<String, dynamic>? ?? {}),
encumbrance: _barFromJson(
progressJson['encumbrance'] as Map<String, dynamic>? ?? {},
),
currentTask: _taskInfoFromJson(
progressJson['taskInfo'] as Map<String, dynamic>? ??
<String, dynamic>{},
),
plotStageCount: progressJson['plotStages'] as int? ?? 1,
questCount: progressJson['questCount'] as int? ?? 0,
plotHistory: _historyListFromJson(
progressJson['plotHistory'] as List<dynamic>?,
),
questHistory: _historyListFromJson(
progressJson['questHistory'] as List<dynamic>?,
),
currentQuestMonster: _questMonsterFromJson(
progressJson['questMonster'] as Map<String, dynamic>?,
),
monstersKilled: progressJson['monstersKilled'] as int? ?? 0,
deathCount: progressJson['deathCount'] as int? ?? 0,
),
queue: QueueState(
entries: Queue<QueueEntry>.from(
queueJson.map((e) {
final m = e as Map<String, dynamic>;
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<String, dynamic>?,
),
);
}
GameState toState() {
return GameState(
rng: DeterministicRandom.fromState(rngState),
traits: traits,
stats: stats,
inventory: inventory,
equipment: equipment,
skillBook: skillBook,
progress: progress,
queue: queue,
);
}
}
Map<String, dynamic> _barToJson(ProgressBarState bar) => {
'pos': bar.position,
'max': bar.max,
};
ProgressBarState _barFromJson(Map<String, dynamic> json) => ProgressBarState(
position: json['pos'] as int? ?? 0,
max: json['max'] as int? ?? 1,
);
TaskInfo _taskInfoFromJson(Map<String, dynamic> 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<HistoryEntry> _historyListFromJson(List<dynamic>? json) {
if (json == null || json.isEmpty) return [];
return json.map((e) {
final m = e as Map<String, dynamic>;
return HistoryEntry(
caption: m['caption'] as String? ?? '',
isComplete: m['complete'] as bool? ?? false,
);
}).toList();
}
QuestMonsterInfo? _questMonsterFromJson(Map<String, dynamic>? 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<String, dynamic> json, int version) {
// v3 이상: 새로운 형식 (items 배열)
if (version >= 3 && json['items'] != null) {
final itemsList = json['items'] as List<dynamic>;
final items = <EquipmentItem>[];
for (var i = 0; i < Equipment.slotCount; i++) {
if (i < itemsList.length) {
final itemJson = itemsList[i] as Map<String, dynamic>;
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<String, dynamic>? json) {
if (json == null) return null;
return MonetizationState.fromJson(json);
}