- DeathInfo에 lostItem 필드 추가 (광고 부활 시 복구용) - 세이브 데이터 v4: MonetizationState 포함 - 사망 오버레이 UI 개선 - 부활 서비스 광고 연동
382 lines
13 KiB
Dart
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);
|
|
}
|