feat(hall): Phase 10 명예의 전당 시스템 구현
- HallOfFameEntry 모델 및 HallOfFame 컬렉션 추가 - HallOfFameStorage 저장소 (JSON 파일 기반) - HallOfFameScreen UI (순위별 색상/아이콘) - 게임 클리어 시 명예의 전당 등록 처리 - FrontScreen에 명예의 전당 버튼 추가 - 클리어 축하 다이얼로그 구현
This commit is contained in:
460
lib/src/features/hall_of_fame/hall_of_fame_screen.dart
Normal file
460
lib/src/features/hall_of_fame/hall_of_fame_screen.dart
Normal file
@@ -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<HallOfFameScreen> createState() => _HallOfFameScreenState();
|
||||
}
|
||||
|
||||
class _HallOfFameScreenState extends State<HallOfFameScreen> {
|
||||
final HallOfFameStorage _storage = HallOfFameStorage();
|
||||
HallOfFame? _hallOfFame;
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadHallOfFame();
|
||||
}
|
||||
|
||||
Future<void> _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<void> 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user