import 'package:flutter/material.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/src/core/audio/audio_service.dart'; import 'package:asciineverdie/src/core/engine/debug_settings_service.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; import 'package:asciineverdie/src/core/engine/game_mutations.dart'; import 'package:asciineverdie/src/core/engine/progress_service.dart'; import 'package:asciineverdie/src/core/engine/reward_service.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/core/model/pq_config.dart'; import 'package:asciineverdie/src/core/notification/notification_service.dart'; import 'package:asciineverdie/src/core/storage/save_manager.dart'; import 'package:asciineverdie/src/core/storage/save_repository.dart'; import 'package:asciineverdie/src/core/storage/hall_of_fame_storage.dart'; import 'package:asciineverdie/src/core/storage/settings_repository.dart'; import 'package:asciineverdie/src/core/model/hall_of_fame.dart'; import 'package:asciineverdie/src/features/arena/arena_screen.dart'; import 'package:asciineverdie/src/features/front/front_screen.dart'; import 'package:asciineverdie/src/features/front/save_picker_dialog.dart'; import 'package:asciineverdie/src/features/game/game_play_screen.dart'; import 'package:asciineverdie/src/features/game/game_session_controller.dart'; import 'package:asciineverdie/src/features/game/widgets/notification_overlay.dart'; import 'package:asciineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart'; import 'package:asciineverdie/src/features/new_character/new_character_screen.dart'; import 'package:asciineverdie/src/features/settings/settings_screen.dart'; class AskiiNeverDieApp extends StatefulWidget { const AskiiNeverDieApp({super.key}); @override State 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 { late final GameSessionController _controller; late final NotificationService _notificationService; late final SettingsRepository _settingsRepository; late final AudioService _audioService; late final HallOfFameStorage _hallOfFameStorage; final RouteObserver> _routeObserver = RouteObserver>(); bool _isCheckingSave = true; bool _hasSave = false; SavedGamePreview? _savedGamePreview; ThemeMode _themeMode = ThemeMode.system; HallOfFame _hallOfFame = HallOfFame.empty(); @override void initState() { super.initState(); const config = PqConfig(); final mutations = GameMutations(config); final rewards = RewardService(mutations, config); _controller = GameSessionController( progressService: ProgressService( config: config, mutations: mutations, rewards: rewards, ), saveManager: SaveManager(SaveRepository()), ); _notificationService = NotificationService(); _settingsRepository = SettingsRepository(); _audioService = AudioService(settingsRepository: _settingsRepository); _hallOfFameStorage = HallOfFameStorage(); // 초기 설정 및 오디오 서비스 로드 _loadSettings(); _audioService.init(); // 디버그 설정 서비스 초기화 (Phase 8) DebugSettingsService.instance.initialize(); // 세이브 파일 존재 여부 확인 _checkForExistingSave(); // 명예의 전당 로드 _loadHallOfFame(); } /// 명예의 전당 로드 Future _loadHallOfFame() async { final hallOfFame = await _hallOfFameStorage.load(); if (mounted) { setState(() { _hallOfFame = hallOfFame; }); } } /// 저장된 설정 불러오기 Future _loadSettings() async { final themeMode = await _settingsRepository.loadThemeMode(); if (mounted) { setState(() => _themeMode = themeMode); } } /// 테마 모드 변경 void _changeThemeMode(ThemeMode mode) { setState(() => _themeMode = mode); _settingsRepository.saveThemeMode(mode); } /// 세이브 파일 존재 여부 확인 및 미리보기 정보 로드 Future _checkForExistingSave() async { 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) { setState(() { _hasSave = exists; _savedGamePreview = preview; _isCheckingSave = false; }); // 세이브 확인 완료 후 타이틀 BGM 재생 _audioService.playBgm('title'); } } /// 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 void dispose() { _controller.dispose(); _notificationService.dispose(); _audioService.dispose(); super.dispose(); } /// 라이트 테마 (Classic Parchment 스타일) ThemeData get _lightTheme => ThemeData( colorScheme: RetroColors.lightColorScheme, scaffoldBackgroundColor: const Color(0xFFFAF4ED), useMaterial3: true, // 카드/다이얼로그 레트로 배경 cardColor: const Color(0xFFF2E8DC), dialogTheme: const DialogThemeData( backgroundColor: Color(0xFFF2E8DC), titleTextStyle: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFB8860B), ), ), // 앱바 레트로 스타일 appBarTheme: const AppBarTheme( backgroundColor: Color(0xFFF2E8DC), foregroundColor: Color(0xFF1F1F28), titleTextStyle: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFB8860B), ), ), // 버튼 테마 filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFE8DDD0), foregroundColor: const Color(0xFF1F1F28), textStyle: const TextStyle( inherit: false, fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF1F1F28), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFB8860B), side: const BorderSide(color: Color(0xFFB8860B), width: 2), textStyle: const TextStyle( inherit: false, fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFB8860B), ), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: const Color(0xFF4A4458), textStyle: const TextStyle( inherit: false, fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF4A4458), ), ), ), // 텍스트 테마 textTheme: const TextTheme( headlineLarge: TextStyle( fontFamily: 'PressStart2P', fontSize: 20, color: Color(0xFFB8860B), ), headlineMedium: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: Color(0xFFB8860B), ), headlineSmall: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFB8860B), ), titleLarge: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFF1F1F28), ), titleMedium: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF1F1F28), ), titleSmall: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF1F1F28), ), bodyLarge: TextStyle(fontSize: 18, color: Color(0xFF1F1F28)), bodyMedium: TextStyle(fontSize: 17, color: Color(0xFF1F1F28)), bodySmall: TextStyle(fontSize: 15, color: Color(0xFF1F1F28)), labelLarge: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF1F1F28), ), labelMedium: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF1F1F28), ), labelSmall: TextStyle( fontFamily: 'PressStart2P', fontSize: 13, color: Color(0xFF1F1F28), ), ), // 칩 테마 chipTheme: const ChipThemeData( backgroundColor: Color(0xFFE8DDD0), labelStyle: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFF1F1F28), ), side: BorderSide(color: Color(0xFF8B7355)), ), // 리스트 타일 테마 listTileTheme: const ListTileThemeData( textColor: Color(0xFF1F1F28), iconColor: Color(0xFFB8860B), ), // 프로그레스 인디케이터 progressIndicatorTheme: const ProgressIndicatorThemeData( color: Color(0xFFB8860B), linearTrackColor: Color(0xFFD4C4B0), ), ); /// 다크 테마 (Dark Fantasy 스타일) ThemeData get _darkTheme => ThemeData( colorScheme: RetroColors.darkColorScheme, scaffoldBackgroundColor: RetroColors.deepBrown, useMaterial3: true, // 카드/다이얼로그 레트로 배경 cardColor: RetroColors.darkBrown, dialogTheme: const DialogThemeData( backgroundColor: Color(0xFF24283B), titleTextStyle: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFE0AF68), ), ), // 앱바 레트로 스타일 appBarTheme: const AppBarTheme( backgroundColor: Color(0xFF24283B), foregroundColor: Color(0xFFC0CAF5), titleTextStyle: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFE0AF68), ), ), // 버튼 테마 (inherit: false로 애니메이션 lerp 오류 방지) filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: const Color(0xFF3D4260), foregroundColor: const Color(0xFFC0CAF5), textStyle: const TextStyle( inherit: false, fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFFE0AF68), side: const BorderSide(color: Color(0xFFE0AF68), width: 2), textStyle: const TextStyle( inherit: false, fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFE0AF68), ), ), ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( foregroundColor: const Color(0xFFC0CAF5), textStyle: const TextStyle( inherit: false, fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), ), ), // 텍스트 테마 textTheme: const TextTheme( headlineLarge: TextStyle( fontFamily: 'PressStart2P', fontSize: 20, color: Color(0xFFE0AF68), ), headlineMedium: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: Color(0xFFE0AF68), ), headlineSmall: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFE0AF68), ), titleLarge: TextStyle( fontFamily: 'PressStart2P', fontSize: 15, color: Color(0xFFC0CAF5), ), titleMedium: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), titleSmall: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), bodyLarge: TextStyle(fontSize: 18, color: Color(0xFFC0CAF5)), bodyMedium: TextStyle(fontSize: 17, color: Color(0xFFC0CAF5)), bodySmall: TextStyle(fontSize: 15, color: Color(0xFFC0CAF5)), labelLarge: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), labelMedium: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), labelSmall: TextStyle( fontFamily: 'PressStart2P', fontSize: 13, color: Color(0xFFC0CAF5), ), ), // 칩 테마 chipTheme: const ChipThemeData( backgroundColor: Color(0xFF2A2E3F), labelStyle: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: Color(0xFFC0CAF5), ), side: BorderSide(color: Color(0xFF545C7E)), ), // 리스트 타일 테마 listTileTheme: const ListTileThemeData( textColor: Color(0xFFC0CAF5), iconColor: Color(0xFFE0AF68), ), // 프로그레스 인디케이터 progressIndicatorTheme: const ProgressIndicatorThemeData( color: Color(0xFFE0AF68), linearTrackColor: Color(0xFF3B4261), ), ); @override Widget build(BuildContext context) { return MaterialApp( title: 'ASCII NEVER DIE', debugShowCheckedModeBanner: false, localizationsDelegates: L10n.localizationsDelegates, supportedLocales: L10n.supportedLocales, theme: _lightTheme, darkTheme: _darkTheme, themeMode: _themeMode, navigatorObservers: [_routeObserver], builder: (context, child) { // 현재 로케일을 게임 텍스트 l10n 시스템에 동기화 final locale = Localizations.localeOf(context); game_l10n.setGameLocale(locale.languageCode); return child ?? const SizedBox.shrink(); }, home: NotificationOverlay( notificationService: _notificationService, child: _buildHomeScreen(), ), ); } /// 홈 화면 결정: 세이브 확인 중 → 스플래시, 그 외 → 프론트 Widget _buildHomeScreen() { // 세이브 확인 중이면 로딩 스플래시 표시 if (_isCheckingSave) { return const _SplashScreen(); } return FrontScreen( onNewCharacter: _navigateToNewCharacter, onLoadSave: _loadSave, onHallOfFame: _navigateToHallOfFame, onLocalArena: _navigateToArena, onSettings: _showSettings, hasSaveFile: _hasSave, savedGamePreview: _savedGamePreview, hallOfFameCount: _hallOfFame.count, routeObserver: _routeObserver, onRefresh: () { _checkForExistingSave(); _loadHallOfFame(); }, ); } void _navigateToNewCharacter(BuildContext context) { Navigator.of(context) .push( MaterialPageRoute( builder: (context) => NewCharacterScreen( onCharacterCreated: (initialState, {bool testMode = false}) { _startGame(context, initialState, testMode: testMode); }, ), ), ) .then((_) { // 새 게임 후 돌아오면 세이브 정보 및 명예의 전당 갱신 _checkForExistingSave(); _loadHallOfFame(); }); } Future _loadSave(BuildContext context) async { // 저장 파일 목록 조회 final saves = await _controller.saveManager.listSaves(); if (!context.mounted) return; String? selectedFileName; if (saves.isEmpty) { // 저장 파일이 없으면 안내 메시지 _notificationService.showInfo(L10n.of(context).noSavedGames); return; } else if (saves.length == 1) { // 파일이 하나면 바로 선택 selectedFileName = saves.first.fileName; } else { // 여러 개면 다이얼로그 표시 selectedFileName = await SavePickerDialog.show(context, saves); } if (selectedFileName == null || !context.mounted) return; // 선택된 파일 로드 (치트 모드는 저장된 상태에서 복원) await _controller.loadAndStart(fileName: selectedFileName); if (_controller.status == GameSessionStatus.running) { if (context.mounted) { _navigateToGame(context); } } else if (_controller.status == GameSessionStatus.error && context.mounted) { _notificationService.showWarning( L10n.of(context).loadError(_controller.error ?? 'Unknown error'), ); } } Future _startGame( BuildContext context, GameState initialState, { bool testMode = false, }) async { await _controller.startNew(initialState, cheatsEnabled: testMode); if (context.mounted) { // NewCharacterScreen을 pop하고 GamePlayScreen으로 이동 Navigator.of(context).pushReplacement( MaterialPageRoute( builder: (context) => GamePlayScreen( controller: _controller, audioService: _audioService, forceCarouselLayout: testMode, currentThemeMode: _themeMode, onThemeModeChange: _changeThemeMode, ), ), ); } } void _navigateToGame(BuildContext context) { Navigator.of(context) .push( MaterialPageRoute( builder: (context) => GamePlayScreen( controller: _controller, audioService: _audioService, // 디버그 모드로 저장된 게임 로드 시 캐로셀 레이아웃 강제 forceCarouselLayout: _controller.cheatsEnabled, currentThemeMode: _themeMode, onThemeModeChange: _changeThemeMode, ), ), ) .then((_) { // 게임에서 돌아오면 세이브 정보 및 명예의 전당 갱신 _checkForExistingSave(); _loadHallOfFame(); }); } /// Phase 10: 명예의 전당 화면으로 이동 void _navigateToHallOfFame(BuildContext context) { Navigator.of(context) .push( MaterialPageRoute( builder: (context) => const HallOfFameScreen(), ), ) .then((_) { // 명예의 전당에서 돌아오면 명예의 전당 갱신 및 타이틀 BGM 재생 _loadHallOfFame(); _audioService.playBgm('title'); }); } /// 로컬 아레나 화면으로 이동 void _navigateToArena(BuildContext context) { Navigator.of(context) .push( MaterialPageRoute(builder: (context) => const ArenaScreen()), ) .then((_) { // 아레나에서 돌아오면 명예의 전당 다시 로드 및 타이틀 BGM 재생 _loadHallOfFame(); _audioService.playBgm('title'); }); } /// 설정 화면 표시 (모달 바텀시트) void _showSettings(BuildContext context) { SettingsScreen.show( context, settingsRepository: _settingsRepository, currentThemeMode: _themeMode, onThemeModeChange: _changeThemeMode, onBgmVolumeChange: _audioService.setBgmVolume, onSfxVolumeChange: _audioService.setSfxVolume, ); } } /// 스플래시 화면 (세이브 파일 확인 중) - 레트로 스타일 class _SplashScreen extends StatelessWidget { const _SplashScreen(); @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: 22, 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: 16, color: RetroColors.cream, shadows: [ Shadow(color: RetroColors.brown, offset: Offset(1, 1)), ], ), ), ], ), ), const SizedBox(height: 32), // 레트로 로딩 바 SizedBox(width: 160, child: _RetroLoadingBar()), ], ), ), ); } } /// 레트로 스타일 로딩 바 (애니메이션) class _RetroLoadingBar extends StatefulWidget { @override State<_RetroLoadingBar> createState() => _RetroLoadingBarState(); } class _RetroLoadingBarState extends State<_RetroLoadingBar> with SingleTickerProviderStateMixin { late AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 1500), vsync: this, )..repeat(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { const segmentCount = 10; return AnimatedBuilder( animation: _controller, builder: (context, child) { // 웨이브 효과: 각 세그먼트가 순차적으로 켜지고 꺼짐 return Container( height: 16, decoration: BoxDecoration( color: RetroColors.panelBg, border: Border.all(color: RetroColors.panelBorderOuter, width: 2), ), child: Row( children: List.generate(segmentCount, (index) { // 웨이브 패턴 계산 final progress = _controller.value * segmentCount; final distance = (index - progress).abs(); final isLit = distance < 2 || (segmentCount - distance) < 2; final opacity = isLit ? 1.0 : 0.2; return Expanded( child: Container( margin: const EdgeInsets.all(1), decoration: BoxDecoration( color: RetroColors.gold.withValues(alpha: opacity), ), ), ); }), ), ); }, ); } }