From 448f500ca02d4c75af49ebd4a06e66ecef98e392 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Mon, 12 Jan 2026 16:17:25 +0900 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20=EA=B8=B0=ED=83=80=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FrontScreen, HallOfFameScreen 개선 - NewCharacterScreen, SettingsScreen 정리 - App 초기화 로직 정리 --- lib/src/app.dart | 102 +++---- lib/src/features/front/front_screen.dart | 14 +- .../front/widgets/hero_vs_boss_animation.dart | 13 +- .../hall_of_fame/hall_of_fame_screen.dart | 252 +++++++++++++++--- .../new_character/new_character_screen.dart | 1 - .../new_character/widgets/race_preview.dart | 9 +- .../features/settings/settings_screen.dart | 27 +- 7 files changed, 287 insertions(+), 131 deletions(-) diff --git a/lib/src/app.dart b/lib/src/app.dart index 70c702c..768ce61 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -469,18 +469,20 @@ class _AskiiNeverDieAppState extends State { } void _navigateToNewCharacter(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => NewCharacterScreen( - onCharacterCreated: (initialState, {bool testMode = false}) { - _startGame(context, initialState, testMode: testMode); - }, - ), - ), - ).then((_) { - // 새 게임 후 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생) - _checkForExistingSave(); - }); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => NewCharacterScreen( + onCharacterCreated: (initialState, {bool testMode = false}) { + _startGame(context, initialState, testMode: testMode); + }, + ), + ), + ) + .then((_) { + // 새 게임 후 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생) + _checkForExistingSave(); + }); } Future _loadSave(BuildContext context) async { @@ -544,44 +546,50 @@ class _AskiiNeverDieAppState extends State { } void _navigateToGame(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => GamePlayScreen( - controller: _controller, - audioService: _audioService, - // 디버그 모드로 저장된 게임 로드 시 캐로셀 레이아웃 강제 - forceCarouselLayout: _controller.cheatsEnabled, - currentThemeMode: _themeMode, - onThemeModeChange: _changeThemeMode, - ), - ), - ).then((_) { - // 게임에서 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생) - _checkForExistingSave(); - }); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => GamePlayScreen( + controller: _controller, + audioService: _audioService, + // 디버그 모드로 저장된 게임 로드 시 캐로셀 레이아웃 강제 + forceCarouselLayout: _controller.cheatsEnabled, + currentThemeMode: _themeMode, + onThemeModeChange: _changeThemeMode, + ), + ), + ) + .then((_) { + // 게임에서 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생) + _checkForExistingSave(); + }); } /// Phase 10: 명예의 전당 화면으로 이동 void _navigateToHallOfFame(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => const HallOfFameScreen()), - ).then((_) { - // 명예의 전당에서 돌아오면 타이틀 BGM 재생 - _audioService.playBgm('title'); - }); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => const HallOfFameScreen(), + ), + ) + .then((_) { + // 명예의 전당에서 돌아오면 타이틀 BGM 재생 + _audioService.playBgm('title'); + }); } /// 로컬 아레나 화면으로 이동 void _navigateToArena(BuildContext context) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const ArenaScreen(), - ), - ).then((_) { - // 아레나에서 돌아오면 명예의 전당 다시 로드 및 타이틀 BGM 재생 - _loadHallOfFame(); - _audioService.playBgm('title'); - }); + Navigator.of(context) + .push( + MaterialPageRoute(builder: (context) => const ArenaScreen()), + ) + .then((_) { + // 아레나에서 돌아오면 명예의 전당 다시 로드 및 타이틀 BGM 재생 + _loadHallOfFame(); + _audioService.playBgm('title'); + }); } } @@ -636,10 +644,7 @@ class _SplashScreen extends StatelessWidget { fontSize: 14, color: RetroColors.cream, shadows: [ - Shadow( - color: RetroColors.brown, - offset: Offset(1, 1), - ), + Shadow(color: RetroColors.brown, offset: Offset(1, 1)), ], ), ), @@ -648,10 +653,7 @@ class _SplashScreen extends StatelessWidget { ), const SizedBox(height: 32), // 레트로 로딩 바 - SizedBox( - width: 160, - child: _RetroLoadingBar(), - ), + SizedBox(width: 160, child: _RetroLoadingBar()), ], ), ), diff --git a/lib/src/features/front/front_screen.dart b/lib/src/features/front/front_screen.dart index 44fb14c..43db9f2 100644 --- a/lib/src/features/front/front_screen.dart +++ b/lib/src/features/front/front_screen.dart @@ -107,8 +107,8 @@ class FrontScreen extends StatelessWidget { onHallOfFame: onHallOfFame != null ? () => onHallOfFame!(context) : null, - onLocalArena: onLocalArena != null && - hallOfFameCount >= 2 + onLocalArena: + onLocalArena != null && hallOfFameCount >= 2 ? () => onLocalArena!(context) : null, savedGamePreview: savedGamePreview, @@ -153,10 +153,7 @@ class _RetroHeader extends StatelessWidget { fontSize: 14, color: RetroColors.gold, shadows: [ - Shadow( - color: RetroColors.goldDark, - offset: Offset(2, 2), - ), + Shadow(color: RetroColors.goldDark, offset: Offset(2, 2)), ], ), ), @@ -169,7 +166,10 @@ class _RetroHeader extends StatelessWidget { spacing: 8, runSpacing: 8, children: [ - _RetroTag(icon: Icons.cloud_off_outlined, label: l10n.tagNoNetwork), + _RetroTag( + icon: Icons.cloud_off_outlined, + label: l10n.tagNoNetwork, + ), _RetroTag(icon: Icons.timer_outlined, label: l10n.tagIdleRpg), _RetroTag(icon: Icons.storage_rounded, label: l10n.tagLocalSaves), ], diff --git a/lib/src/features/front/widgets/hero_vs_boss_animation.dart b/lib/src/features/front/widgets/hero_vs_boss_animation.dart index 7c76e32..764b694 100644 --- a/lib/src/features/front/widgets/hero_vs_boss_animation.dart +++ b/lib/src/features/front/widgets/hero_vs_boss_animation.dart @@ -159,9 +159,7 @@ class _HeroVsBossAnimationState extends State { @override Widget build(BuildContext context) { - final frame = _applyGlitchEffect( - frontScreenAnimationFrames[_currentFrame], - ); + final frame = _applyGlitchEffect(frontScreenAnimationFrames[_currentFrame]); // 현재 종족 이름 (UI 표시용) final raceName = RaceData.findById(_currentRaceId)?.name ?? 'Hero'; @@ -172,10 +170,7 @@ class _HeroVsBossAnimationState extends State { // 항상 검은 배경 color: Colors.black, borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Colors.white24, - width: 1, - ), + border: Border.all(color: Colors.white24, width: 1), // 은은한 글로우 효과 boxShadow: [ BoxShadow( @@ -192,9 +187,7 @@ class _HeroVsBossAnimationState extends State { Flexible( child: FittedBox( fit: BoxFit.scaleDown, - child: RichText( - text: _buildColoredTextSpan(frame), - ), + child: RichText(text: _buildColoredTextSpan(frame)), ), ), const SizedBox(height: 8), 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 index be6a66f..1e1ce4b 100644 --- a/lib/src/features/hall_of_fame/hall_of_fame_screen.dart +++ b/lib/src/features/hall_of_fame/hall_of_fame_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; @@ -136,9 +137,7 @@ class _HallOfFameScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: goldColor.withValues(alpha: 0.2), - border: Border( - bottom: BorderSide(color: goldColor, width: 2), - ), + border: Border(bottom: BorderSide(color: goldColor, width: 2)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -166,7 +165,11 @@ class _HallOfFameScreenState extends State { itemCount: hallOfFame.entries.length, itemBuilder: (context, index) { final entry = hallOfFame.entries[index]; - return _HallOfFameEntryCard(entry: entry, rank: index + 1); + return _HallOfFameEntryCard( + entry: entry, + rank: index + 1, + onDeleteRequest: () => _confirmDelete(entry), + ); }, ), ), @@ -174,14 +177,103 @@ class _HallOfFameScreenState extends State { ), ); } + + /// 삭제 확인 다이얼로그 (디버그 모드 전용) + Future _confirmDelete(HallOfFameEntry entry) async { + final confirmed = await showDialog( + context: context, + builder: (context) => RetroDialog( + title: l10n.uiConfirm.toUpperCase(), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${entry.characterName} (Lv.${entry.level})\n' + '${l10n.uiConfirmDelete}', + style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 7), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () => Navigator.pop(context, false), + child: Text(l10n.uiCancel), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: RetroColors.hpOf(context), + ), + onPressed: () => Navigator.pop(context, true), + child: Text(l10n.uiDelete), + ), + ], + ), + ], + ), + ), + ), + ); + + if (confirmed == true && mounted) { + await _deleteEntry(entry.id); + } + } + + /// 엔트리 삭제 실행 + Future _deleteEntry(String id) async { + final success = await _storage.deleteEntry(id); + + if (!mounted) return; + + if (success) { + // 성공 시 목록 새로고침 + await _loadHallOfFame(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + l10n.uiDeleted, + style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 6), + ), + backgroundColor: RetroColors.mpOf(context), + duration: const Duration(seconds: 2), + ), + ); + } + } else { + // 실패 시 에러 메시지 + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + l10n.uiError, + style: const TextStyle(fontFamily: 'PressStart2P', fontSize: 6), + ), + backgroundColor: RetroColors.hpOf(context), + duration: const Duration(seconds: 2), + ), + ); + } + } + } } /// 명예의 전당 엔트리 카드 class _HallOfFameEntryCard extends StatelessWidget { - const _HallOfFameEntryCard({required this.entry, required this.rank}); + const _HallOfFameEntryCard({ + required this.entry, + required this.rank, + required this.onDeleteRequest, + }); final HallOfFameEntry entry; final int rank; + final VoidCallback onDeleteRequest; void _showDetailDialog(BuildContext context) { showDialog( @@ -259,8 +351,9 @@ class _HallOfFameEntryCard extends StatelessWidget { vertical: 2, ), decoration: BoxDecoration( - color: RetroColors.mpOf(context) - .withValues(alpha: 0.2), + color: RetroColors.mpOf( + context, + ).withValues(alpha: 0.2), border: Border.all( color: RetroColors.mpOf(context), width: 1, @@ -337,6 +430,36 @@ class _HallOfFameEntryCard extends StatelessWidget { ), ], ), + // 삭제 버튼 (디버그 모드 전용) + if (kDebugMode) ...[ + const SizedBox(width: 8), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + // 이벤트 전파 중지 (카드 클릭 방지) + onDeleteRequest(); + }, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: RetroColors.hpOf( + context, + ).withValues(alpha: 0.2), + border: Border.all( + color: RetroColors.hpOf(context), + width: 1, + ), + ), + child: Icon( + Icons.delete_outline, + size: 16, + color: RetroColors.hpOf(context), + ), + ), + ), + ), + ], ], ), ), @@ -459,9 +582,7 @@ class _GameClearDialog extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( color: goldColor.withValues(alpha: 0.2), - border: Border( - bottom: BorderSide(color: goldColor, width: 2), - ), + border: Border(bottom: BorderSide(color: goldColor, width: 2)), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -496,10 +617,7 @@ class _GameClearDialog extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 16), - Container( - height: 2, - color: borderColor, - ), + Container(height: 2, color: borderColor), const SizedBox(height: 16), // 캐릭터 정보 Text( @@ -528,9 +646,21 @@ class _GameClearDialog extends StatelessWidget { alignment: WrapAlignment.center, children: [ _buildStat(context, l10n.hofLevel, '${entry.level}'), - _buildStat(context, l10n.hofTime, entry.formattedPlayTime), - _buildStat(context, l10n.hofDeaths, '${entry.totalDeaths}'), - _buildStat(context, l10n.hofQuests, '${entry.questsCompleted}'), + _buildStat( + context, + l10n.hofTime, + entry.formattedPlayTime, + ), + _buildStat( + context, + l10n.hofDeaths, + '${entry.totalDeaths}', + ), + _buildStat( + context, + l10n.hofQuests, + '${entry.questsCompleted}', + ), ], ), const SizedBox(height: 16), @@ -817,7 +947,12 @@ class _HallOfFameDetailDialog extends StatelessWidget { spacing: 8, runSpacing: 6, children: [ - _buildStatItem(context, Icons.timer, l10n.hofTime, entry.formattedPlayTime), + _buildStatItem( + context, + Icons.timer, + l10n.hofTime, + entry.formattedPlayTime, + ), _buildStatItem( context, Icons.pest_control, @@ -860,7 +995,11 @@ class _HallOfFameDetailDialog extends StatelessWidget { _buildCombatStatChip(l10n.statStr, '${stats.str}', Colors.red), _buildCombatStatChip(l10n.statCon, '${stats.con}', Colors.orange), _buildCombatStatChip(l10n.statDex, '${stats.dex}', Colors.green), - _buildCombatStatChip(l10n.statInt, '${stats.intelligence}', Colors.blue), + _buildCombatStatChip( + l10n.statInt, + '${stats.intelligence}', + Colors.blue, + ), _buildCombatStatChip(l10n.statWis, '${stats.wis}', Colors.purple), _buildCombatStatChip(l10n.statCha, '${stats.cha}', Colors.pink), ], @@ -873,8 +1012,16 @@ class _HallOfFameDetailDialog extends StatelessWidget { spacing: 6, runSpacing: 4, children: [ - _buildCombatStatChip(l10n.statAtk, '${stats.atk}', Colors.red.shade700), - _buildCombatStatChip(l10n.statMAtk, '${stats.magAtk}', Colors.blue.shade700), + _buildCombatStatChip( + l10n.statAtk, + '${stats.atk}', + Colors.red.shade700, + ), + _buildCombatStatChip( + l10n.statMAtk, + '${stats.magAtk}', + Colors.blue.shade700, + ), _buildCombatStatChip( l10n.statCri, '${(stats.criRate * 100).toStringAsFixed(1)}%', @@ -889,7 +1036,11 @@ class _HallOfFameDetailDialog extends StatelessWidget { runSpacing: 4, children: [ _buildCombatStatChip(l10n.statDef, '${stats.def}', Colors.brown), - _buildCombatStatChip(l10n.statMDef, '${stats.magDef}', Colors.indigo), + _buildCombatStatChip( + l10n.statMDef, + '${stats.magDef}', + Colors.indigo, + ), _buildCombatStatChip( l10n.statEva, '${(stats.evasion * 100).toStringAsFixed(1)}%', @@ -908,8 +1059,16 @@ class _HallOfFameDetailDialog extends StatelessWidget { spacing: 6, runSpacing: 4, children: [ - _buildCombatStatChip(l10n.statHp, '${stats.hpMax}', Colors.red.shade400), - _buildCombatStatChip(l10n.statMp, '${stats.mpMax}', Colors.blue.shade400), + _buildCombatStatChip( + l10n.statHp, + '${stats.hpMax}', + Colors.red.shade400, + ), + _buildCombatStatChip( + l10n.statMp, + '${stats.mpMax}', + Colors.blue.shade400, + ), ], ), ], @@ -1021,7 +1180,10 @@ class _HallOfFameDetailDialog extends StatelessWidget { padding: const EdgeInsets.all(6), decoration: BoxDecoration( color: rarityColor.withValues(alpha: 0.1), - border: Border.all(color: rarityColor.withValues(alpha: 0.3), width: 1), + border: Border.all( + color: rarityColor.withValues(alpha: 0.3), + width: 1, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1076,11 +1238,7 @@ class _HallOfFameDetailDialog extends StatelessWidget { // 스탯 요약 if (statSummary.isNotEmpty) ...[ const SizedBox(height: 4), - Wrap( - spacing: 6, - runSpacing: 2, - children: statSummary, - ), + Wrap(spacing: 6, runSpacing: 2, children: statSummary), ], ], ), @@ -1180,16 +1338,32 @@ class _HallOfFameDetailDialog extends StatelessWidget { // 확률 스탯 if (stats.criRate > 0) { - addStat(l10n.statCri, '+${(stats.criRate * 100).toStringAsFixed(0)}%', Colors.amber); + addStat( + l10n.statCri, + '+${(stats.criRate * 100).toStringAsFixed(0)}%', + Colors.amber, + ); } if (stats.blockRate > 0) { - addStat(l10n.statBlock, '+${(stats.blockRate * 100).toStringAsFixed(0)}%', Colors.blueGrey); + addStat( + l10n.statBlock, + '+${(stats.blockRate * 100).toStringAsFixed(0)}%', + Colors.blueGrey, + ); } if (stats.evasion > 0) { - addStat(l10n.statEva, '+${(stats.evasion * 100).toStringAsFixed(0)}%', Colors.teal); + addStat( + l10n.statEva, + '+${(stats.evasion * 100).toStringAsFixed(0)}%', + Colors.teal, + ); } if (stats.parryRate > 0) { - addStat(l10n.statParry, '+${(stats.parryRate * 100).toStringAsFixed(0)}%', Colors.cyan); + addStat( + l10n.statParry, + '+${(stats.parryRate * 100).toStringAsFixed(0)}%', + Colors.cyan, + ); } // 보너스 스탯 @@ -1227,10 +1401,7 @@ class _HallOfFameDetailDialog extends StatelessWidget { padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: RetroColors.backgroundOf(context), - border: Border.all( - color: RetroColors.borderOf(context), - width: 1, - ), + border: Border.all(color: RetroColors.borderOf(context), width: 1), ), child: Text( l10n.hofNoSkills.toUpperCase(), @@ -1255,7 +1426,10 @@ class _HallOfFameDetailDialog extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: skillColor.withValues(alpha: 0.1), - border: Border.all(color: skillColor.withValues(alpha: 0.3), width: 1), + border: Border.all( + color: skillColor.withValues(alpha: 0.3), + width: 1, + ), ), child: Text( '$translatedName $rank', diff --git a/lib/src/features/new_character/new_character_screen.dart b/lib/src/features/new_character/new_character_screen.dart index 901e75a..17e83bd 100644 --- a/lib/src/features/new_character/new_character_screen.dart +++ b/lib/src/features/new_character/new_character_screen.dart @@ -784,5 +784,4 @@ class _NewCharacterScreenState extends State { ClassPassiveType.firstStrikeBonus => passive.description, }; } - } diff --git a/lib/src/features/new_character/widgets/race_preview.dart b/lib/src/features/new_character/widgets/race_preview.dart index cf64c10..238a8ee 100644 --- a/lib/src/features/new_character/widgets/race_preview.dart +++ b/lib/src/features/new_character/widgets/race_preview.dart @@ -11,10 +11,7 @@ import 'package:asciineverdie/src/core/animation/race_character_frames.dart'; /// 새 캐릭터 생성 화면에서 선택한 종족의 idle 애니메이션을 보여줌. /// RichText 기반 색상 적용. class RacePreview extends StatefulWidget { - const RacePreview({ - super.key, - required this.raceId, - }); + const RacePreview({super.key, required this.raceId}); /// 종족 ID (예: "byte_human", "kernel_giant") final String raceId; @@ -121,9 +118,7 @@ class _RacePreviewState extends State { ); } - return RichText( - text: TextSpan(children: spans), - ); + return RichText(text: TextSpan(children: spans)); } /// 문자별 색상 매핑 diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/settings_screen.dart index 02db2bb..65c6425 100644 --- a/lib/src/features/settings/settings_screen.dart +++ b/lib/src/features/settings/settings_screen.dart @@ -122,10 +122,7 @@ class _SettingsScreenState extends State { children: [ Icon(Icons.settings, color: theme.colorScheme.primary), const SizedBox(width: 8), - Text( - game_l10n.uiSettings, - style: theme.textTheme.titleLarge, - ), + Text(game_l10n.uiSettings, style: theme.textTheme.titleLarge), const Spacer(), IconButton( icon: const Icon(Icons.close), @@ -196,9 +193,9 @@ class _SettingsScreenState extends State { padding: const EdgeInsets.only(bottom: 8), child: Text( title, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), ), ); } @@ -292,7 +289,10 @@ class _SettingsScreenState extends State { leading: Text(lang.$3, style: const TextStyle(fontSize: 24)), title: Text(lang.$2), trailing: isSelected - ? Icon(Icons.check, color: Theme.of(context).colorScheme.primary) + ? Icon( + Icons.check, + color: Theme.of(context).colorScheme.primary, + ) : null, onTap: () { game_l10n.setGameLocale(lang.$1); @@ -331,16 +331,9 @@ class _SettingsScreenState extends State { children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(label), - Text('$percentage%'), - ], - ), - Slider( - value: value, - onChanged: onChanged, - divisions: 10, + children: [Text(label), Text('$percentage%')], ), + Slider(value: value, onChanged: onChanged, divisions: 10), ], ), ),