refactor(app): 앱 초기화 및 프론트 화면 개선

- app.dart 코드 간소화
- front_screen.dart 기능 확장
This commit is contained in:
JiWoong Sul
2026-01-02 16:52:52 +09:00
parent 86b14427f6
commit afbd4e6853
2 changed files with 93 additions and 149 deletions

View File

@@ -28,6 +28,19 @@ class AskiiNeverDieApp extends StatefulWidget {
State<AskiiNeverDieApp> createState() => _AskiiNeverDieAppState(); State<AskiiNeverDieApp> createState() => _AskiiNeverDieAppState();
} }
/// 저장된 게임 미리보기 정보
class SavedGamePreview {
const SavedGamePreview({
required this.characterName,
required this.level,
required this.actName,
});
final String characterName;
final int level;
final String actName;
}
class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> { class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
late final GameSessionController _controller; late final GameSessionController _controller;
late final NotificationService _notificationService; late final NotificationService _notificationService;
@@ -35,6 +48,7 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
late final AudioService _audioService; late final AudioService _audioService;
bool _isCheckingSave = true; bool _isCheckingSave = true;
bool _hasSave = false; bool _hasSave = false;
SavedGamePreview? _savedGamePreview;
ThemeMode _themeMode = ThemeMode.system; ThemeMode _themeMode = ThemeMode.system;
@override @override
@@ -77,17 +91,46 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
_settingsRepository.saveThemeMode(mode); _settingsRepository.saveThemeMode(mode);
} }
/// 세이브 파일 존재 여부 확인 후 자동 로드 /// 세이브 파일 존재 여부 확인 및 미리보기 정보 로드
Future<void> _checkForExistingSave() async { Future<void> _checkForExistingSave() async {
final exists = await _controller.saveManager.saveExists(); final exists = await _controller.saveManager.saveExists();
SavedGamePreview? preview;
if (exists) {
// 세이브 파일에서 미리보기 정보 추출
final (outcome, state) = await _controller.saveManager.loadState();
if (outcome.success && state != null) {
final actName = _getActName(state.progress.plotStageCount);
preview = SavedGamePreview(
characterName: state.traits.name,
level: state.traits.level,
actName: actName,
);
}
}
if (mounted) { if (mounted) {
setState(() { setState(() {
_hasSave = exists; _hasSave = exists;
_savedGamePreview = preview;
_isCheckingSave = false; _isCheckingSave = false;
}); });
} }
} }
/// plotStageCount를 Act 이름으로 변환
String _getActName(int plotStageCount) {
return switch (plotStageCount) {
1 => 'Prologue',
2 => 'Act I',
3 => 'Act II',
4 => 'Act III',
5 => 'Act IV',
6 => 'Act V',
_ => 'Act V',
};
}
@override @override
void dispose() { void dispose() {
_controller.dispose(); _controller.dispose();
@@ -387,34 +430,21 @@ class _AskiiNeverDieAppState extends State<AskiiNeverDieApp> {
); );
} }
/// 홈 화면 결정: 세이브 확인 중 → 스플래시, 세이브 있음 → 자동 로드, 없음 → 프론트 /// 홈 화면 결정: 세이브 확인 중 → 스플래시, 그 외 → 프론트
Widget _buildHomeScreen() { Widget _buildHomeScreen() {
// 세이브 확인 중이면 로딩 스플래시 표시 // 세이브 확인 중이면 로딩 스플래시 표시
if (_isCheckingSave) { if (_isCheckingSave) {
return const _SplashScreen(); return const _SplashScreen();
} }
// 세이브 파일이 있으면 자동 로드 화면 // 항상 프론트 화면 표시 (타이틀 BGM 재생)
if (_hasSave) {
return _AutoLoadScreen(
controller: _controller,
onLoadFailed: () {
// 로드 실패 시 프론트 화면으로
setState(() => _hasSave = false);
},
currentThemeMode: _themeMode,
onThemeModeChange: _changeThemeMode,
audioService: _audioService,
);
}
// 세이브 파일이 없으면 기존 프론트 화면 (타이틀 BGM 재생)
_audioService.playBgm('title'); _audioService.playBgm('title');
return FrontScreen( return FrontScreen(
onNewCharacter: _navigateToNewCharacter, onNewCharacter: _navigateToNewCharacter,
onLoadSave: _loadSave, onLoadSave: _loadSave,
onHallOfFame: _navigateToHallOfFame, onHallOfFame: _navigateToHallOfFame,
hasSaveFile: _hasSave, hasSaveFile: _hasSave,
savedGamePreview: _savedGamePreview,
); );
} }
@@ -588,138 +618,6 @@ class _SplashScreen extends StatelessWidget {
} }
} }
/// 자동 로드 화면 (세이브 파일 자동 로드) - 레트로 스타일
class _AutoLoadScreen extends StatefulWidget {
const _AutoLoadScreen({
required this.controller,
required this.onLoadFailed,
required this.currentThemeMode,
required this.onThemeModeChange,
this.audioService,
});
final GameSessionController controller;
final VoidCallback onLoadFailed;
final ThemeMode currentThemeMode;
final void Function(ThemeMode mode) onThemeModeChange;
final AudioService? audioService;
@override
State<_AutoLoadScreen> createState() => _AutoLoadScreenState();
}
class _AutoLoadScreenState extends State<_AutoLoadScreen> {
@override
void initState() {
super.initState();
// 로딩 중에도 타이틀 BGM 재생
widget.audioService?.playBgm('title');
_autoLoad();
}
Future<void> _autoLoad() async {
await widget.controller.loadAndStart(cheatsEnabled: false);
if (!mounted) return;
if (widget.controller.status == GameSessionStatus.running) {
// 로드 성공 → 게임 화면으로 교체
Navigator.of(context).pushReplacement(
MaterialPageRoute<void>(
builder: (context) => GamePlayScreen(
controller: widget.controller,
audioService: widget.audioService,
currentThemeMode: widget.currentThemeMode,
onThemeModeChange: widget.onThemeModeChange,
),
),
);
} else {
// 로드 실패 → 프론트 화면으로 돌아가기
widget.onLoadFailed();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: RetroColors.deepBrown,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 타이틀 로고
Container(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
decoration: BoxDecoration(
color: RetroColors.panelBg,
border: Border.all(color: RetroColors.gold, width: 3),
),
child: Column(
children: [
// 아이콘
const Icon(
Icons.auto_awesome,
size: 32,
color: RetroColors.gold,
),
const SizedBox(height: 12),
// 타이틀
const Text(
'ASCII',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 20,
color: RetroColors.gold,
shadows: [
Shadow(
color: RetroColors.goldDark,
offset: Offset(2, 2),
),
],
),
),
const SizedBox(height: 4),
const Text(
'NEVER DIE',
style: TextStyle(
fontFamily: 'PressStart2P',
fontSize: 14,
color: RetroColors.cream,
shadows: [
Shadow(
color: RetroColors.brown,
offset: Offset(1, 1),
),
],
),
),
],
),
),
const SizedBox(height: 32),
// 레트로 로딩 바
SizedBox(
width: 160,
child: _RetroLoadingBar(),
),
const SizedBox(height: 16),
// 로딩 텍스트
Text(
game_l10n.uiLoading,
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 8,
color: RetroColors.textDisabled,
),
),
],
),
),
);
}
}
/// 레트로 스타일 로딩 바 (애니메이션) /// 레트로 스타일 로딩 바 (애니메이션)
class _RetroLoadingBar extends StatefulWidget { class _RetroLoadingBar extends StatefulWidget {
@override @override

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/l10n/app_localizations.dart';
import 'package:asciineverdie/src/app.dart' show SavedGamePreview;
import 'package:asciineverdie/src/features/front/widgets/hero_vs_boss_animation.dart'; import 'package:asciineverdie/src/features/front/widgets/hero_vs_boss_animation.dart';
import 'package:asciineverdie/src/shared/retro_colors.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart';
import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart'; import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart';
@@ -13,6 +14,7 @@ class FrontScreen extends StatelessWidget {
this.onLoadSave, this.onLoadSave,
this.onHallOfFame, this.onHallOfFame,
this.hasSaveFile = false, this.hasSaveFile = false,
this.savedGamePreview,
}); });
/// "New character" 버튼 클릭 시 호출 /// "New character" 버튼 클릭 시 호출
@@ -27,6 +29,9 @@ class FrontScreen extends StatelessWidget {
/// 세이브 파일 존재 여부 (새 캐릭터 시 경고용) /// 세이브 파일 존재 여부 (새 캐릭터 시 경고용)
final bool hasSaveFile; final bool hasSaveFile;
/// 저장된 게임 미리보기 정보
final SavedGamePreview? savedGamePreview;
/// 새 캐릭터 생성 시 세이브 파일 존재하면 경고 표시 /// 새 캐릭터 생성 시 세이브 파일 존재하면 경고 표시
void _handleNewCharacter(BuildContext context) { void _handleNewCharacter(BuildContext context) {
if (hasSaveFile) { if (hasSaveFile) {
@@ -91,6 +96,7 @@ class FrontScreen extends StatelessWidget {
onHallOfFame: onHallOfFame != null onHallOfFame: onHallOfFame != null
? () => onHallOfFame!(context) ? () => onHallOfFame!(context)
: null, : null,
savedGamePreview: savedGamePreview,
), ),
], ],
), ),
@@ -181,11 +187,13 @@ class _ActionButtons extends StatelessWidget {
this.onNewCharacter, this.onNewCharacter,
this.onLoadSave, this.onLoadSave,
this.onHallOfFame, this.onHallOfFame,
this.savedGamePreview,
}); });
final VoidCallback? onNewCharacter; final VoidCallback? onNewCharacter;
final VoidCallback? onLoadSave; final VoidCallback? onLoadSave;
final VoidCallback? onHallOfFame; final VoidCallback? onHallOfFame;
final SavedGamePreview? savedGamePreview;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -211,6 +219,11 @@ class _ActionButtons extends StatelessWidget {
onPressed: onLoadSave, onPressed: onLoadSave,
isPrimary: false, isPrimary: false,
), ),
// 저장된 게임 정보 표시
if (savedGamePreview != null) ...[
const SizedBox(height: 6),
_SavedGameInfo(preview: savedGamePreview!),
],
const SizedBox(height: 12), const SizedBox(height: 12),
// 명예의 전당 // 명예의 전당
if (onHallOfFame != null) if (onHallOfFame != null)
@@ -226,6 +239,39 @@ class _ActionButtons extends StatelessWidget {
} }
} }
/// 저장된 게임 미리보기 정보 위젯
class _SavedGameInfo extends StatelessWidget {
const _SavedGameInfo({required this.preview});
final SavedGamePreview preview;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.person_outline,
size: 10,
color: RetroColors.textDisabled,
),
const SizedBox(width: 4),
Text(
'${preview.characterName} Lv.${preview.level} ${preview.actName}',
style: const TextStyle(
fontFamily: 'PressStart2P',
fontSize: 7,
color: RetroColors.textDisabled,
),
),
],
),
);
}
}
/// 카피라이트 푸터 /// 카피라이트 푸터
class _CopyrightFooter extends StatelessWidget { class _CopyrightFooter extends StatelessWidget {
const _CopyrightFooter(); const _CopyrightFooter();