refactor(ui): 기타 화면 정리

- FrontScreen, HallOfFameScreen 개선
- NewCharacterScreen, SettingsScreen 정리
- App 초기화 로직 정리
This commit is contained in:
JiWoong Sul
2026-01-12 16:17:25 +09:00
parent cbbbbba1a5
commit 448f500ca0
7 changed files with 287 additions and 131 deletions

View File

@@ -469,18 +469,20 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
}
void _navigateToNewCharacter(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => NewCharacterScreen(
onCharacterCreated: (initialState, {bool testMode = false}) {
_startGame(context, initialState, testMode: testMode);
},
),
),
).then((_) {
// 새 게임 후 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생)
_checkForExistingSave();
});
Navigator.of(context)
.push(
MaterialPageRoute<void>(
builder: (context) => NewCharacterScreen(
onCharacterCreated: (initialState, {bool testMode = false}) {
_startGame(context, initialState, testMode: testMode);
},
),
),
)
.then((_) {
// 새 게임 후 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생)
_checkForExistingSave();
});
}
Future<void> _loadSave(BuildContext context) async {
@@ -544,44 +546,50 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
}
void _navigateToGame(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => GamePlayScreen(
controller: _controller,
audioService: _audioService,
// 디버그 모드로 저장된 게임 로드 시 캐로셀 레이아웃 강제
forceCarouselLayout: _controller.cheatsEnabled,
currentThemeMode: _themeMode,
onThemeModeChange: _changeThemeMode,
),
),
).then((_) {
// 게임에서 돌아오면 세이브 정보 갱신 (BGM은 _checkForExistingSave에서 재생)
_checkForExistingSave();
});
Navigator.of(context)
.push(
MaterialPageRoute<void>(
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<void>(builder: (context) => const HallOfFameScreen()),
).then((_) {
// 명예의 전당에서 돌아오면 타이틀 BGM 재생
_audioService.playBgm('title');
});
Navigator.of(context)
.push(
MaterialPageRoute<void>(
builder: (context) => const HallOfFameScreen(),
),
)
.then((_) {
// 명예의 전당에서 돌아오면 타이틀 BGM 재생
_audioService.playBgm('title');
});
}
/// 로컬 아레나 화면으로 이동
void _navigateToArena(BuildContext context) {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (context) => const ArenaScreen(),
),
).then((_) {
// 아레나에서 돌아오면 명예의 전당 다시 로드 및 타이틀 BGM 재생
_loadHallOfFame();
_audioService.playBgm('title');
});
Navigator.of(context)
.push(
MaterialPageRoute<void>(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()),
],
),
),

View File

@@ -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),
],

View File

@@ -159,9 +159,7 @@ class _HeroVsBossAnimationState extends State<HeroVsBossAnimation> {
@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<HeroVsBossAnimation> {
// 항상 검은 배경
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<HeroVsBossAnimation> {
Flexible(
child: FittedBox(
fit: BoxFit.scaleDown,
child: RichText(
text: _buildColoredTextSpan(frame),
),
child: RichText(text: _buildColoredTextSpan(frame)),
),
),
const SizedBox(height: 8),

View File

@@ -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<HallOfFameScreen> {
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<HallOfFameScreen> {
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<HallOfFameScreen> {
),
);
}
/// 삭제 확인 다이얼로그 (디버그 모드 전용)
Future<void> _confirmDelete(HallOfFameEntry entry) async {
final confirmed = await showDialog<bool>(
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<void> _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<void>(
@@ -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',

View File

@@ -784,5 +784,4 @@ class _NewCharacterScreenState extends State<NewCharacterScreen> {
ClassPassiveType.firstStrikeBonus => passive.description,
};
}
}

View File

@@ -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<RacePreview> {
);
}
return RichText(
text: TextSpan(children: spans),
);
return RichText(text: TextSpan(children: spans));
}
/// 문자별 색상 매핑

View File

@@ -122,10 +122,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
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<SettingsScreen> {
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),
],
),
),