feat(hall): Phase 10 명예의 전당 시스템 구현
- HallOfFameEntry 모델 및 HallOfFame 컬렉션 추가 - HallOfFameStorage 저장소 (JSON 파일 기반) - HallOfFameScreen UI (순위별 색상/아이콘) - 게임 클리어 시 명예의 전당 등록 처리 - FrontScreen에 명예의 전당 버튼 추가 - 클리어 축하 다이얼로그 구현
This commit is contained in:
192
lib/src/core/model/hall_of_fame.dart
Normal file
192
lib/src/core/model/hall_of_fame.dart
Normal file
@@ -0,0 +1,192 @@
|
||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/game_state.dart';
|
||||
|
||||
/// 명예의 전당 엔트리 (Phase 10: Hall of Fame Entry)
|
||||
///
|
||||
/// 게임 클리어 시 저장되는 캐릭터 정보
|
||||
class HallOfFameEntry {
|
||||
const HallOfFameEntry({
|
||||
required this.id,
|
||||
required this.characterName,
|
||||
required this.race,
|
||||
required this.klass,
|
||||
required this.level,
|
||||
required this.totalPlayTimeMs,
|
||||
required this.totalDeaths,
|
||||
required this.monstersKilled,
|
||||
required this.questsCompleted,
|
||||
required this.clearedAt,
|
||||
this.finalStats,
|
||||
this.finalEquipment,
|
||||
});
|
||||
|
||||
/// 고유 ID (UUID)
|
||||
final String id;
|
||||
|
||||
/// 캐릭터 이름
|
||||
final String characterName;
|
||||
|
||||
/// 종족
|
||||
final String race;
|
||||
|
||||
/// 클래스
|
||||
final String klass;
|
||||
|
||||
/// 최종 레벨
|
||||
final int level;
|
||||
|
||||
/// 총 플레이 시간 (밀리초)
|
||||
final int totalPlayTimeMs;
|
||||
|
||||
/// 총 사망 횟수
|
||||
final int totalDeaths;
|
||||
|
||||
/// 처치한 몬스터 수
|
||||
final int monstersKilled;
|
||||
|
||||
/// 완료한 퀘스트 수
|
||||
final int questsCompleted;
|
||||
|
||||
/// 클리어 일시
|
||||
final DateTime clearedAt;
|
||||
|
||||
/// 최종 전투 스탯 (향후 아스키 아레나용)
|
||||
final CombatStats? finalStats;
|
||||
|
||||
/// 최종 장비 목록 (향후 아스키 아레나용)
|
||||
final Map<String, String>? finalEquipment;
|
||||
|
||||
/// 플레이 시간을 Duration으로 변환
|
||||
Duration get totalPlayTime => Duration(milliseconds: totalPlayTimeMs);
|
||||
|
||||
/// 플레이 시간 포맷팅 (HH:MM:SS)
|
||||
String get formattedPlayTime {
|
||||
final hours = totalPlayTime.inHours;
|
||||
final minutes = totalPlayTime.inMinutes % 60;
|
||||
final seconds = totalPlayTime.inSeconds % 60;
|
||||
return '${hours.toString().padLeft(2, '0')}:'
|
||||
'${minutes.toString().padLeft(2, '0')}:'
|
||||
'${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 클리어 날짜 포맷팅 (YYYY.MM.DD)
|
||||
String get formattedClearedDate {
|
||||
return '${clearedAt.year}.${clearedAt.month.toString().padLeft(2, '0')}.'
|
||||
'${clearedAt.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// GameState에서 HallOfFameEntry 생성
|
||||
factory HallOfFameEntry.fromGameState({
|
||||
required GameState state,
|
||||
required int totalDeaths,
|
||||
required int monstersKilled,
|
||||
CombatStats? combatStats,
|
||||
}) {
|
||||
return HallOfFameEntry(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
characterName: state.traits.name,
|
||||
race: state.traits.race,
|
||||
klass: state.traits.klass,
|
||||
level: state.traits.level,
|
||||
totalPlayTimeMs: state.skillSystem.elapsedMs,
|
||||
totalDeaths: totalDeaths,
|
||||
monstersKilled: monstersKilled,
|
||||
questsCompleted: state.progress.questCount,
|
||||
clearedAt: DateTime.now(),
|
||||
finalStats: combatStats,
|
||||
finalEquipment: {
|
||||
'weapon': state.equipment.weapon,
|
||||
'shield': state.equipment.shield,
|
||||
'helm': state.equipment.helm,
|
||||
'hauberk': state.equipment.hauberk,
|
||||
'brassairts': state.equipment.brassairts,
|
||||
'vambraces': state.equipment.vambraces,
|
||||
'gauntlets': state.equipment.gauntlets,
|
||||
'gambeson': state.equipment.gambeson,
|
||||
'cuisses': state.equipment.cuisses,
|
||||
'greaves': state.equipment.greaves,
|
||||
'sollerets': state.equipment.sollerets,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// JSON으로 직렬화
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'characterName': characterName,
|
||||
'race': race,
|
||||
'klass': klass,
|
||||
'level': level,
|
||||
'totalPlayTimeMs': totalPlayTimeMs,
|
||||
'totalDeaths': totalDeaths,
|
||||
'monstersKilled': monstersKilled,
|
||||
'questsCompleted': questsCompleted,
|
||||
'clearedAt': clearedAt.toIso8601String(),
|
||||
'finalEquipment': finalEquipment,
|
||||
};
|
||||
}
|
||||
|
||||
/// JSON에서 역직렬화
|
||||
factory HallOfFameEntry.fromJson(Map<String, dynamic> json) {
|
||||
return HallOfFameEntry(
|
||||
id: json['id'] as String,
|
||||
characterName: json['characterName'] as String,
|
||||
race: json['race'] as String,
|
||||
klass: json['klass'] as String,
|
||||
level: json['level'] as int,
|
||||
totalPlayTimeMs: json['totalPlayTimeMs'] as int,
|
||||
totalDeaths: json['totalDeaths'] as int? ?? 0,
|
||||
monstersKilled: json['monstersKilled'] as int? ?? 0,
|
||||
questsCompleted: json['questsCompleted'] as int? ?? 0,
|
||||
clearedAt: DateTime.parse(json['clearedAt'] as String),
|
||||
finalEquipment: json['finalEquipment'] != null
|
||||
? Map<String, String>.from(json['finalEquipment'] as Map)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 명예의 전당 (Hall of Fame)
|
||||
///
|
||||
/// 클리어한 캐릭터 목록 관리
|
||||
class HallOfFame {
|
||||
const HallOfFame({required this.entries});
|
||||
|
||||
/// 명예의 전당 엔트리 목록 (클리어 시간 역순)
|
||||
final List<HallOfFameEntry> entries;
|
||||
|
||||
/// 빈 명예의 전당
|
||||
factory HallOfFame.empty() => const HallOfFame(entries: []);
|
||||
|
||||
/// 새 엔트리 추가
|
||||
HallOfFame addEntry(HallOfFameEntry entry) {
|
||||
final newEntries = List<HallOfFameEntry>.from(entries)
|
||||
..add(entry)
|
||||
..sort((a, b) => b.clearedAt.compareTo(a.clearedAt));
|
||||
return HallOfFame(entries: newEntries);
|
||||
}
|
||||
|
||||
/// 엔트리 수
|
||||
int get count => entries.length;
|
||||
|
||||
/// 비어있는지 확인
|
||||
bool get isEmpty => entries.isEmpty;
|
||||
|
||||
/// JSON으로 직렬화
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'entries': entries.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
/// JSON에서 역직렬화
|
||||
factory HallOfFame.fromJson(Map<String, dynamic> json) {
|
||||
final entriesJson = json['entries'] as List<dynamic>? ?? [];
|
||||
return HallOfFame(
|
||||
entries: entriesJson
|
||||
.map((e) => HallOfFameEntry.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
81
lib/src/core/storage/hall_of_fame_storage.dart
Normal file
81
lib/src/core/storage/hall_of_fame_storage.dart
Normal file
@@ -0,0 +1,81 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:askiineverdie/src/core/model/hall_of_fame.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
/// 명예의 전당 저장소 (Phase 10: Hall of Fame Storage)
|
||||
///
|
||||
/// 명예의 전당 데이터 저장/로드 관리
|
||||
class HallOfFameStorage {
|
||||
HallOfFameStorage();
|
||||
|
||||
static const String _fileName = 'hall_of_fame.json';
|
||||
|
||||
Directory? _storageDir;
|
||||
|
||||
Future<Directory> _getStorageDir() async {
|
||||
if (_storageDir != null) return _storageDir!;
|
||||
_storageDir = await getApplicationSupportDirectory();
|
||||
return _storageDir!;
|
||||
}
|
||||
|
||||
File _getFile(Directory dir) {
|
||||
return File('${dir.path}/$_fileName');
|
||||
}
|
||||
|
||||
/// 명예의 전당 로드
|
||||
Future<HallOfFame> load() async {
|
||||
try {
|
||||
final dir = await _getStorageDir();
|
||||
final file = _getFile(dir);
|
||||
|
||||
if (!await file.exists()) {
|
||||
return HallOfFame.empty();
|
||||
}
|
||||
|
||||
final content = await file.readAsString();
|
||||
final json = jsonDecode(content) as Map<String, dynamic>;
|
||||
return HallOfFame.fromJson(json);
|
||||
} catch (e) {
|
||||
// 파일이 없거나 손상된 경우 빈 명예의 전당 반환
|
||||
return HallOfFame.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/// 명예의 전당 저장
|
||||
Future<bool> save(HallOfFame hallOfFame) async {
|
||||
try {
|
||||
final dir = await _getStorageDir();
|
||||
final file = _getFile(dir);
|
||||
|
||||
final content = jsonEncode(hallOfFame.toJson());
|
||||
await file.writeAsString(content);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 새 엔트리 추가 및 저장
|
||||
Future<bool> addEntry(HallOfFameEntry entry) async {
|
||||
final hallOfFame = await load();
|
||||
final updated = hallOfFame.addEntry(entry);
|
||||
return save(updated);
|
||||
}
|
||||
|
||||
/// 명예의 전당 초기화 (테스트용)
|
||||
Future<bool> clear() async {
|
||||
try {
|
||||
final dir = await _getStorageDir();
|
||||
final file = _getFile(dir);
|
||||
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user