feat(hall): Phase 10 명예의 전당 시스템 구현

- HallOfFameEntry 모델 및 HallOfFame 컬렉션 추가
- HallOfFameStorage 저장소 (JSON 파일 기반)
- HallOfFameScreen UI (순위별 색상/아이콘)
- 게임 클리어 시 명예의 전당 등록 처리
- FrontScreen에 명예의 전당 버튼 추가
- 클리어 축하 다이얼로그 구현
This commit is contained in:
JiWoong Sul
2025-12-17 18:57:26 +09:00
parent 7c7f3b0d9e
commit 9af5c4dc13
6 changed files with 814 additions and 2 deletions

View 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(),
);
}
}