feat(hall-of-fame): 명예의 전당 상세 보기 및 스펠북 기록 추가

- HallOfFameEntry에 finalSpells 필드 추가 (스펠 이름 + 랭크)
- 명예의 전당 카드 클릭 시 상세 정보 다이얼로그 표시
- 디버그 모드에서 샘플 엔트리 자동 생성 (테스트용)
- pq_logic 및 progress 관련 minor 수정
This commit is contained in:
JiWoong Sul
2025-12-24 16:33:13 +09:00
parent 7219f58853
commit c1db1fd5d3
6 changed files with 333 additions and 19 deletions

View File

@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:askiineverdie/data/game_text_l10n.dart' as l10n;
@@ -25,7 +26,13 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
}
Future<void> _loadHallOfFame() async {
final hallOfFame = await _storage.load();
var hallOfFame = await _storage.load();
// 디버그 모드일 때 샘플 엔트리 추가 (빈 경우에만)
if (kDebugMode && hallOfFame.isEmpty) {
hallOfFame = hallOfFame.addEntry(_createDebugSampleEntry());
}
if (mounted) {
setState(() {
_hallOfFame = hallOfFame;
@@ -134,6 +141,43 @@ class _HallOfFameScreenState extends State<HallOfFameScreen> {
}
}
/// 디버그 모드 샘플 엔트리 생성 (kDebugMode에서만 사용)
HallOfFameEntry _createDebugSampleEntry() {
return HallOfFameEntry(
id: 'debug_sample_001',
characterName: 'Debug Hero',
race: 'byte_human',
klass: 'loop_wizard',
level: 100,
totalPlayTimeMs: 10 * 60 * 60 * 1000, // 10시간
totalDeaths: 3,
monstersKilled: 1234,
questsCompleted: 42,
clearedAt: DateTime.now(),
finalEquipment: {
'weapon': '+15 Legendary Debugger',
'shield': '+10 Exception Shield',
'helm': '+8 Null Pointer Helm',
'hauberk': '+12 Thread-Safe Armor',
'brassairts': '+6 Memory Guard',
'vambraces': '+5 Stack Overflow Band',
'gauntlets': '+7 Syntax Checker Gloves',
'gambeson': '+9 Buffer Padding',
'cuisses': '+4 Runtime Protector',
'greaves': '+6 Compile Time Shin',
'sollerets': '+5 Binary Boots',
},
finalSpells: [
{'name': 'Recursive Thunder', 'rank': 'XII'},
{'name': 'Async Heal', 'rank': 'VIII'},
{'name': 'Memory Leak Curse', 'rank': 'X'},
{'name': 'Stack Overflow', 'rank': 'VI'},
{'name': 'Null Pointer Strike', 'rank': 'IX'},
{'name': 'Thread Lock', 'rank': 'VII'},
],
);
}
/// 명예의 전당 엔트리 카드
class _HallOfFameEntryCard extends StatelessWidget {
const _HallOfFameEntryCard({required this.entry, required this.rank});
@@ -141,6 +185,13 @@ class _HallOfFameEntryCard extends StatelessWidget {
final HallOfFameEntry entry;
final int rank;
void _showDetailDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => _HallOfFameDetailDialog(entry: entry),
);
}
@override
Widget build(BuildContext context) {
final rankColor = _getRankColor(rank);
@@ -148,10 +199,13 @@ class _HallOfFameEntryCard extends StatelessWidget {
return Card(
margin: const EdgeInsets.symmetric(vertical: 4),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
child: InkWell(
onTap: () => _showDetailDialog(context),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
// 순위 표시
Container(
width: 40,
@@ -265,7 +319,8 @@ class _HallOfFameEntryCard extends StatelessWidget {
],
),
),
);
),
);
}
Widget _buildStatChip(IconData icon, String value, Color color) {
@@ -433,3 +488,233 @@ class _GameClearDialog extends StatelessWidget {
);
}
}
/// 명예의 전당 상세 정보 다이얼로그
class _HallOfFameDetailDialog extends StatelessWidget {
const _HallOfFameDetailDialog({required this.entry});
final HallOfFameEntry entry;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Column(
children: [
Text(
'"${entry.characterName}"',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
'${GameDataL10n.getRaceName(context, entry.race)} '
'${GameDataL10n.getKlassName(context, entry.klass)} - '
'${l10n.uiLevel(entry.level)}',
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
),
],
),
content: SizedBox(
width: double.maxFinite,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 통계 섹션
_buildSection(
icon: Icons.analytics,
title: 'Statistics',
child: _buildStatsGrid(),
),
const SizedBox(height: 16),
// 장비 섹션
if (entry.finalEquipment != null) ...[
_buildSection(
icon: Icons.shield,
title: 'Equipment',
child: _buildEquipmentList(),
),
const SizedBox(height: 16),
],
// 스펠 섹션
if (entry.finalSpells != null && entry.finalSpells!.isNotEmpty)
_buildSection(
icon: Icons.auto_fix_high,
title: 'Spells',
child: _buildSpellList(),
),
],
),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
Widget _buildSection({
required IconData icon,
required String title,
required Widget child,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 18, color: Colors.amber.shade700),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.amber.shade700,
),
),
],
),
const SizedBox(height: 8),
child,
],
);
}
Widget _buildStatsGrid() {
return Wrap(
spacing: 16,
runSpacing: 8,
children: [
_buildStatItem(Icons.timer, l10n.hofTime, entry.formattedPlayTime),
_buildStatItem(
Icons.pest_control,
'Monsters',
'${entry.monstersKilled}',
),
_buildStatItem(
Icons.heart_broken,
l10n.hofDeaths,
'${entry.totalDeaths}',
),
_buildStatItem(
Icons.check_circle,
l10n.hofQuests,
'${entry.questsCompleted}',
),
_buildStatItem(
Icons.calendar_today,
'Cleared',
entry.formattedClearedDate,
),
],
);
}
Widget _buildStatItem(IconData icon, String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: Colors.grey.shade600),
const SizedBox(width: 6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 13,
),
),
Text(
label,
style: TextStyle(fontSize: 10, color: Colors.grey.shade600),
),
],
),
],
),
);
}
Widget _buildEquipmentList() {
final equipment = entry.finalEquipment!;
final slots = [
('weapon', Icons.gavel, 'Weapon'),
('shield', Icons.shield, 'Shield'),
('helm', Icons.sports_mma, 'Helm'),
('hauberk', Icons.checkroom, 'Hauberk'),
('brassairts', Icons.front_hand, 'Brassairts'),
('vambraces', Icons.back_hand, 'Vambraces'),
('gauntlets', Icons.sports_handball, 'Gauntlets'),
('gambeson', Icons.dry_cleaning, 'Gambeson'),
('cuisses', Icons.airline_seat_legroom_normal, 'Cuisses'),
('greaves', Icons.snowshoeing, 'Greaves'),
('sollerets', Icons.do_not_step, 'Sollerets'),
];
return Column(
children: slots.map((slot) {
final (key, icon, label) = slot;
final value = equipment[key] ?? '-';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Icon(icon, size: 16, color: Colors.grey.shade500),
const SizedBox(width: 8),
SizedBox(
width: 80,
child: Text(
label,
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
),
),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}).toList(),
);
}
Widget _buildSpellList() {
final spells = entry.finalSpells!;
return Wrap(
spacing: 8,
runSpacing: 4,
children: spells.map((spell) {
final name = spell['name'] ?? '';
final rank = spell['rank'] ?? '';
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.purple.shade200),
),
child: Text(
'$name $rank',
style: TextStyle(fontSize: 12, color: Colors.purple.shade700),
),
);
}).toList(),
);
}
}