From 9af5c4dc13ede6fc86468de1f5ca55b12143f2f4 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 17 Dec 2025 18:57:26 +0900 Subject: [PATCH] =?UTF-8?q?feat(hall):=20Phase=2010=20=EB=AA=85=EC=98=88?= =?UTF-8?q?=EC=9D=98=20=EC=A0=84=EB=8B=B9=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HallOfFameEntry 모델 및 HallOfFame 컬렉션 추가 - HallOfFameStorage 저장소 (JSON 파일 기반) - HallOfFameScreen UI (순위별 색상/아이콘) - 게임 클리어 시 명예의 전당 등록 처리 - FrontScreen에 명예의 전당 버튼 추가 - 클리어 축하 다이얼로그 구현 --- lib/src/app.dart | 11 + lib/src/core/model/hall_of_fame.dart | 192 ++++++++ .../core/storage/hall_of_fame_storage.dart | 81 +++ lib/src/features/front/front_screen.dart | 27 +- lib/src/features/game/game_play_screen.dart | 45 ++ .../hall_of_fame/hall_of_fame_screen.dart | 460 ++++++++++++++++++ 6 files changed, 814 insertions(+), 2 deletions(-) create mode 100644 lib/src/core/model/hall_of_fame.dart create mode 100644 lib/src/core/storage/hall_of_fame_storage.dart create mode 100644 lib/src/features/hall_of_fame/hall_of_fame_screen.dart diff --git a/lib/src/app.dart b/lib/src/app.dart index 348e5c5..e2a5e77 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -13,6 +13,7 @@ import 'package:askiineverdie/src/features/front/front_screen.dart'; import 'package:askiineverdie/src/features/front/save_picker_dialog.dart'; import 'package:askiineverdie/src/features/game/game_play_screen.dart'; import 'package:askiineverdie/src/features/game/game_session_controller.dart'; +import 'package:askiineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart'; import 'package:askiineverdie/src/features/new_character/new_character_screen.dart'; class AskiiNeverDieApp extends StatefulWidget { @@ -69,6 +70,7 @@ class _AskiiNeverDieAppState extends State { home: FrontScreen( onNewCharacter: _navigateToNewCharacter, onLoadSave: _loadSave, + onHallOfFame: _navigateToHallOfFame, ), ); } @@ -152,4 +154,13 @@ class _AskiiNeverDieAppState extends State { ), ); } + + /// Phase 10: 명예의 전당 화면으로 이동 + void _navigateToHallOfFame(BuildContext context) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const HallOfFameScreen(), + ), + ); + } } diff --git a/lib/src/core/model/hall_of_fame.dart b/lib/src/core/model/hall_of_fame.dart new file mode 100644 index 0000000..7d97323 --- /dev/null +++ b/lib/src/core/model/hall_of_fame.dart @@ -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? 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 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 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.from(json['finalEquipment'] as Map) + : null, + ); + } +} + +/// 명예의 전당 (Hall of Fame) +/// +/// 클리어한 캐릭터 목록 관리 +class HallOfFame { + const HallOfFame({required this.entries}); + + /// 명예의 전당 엔트리 목록 (클리어 시간 역순) + final List entries; + + /// 빈 명예의 전당 + factory HallOfFame.empty() => const HallOfFame(entries: []); + + /// 새 엔트리 추가 + HallOfFame addEntry(HallOfFameEntry entry) { + final newEntries = List.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 toJson() { + return { + 'entries': entries.map((e) => e.toJson()).toList(), + }; + } + + /// JSON에서 역직렬화 + factory HallOfFame.fromJson(Map json) { + final entriesJson = json['entries'] as List? ?? []; + return HallOfFame( + entries: entriesJson + .map((e) => HallOfFameEntry.fromJson(e as Map)) + .toList(), + ); + } +} diff --git a/lib/src/core/storage/hall_of_fame_storage.dart b/lib/src/core/storage/hall_of_fame_storage.dart new file mode 100644 index 0000000..5094f67 --- /dev/null +++ b/lib/src/core/storage/hall_of_fame_storage.dart @@ -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 _getStorageDir() async { + if (_storageDir != null) return _storageDir!; + _storageDir = await getApplicationSupportDirectory(); + return _storageDir!; + } + + File _getFile(Directory dir) { + return File('${dir.path}/$_fileName'); + } + + /// 명예의 전당 로드 + Future 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; + return HallOfFame.fromJson(json); + } catch (e) { + // 파일이 없거나 손상된 경우 빈 명예의 전당 반환 + return HallOfFame.empty(); + } + } + + /// 명예의 전당 저장 + Future 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 addEntry(HallOfFameEntry entry) async { + final hallOfFame = await load(); + final updated = hallOfFame.addEntry(entry); + return save(updated); + } + + /// 명예의 전당 초기화 (테스트용) + Future clear() async { + try { + final dir = await _getStorageDir(); + final file = _getFile(dir); + + if (await file.exists()) { + await file.delete(); + } + return true; + } catch (e) { + return false; + } + } +} diff --git a/lib/src/features/front/front_screen.dart b/lib/src/features/front/front_screen.dart index 08d17d1..9a1af8b 100644 --- a/lib/src/features/front/front_screen.dart +++ b/lib/src/features/front/front_screen.dart @@ -3,7 +3,12 @@ import 'package:flutter/material.dart'; import 'package:askiineverdie/l10n/app_localizations.dart'; class FrontScreen extends StatelessWidget { - const FrontScreen({super.key, this.onNewCharacter, this.onLoadSave}); + const FrontScreen({ + super.key, + this.onNewCharacter, + this.onLoadSave, + this.onHallOfFame, + }); /// "New character" 버튼 클릭 시 호출 final void Function(BuildContext context)? onNewCharacter; @@ -11,6 +16,9 @@ class FrontScreen extends StatelessWidget { /// "Load save" 버튼 클릭 시 호출 final Future Function(BuildContext context)? onLoadSave; + /// "Hall of Fame" 버튼 클릭 시 호출 (Phase 10) + final void Function(BuildContext context)? onHallOfFame; + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -43,6 +51,9 @@ class FrontScreen extends StatelessWidget { onLoadSave: onLoadSave != null ? () => onLoadSave!(context) : () => _showPlaceholder(context), + onHallOfFame: onHallOfFame != null + ? () => onHallOfFame!(context) + : null, ), const SizedBox(height: 24), const _StatusCards(), @@ -150,10 +161,15 @@ class _HeroHeader extends StatelessWidget { } class _ActionRow extends StatelessWidget { - const _ActionRow({required this.onNewCharacter, required this.onLoadSave}); + const _ActionRow({ + required this.onNewCharacter, + required this.onLoadSave, + this.onHallOfFame, + }); final VoidCallback onNewCharacter; final VoidCallback onLoadSave; + final VoidCallback? onHallOfFame; @override Widget build(BuildContext context) { @@ -187,6 +203,13 @@ class _ActionRow extends StatelessWidget { icon: const Icon(Icons.menu_book_outlined), label: Text(l10n.viewBuildPlan), ), + // Phase 10: 명예의 전당 버튼 + if (onHallOfFame != null) + TextButton.icon( + onPressed: onHallOfFame, + icon: const Icon(Icons.emoji_events_outlined), + label: const Text('Hall of Fame'), + ), ], ); } diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index 9306f77..b888e39 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -6,9 +6,12 @@ import 'package:askiineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:askiineverdie/src/core/engine/story_service.dart'; import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:askiineverdie/src/core/model/game_state.dart'; +import 'package:askiineverdie/src/core/model/hall_of_fame.dart'; import 'package:askiineverdie/src/core/notification/notification_service.dart'; +import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart'; import 'package:askiineverdie/src/core/util/pq_logic.dart' as pq_logic; import 'package:askiineverdie/src/features/game/game_session_controller.dart'; +import 'package:askiineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart'; import 'package:askiineverdie/src/features/game/widgets/cinematic_view.dart'; import 'package:askiineverdie/src/features/game/widgets/combat_log.dart'; import 'package:askiineverdie/src/features/game/widgets/hp_mp_bar.dart'; @@ -70,6 +73,11 @@ class _GamePlayScreenState extends State if (newAct != _lastAct && !_showingCinematic) { _lastAct = newAct; _showCinematicForAct(newAct); + + // Phase 10: 엔딩 도달 시 클리어 처리 + if (newAct == StoryAct.ending && state.traits.level >= 100) { + _handleGameClear(state); + } } } _lastLevel = state.traits.level; @@ -131,6 +139,43 @@ class _GamePlayScreenState extends State _showingCinematic = false; } + /// Phase 10: 게임 클리어 처리 (Handle Game Clear) + Future _handleGameClear(GameState state) async { + // 게임 일시 정지 + await widget.controller.pause(saveOnStop: true); + + // 명예의 전당 엔트리 생성 + final entry = HallOfFameEntry.fromGameState( + state: state, + totalDeaths: 0, // TODO: 사망 횟수 추적 구현 시 연결 + monstersKilled: 0, // TODO: 처치 수 추적 구현 시 연결 + ); + + // 명예의 전당에 저장 + final storage = HallOfFameStorage(); + await storage.addEntry(entry); + + // 클리어 다이얼로그 표시 + if (mounted) { + await showGameClearDialog( + context, + entry: entry, + onNewGame: () { + // 프론트 화면으로 돌아가기 + Navigator.of(context).popUntil((route) => route.isFirst); + }, + onViewHallOfFame: () { + // 명예의 전당 화면으로 이동 + Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (context) => const HallOfFameScreen(), + ), + ); + }, + ); + } + } + void _resetSpecialAnimationAfterFrame() { // 다음 프레임에서 리셋 (AsciiAnimationCard가 값을 받은 후) WidgetsBinding.instance.addPostFrameCallback((_) { diff --git a/lib/src/features/hall_of_fame/hall_of_fame_screen.dart b/lib/src/features/hall_of_fame/hall_of_fame_screen.dart new file mode 100644 index 0000000..afd4553 --- /dev/null +++ b/lib/src/features/hall_of_fame/hall_of_fame_screen.dart @@ -0,0 +1,460 @@ +import 'package:flutter/material.dart'; + +import 'package:askiineverdie/src/core/l10n/game_data_l10n.dart'; +import 'package:askiineverdie/src/core/model/hall_of_fame.dart'; +import 'package:askiineverdie/src/core/storage/hall_of_fame_storage.dart'; + +/// 명예의 전당 화면 (Phase 10: Hall of Fame Screen) +class HallOfFameScreen extends StatefulWidget { + const HallOfFameScreen({super.key}); + + @override + State createState() => _HallOfFameScreenState(); +} + +class _HallOfFameScreenState extends State { + final HallOfFameStorage _storage = HallOfFameStorage(); + HallOfFame? _hallOfFame; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadHallOfFame(); + } + + Future _loadHallOfFame() async { + final hallOfFame = await _storage.load(); + if (mounted) { + setState(() { + _hallOfFame = hallOfFame; + _isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Hall of Fame'), + centerTitle: true, + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _buildContent(), + ); + } + + Widget _buildContent() { + final hallOfFame = _hallOfFame; + if (hallOfFame == null || hallOfFame.isEmpty) { + return _buildEmptyState(); + } + + return _buildHallOfFameList(hallOfFame); + } + + Widget _buildEmptyState() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.emoji_events_outlined, + size: 80, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + Text( + 'No heroes yet', + style: TextStyle( + fontSize: 20, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 8), + Text( + 'Defeat the Glitch God to enshrine your legend!', + style: TextStyle( + fontSize: 14, + color: Colors.grey.shade500, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + Widget _buildHallOfFameList(HallOfFame hallOfFame) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.amber.shade700, width: 2), + borderRadius: BorderRadius.circular(8), + ), + margin: const EdgeInsets.all(16), + child: Column( + children: [ + // 헤더 + Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: Colors.amber.shade700, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(6), + topRight: Radius.circular(6), + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.emoji_events, color: Colors.white), + SizedBox(width: 8), + Text( + 'HALL OF FAME', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 2, + ), + ), + SizedBox(width: 8), + Icon(Icons.emoji_events, color: Colors.white), + ], + ), + ), + // 엔트리 목록 + Expanded( + child: ListView.builder( + padding: const EdgeInsets.all(8), + itemCount: hallOfFame.entries.length, + itemBuilder: (context, index) { + final entry = hallOfFame.entries[index]; + return _HallOfFameEntryCard( + entry: entry, + rank: index + 1, + ); + }, + ), + ), + ], + ), + ); + } +} + +/// 명예의 전당 엔트리 카드 +class _HallOfFameEntryCard extends StatelessWidget { + const _HallOfFameEntryCard({ + required this.entry, + required this.rank, + }); + + final HallOfFameEntry entry; + final int rank; + + @override + Widget build(BuildContext context) { + final rankColor = _getRankColor(rank); + final rankIcon = _getRankIcon(rank); + + return Card( + margin: const EdgeInsets.symmetric(vertical: 4), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + // 순위 표시 + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: rankColor.withValues(alpha: 0.2), + shape: BoxShape.circle, + border: Border.all(color: rankColor, width: 2), + ), + child: Center( + child: rankIcon != null + ? Icon(rankIcon, color: rankColor, size: 20) + : Text( + '$rank', + style: TextStyle( + color: rankColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + const SizedBox(width: 12), + // 캐릭터 정보 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 이름과 레벨 + Row( + children: [ + Expanded( + child: Text( + '"${entry.characterName}"', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + 'Lv.${entry.level}', + style: TextStyle( + color: Colors.blue.shade800, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ), + ], + ), + const SizedBox(height: 4), + // 종족/클래스 + Text( + '${GameDataL10n.getRaceName(context, entry.race)} ' + '${GameDataL10n.getKlassName(context, entry.klass)}', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade700, + ), + ), + const SizedBox(height: 4), + // 통계 + Row( + children: [ + _buildStatChip( + Icons.timer, + entry.formattedPlayTime, + Colors.green, + ), + const SizedBox(width: 8), + _buildStatChip( + Icons.heart_broken, + '${entry.totalDeaths}', + Colors.red, + ), + const SizedBox(width: 8), + _buildStatChip( + Icons.check_circle, + '${entry.questsCompleted}Q', + Colors.orange, + ), + ], + ), + ], + ), + ), + // 클리어 날짜 + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Icon(Icons.calendar_today, size: 14, color: Colors.grey.shade500), + const SizedBox(height: 4), + Text( + entry.formattedClearedDate, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildStatChip(IconData icon, String value, Color color) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 12, color: color), + const SizedBox(width: 2), + Text( + value, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } + + Color _getRankColor(int rank) { + switch (rank) { + case 1: + return Colors.amber.shade700; + case 2: + return Colors.grey.shade500; + case 3: + return Colors.brown.shade400; + default: + return Colors.blue.shade400; + } + } + + IconData? _getRankIcon(int rank) { + switch (rank) { + case 1: + return Icons.emoji_events; + case 2: + return Icons.workspace_premium; + case 3: + return Icons.military_tech; + default: + return null; + } + } +} + +/// 게임 클리어 축하 다이얼로그 +Future showGameClearDialog( + BuildContext context, { + required HallOfFameEntry entry, + required VoidCallback onNewGame, + required VoidCallback onViewHallOfFame, +}) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => _GameClearDialog( + entry: entry, + onNewGame: onNewGame, + onViewHallOfFame: onViewHallOfFame, + ), + ); +} + +/// 게임 클리어 다이얼로그 위젯 +class _GameClearDialog extends StatelessWidget { + const _GameClearDialog({ + required this.entry, + required this.onNewGame, + required this.onViewHallOfFame, + }); + + final HallOfFameEntry entry; + final VoidCallback onNewGame; + final VoidCallback onViewHallOfFame; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.emoji_events, color: Colors.amber, size: 32), + SizedBox(width: 8), + Text('VICTORY!'), + SizedBox(width: 8), + Icon(Icons.emoji_events, color: Colors.amber, size: 32), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'You have defeated the Glitch God!', + style: TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + // 캐릭터 정보 + Text( + '"${entry.characterName}"', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${entry.race} ${entry.klass}', + style: TextStyle(color: Colors.grey.shade600), + ), + const SizedBox(height: 16), + // 통계 + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildStat('Level', '${entry.level}'), + _buildStat('Time', entry.formattedPlayTime), + _buildStat('Deaths', '${entry.totalDeaths}'), + _buildStat('Quests', '${entry.questsCompleted}'), + ], + ), + const SizedBox(height: 16), + const Text( + 'Your legend has been enshrined in the Hall of Fame!', + style: TextStyle( + fontStyle: FontStyle.italic, + color: Colors.amber, + ), + textAlign: TextAlign.center, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + onViewHallOfFame(); + }, + child: const Text('View Hall of Fame'), + ), + FilledButton( + onPressed: () { + Navigator.of(context).pop(); + onNewGame(); + }, + child: const Text('New Game'), + ), + ], + ); + } + + Widget _buildStat(String label, String value) { + return Column( + children: [ + Text( + value, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, + ), + ), + ], + ); + } +}