import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:asciineverdie/src/core/notification/notification_service.dart'; import 'package:asciineverdie/data/game_text_l10n.dart' as l10n; import 'package:asciineverdie/l10n/app_localizations.dart'; import 'package:asciineverdie/src/core/animation/ascii_animation_type.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; import 'package:asciineverdie/src/features/game/pages/character_sheet_page.dart'; import 'package:asciineverdie/src/features/game/pages/combat_log_page.dart'; import 'package:asciineverdie/src/features/game/pages/equipment_page.dart'; import 'package:asciineverdie/src/features/game/pages/inventory_page.dart'; import 'package:asciineverdie/src/features/game/pages/quest_page.dart'; import 'package:asciineverdie/src/features/game/pages/skills_page.dart'; import 'package:asciineverdie/src/features/game/pages/story_page.dart'; import 'package:asciineverdie/src/features/game/widgets/carousel_nav_bar.dart'; import 'package:asciineverdie/src/features/game/widgets/combat_log.dart'; import 'package:asciineverdie/src/features/game/widgets/enhanced_animation_panel.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; /// 모바일 캐로셀 레이아웃 /// /// 모바일 앱용 레이아웃: /// - 상단: 확장 애니메이션 패널 (ASCII 애니메이션, HP/MP, 버프, 몬스터 HP) /// - 중앙: 캐로셀 (7개 페이지: 스킬, 인벤토리, 장비, 캐릭터시트, 전투로그, 스토리, 퀘스트) /// - 하단: 네비게이션 바 (7개 버튼) class MobileCarouselLayout extends StatefulWidget { const MobileCarouselLayout({ super.key, required this.state, required this.combatLogEntries, required this.speedMultiplier, required this.onSpeedCycle, required this.isPaused, required this.onPauseToggle, required this.onSave, required this.onExit, required this.notificationService, required this.onLanguageChange, required this.onDeleteSaveAndNewGame, this.specialAnimation, this.currentThemeMode = ThemeMode.system, this.onThemeModeChange, this.bgmVolume = 0.7, this.sfxVolume = 0.8, this.onBgmVolumeChange, this.onSfxVolumeChange, this.onShowStatistics, this.onShowHelp, this.cheatsEnabled = false, this.onCheatTask, this.onCheatQuest, this.onCheatPlot, this.onCreateTestCharacter, }); final GameState state; final List combatLogEntries; final int speedMultiplier; final VoidCallback onSpeedCycle; final bool isPaused; final VoidCallback onPauseToggle; final VoidCallback onSave; final VoidCallback onExit; final NotificationService notificationService; final void Function(String locale) onLanguageChange; final VoidCallback onDeleteSaveAndNewGame; final AsciiAnimationType? specialAnimation; final ThemeMode currentThemeMode; final void Function(ThemeMode mode)? onThemeModeChange; /// BGM 볼륨 (0.0 ~ 1.0) final double bgmVolume; /// SFX 볼륨 (0.0 ~ 1.0) final double sfxVolume; /// BGM 볼륨 변경 콜백 final void Function(double volume)? onBgmVolumeChange; /// SFX 볼륨 변경 콜백 final void Function(double volume)? onSfxVolumeChange; /// 통계 표시 콜백 final VoidCallback? onShowStatistics; /// 도움말 표시 콜백 final VoidCallback? onShowHelp; /// 치트 모드 활성화 여부 final bool cheatsEnabled; /// 치트: 태스크 완료 final VoidCallback? onCheatTask; /// 치트: 퀘스트 완료 final VoidCallback? onCheatQuest; /// 치트: 액트(플롯) 완료 final VoidCallback? onCheatPlot; /// 테스트 캐릭터 생성 콜백 (디버그 모드 전용) final Future Function()? onCreateTestCharacter; @override State createState() => _MobileCarouselLayoutState(); } class _MobileCarouselLayoutState extends State { late PageController _pageController; int _currentPage = CarouselPage.character.index; // 기본: 캐릭터시트 @override void initState() { super.initState(); _pageController = PageController(initialPage: _currentPage); } @override void dispose() { _pageController.dispose(); super.dispose(); } void _onPageChanged(int page) { setState(() { _currentPage = page; }); } void _onNavPageSelected(int page) { _pageController.animateToPage( page, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); } /// 현재 언어명 가져오기 String _getCurrentLanguageName() { final locale = l10n.currentGameLocale; if (locale == 'ko') return l10n.languageKorean; if (locale == 'ja') return l10n.languageJapanese; return l10n.languageEnglish; } /// 현재 테마명 가져오기 String _getCurrentThemeName() { return switch (widget.currentThemeMode) { ThemeMode.light => l10n.themeLight, ThemeMode.dark => l10n.themeDark, ThemeMode.system => l10n.themeSystem, }; } /// 테마 아이콘 가져오기 IconData _getThemeIcon() { return switch (widget.currentThemeMode) { ThemeMode.light => Icons.light_mode, ThemeMode.dark => Icons.dark_mode, ThemeMode.system => Icons.brightness_auto, }; } /// 테마 선택 다이얼로그 표시 void _showThemeDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.menuTheme), content: Column( mainAxisSize: MainAxisSize.min, children: [ _buildThemeOption(context, ThemeMode.system, l10n.themeSystem), _buildThemeOption(context, ThemeMode.light, l10n.themeLight), _buildThemeOption(context, ThemeMode.dark, l10n.themeDark), ], ), ), ); } Widget _buildThemeOption(BuildContext context, ThemeMode mode, String label) { final isSelected = widget.currentThemeMode == mode; return ListTile( leading: Icon( isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: isSelected ? Theme.of(context).colorScheme.primary : null, ), title: Text( label, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), onTap: () { Navigator.pop(context); // 다이얼로그 닫기 widget.onThemeModeChange?.call(mode); }, ); } /// 언어 선택 다이얼로그 표시 void _showLanguageDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.menuLanguage), content: Column( mainAxisSize: MainAxisSize.min, children: [ _buildLanguageOption(context, 'en', l10n.languageEnglish), _buildLanguageOption(context, 'ko', l10n.languageKorean), _buildLanguageOption(context, 'ja', l10n.languageJapanese), ], ), ), ); } Widget _buildLanguageOption( BuildContext context, String locale, String label, ) { final isSelected = l10n.currentGameLocale == locale; return ListTile( leading: Icon( isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: isSelected ? Theme.of(context).colorScheme.primary : null, ), title: Text( label, style: TextStyle( fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, ), ), onTap: () { Navigator.pop(context); // 다이얼로그 닫기 widget.onLanguageChange(locale); }, ); } /// 사운드 상태 텍스트 가져오기 String _getSoundStatus() { final bgmPercent = (widget.bgmVolume * 100).round(); final sfxPercent = (widget.sfxVolume * 100).round(); if (bgmPercent == 0 && sfxPercent == 0) { return l10n.uiSoundOff; } return 'BGM $bgmPercent% / SFX $sfxPercent%'; } /// 사운드 설정 다이얼로그 표시 void _showSoundDialog(BuildContext context) { // StatefulBuilder를 사용하여 다이얼로그 내 상태 관리 var bgmVolume = widget.bgmVolume; var sfxVolume = widget.sfxVolume; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => AlertDialog( title: Text(l10n.uiSound), content: Column( mainAxisSize: MainAxisSize.min, children: [ // BGM 볼륨 Row( children: [ Icon( bgmVolume == 0 ? Icons.music_off : Icons.music_note, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.uiBgmVolume), Text('${(bgmVolume * 100).round()}%'), ], ), Slider( value: bgmVolume, onChanged: (value) { setDialogState(() => bgmVolume = value); widget.onBgmVolumeChange?.call(value); }, divisions: 10, ), ], ), ), ], ), const SizedBox(height: 8), // SFX 볼륨 Row( children: [ Icon( sfxVolume == 0 ? Icons.volume_off : Icons.volume_up, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l10n.uiSfxVolume), Text('${(sfxVolume * 100).round()}%'), ], ), Slider( value: sfxVolume, onChanged: (value) { setDialogState(() => sfxVolume = value); widget.onSfxVolumeChange?.call(value); }, divisions: 10, ), ], ), ), ], ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.buttonConfirm), ), ], ), ), ); } /// 세이브 삭제 확인 다이얼로그 표시 void _showDeleteConfirmDialog(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( title: Text(l10n.confirmDeleteTitle), content: Text(l10n.confirmDeleteMessage), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(l10n.buttonCancel), ), TextButton( onPressed: () { Navigator.pop(context); // 다이얼로그 닫기 widget.onDeleteSaveAndNewGame(); }, style: TextButton.styleFrom(foregroundColor: Colors.red), child: Text(l10n.buttonConfirm), ), ], ), ); } /// 테스트 캐릭터 생성 확인 다이얼로그 Future _showTestCharacterDialog(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Create Test Character?'), content: const Text( '현재 캐릭터가 레벨 100으로 변환되어 명예의 전당에 등록됩니다.\n\n' '⚠️ 현재 세이브 파일이 삭제됩니다.\n' '이 작업은 되돌릴 수 없습니다.', ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.error, foregroundColor: Theme.of(context).colorScheme.onError, ), child: const Text('Create'), ), ], ), ); if (confirmed == true && mounted) { await widget.onCreateTestCharacter?.call(); } } /// 옵션 메뉴 표시 void _showOptionsMenu(BuildContext context) { final localizations = L10n.of(context); final panelBg = RetroColors.panelBgOf(context); final gold = RetroColors.goldOf(context); final surface = RetroColors.surfaceOf(context); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: panelBg, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.7, ), builder: (context) => SafeArea( child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ // 헤더 (레트로 스타일) Container( padding: const EdgeInsets.all(16), width: double.infinity, decoration: BoxDecoration( color: surface, border: Border(bottom: BorderSide(color: gold, width: 2)), ), child: Text( 'OPTIONS', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 12, color: gold, ), ), ), // 일시정지/재개 ListTile( leading: Icon( widget.isPaused ? Icons.play_arrow : Icons.pause, color: widget.isPaused ? Colors.green : Colors.orange, ), title: Text(widget.isPaused ? l10n.menuResume : l10n.menuPause), onTap: () { Navigator.pop(context); widget.onPauseToggle(); }, ), // 속도 조절 ListTile( leading: const Icon(Icons.speed), title: Text(l10n.menuSpeed), trailing: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 4, ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(12), ), child: Text( '${widget.speedMultiplier}x', style: TextStyle( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.primary, ), ), ), onTap: () { widget.onSpeedCycle(); Navigator.pop(context); }, ), const Divider(), // 통계 if (widget.onShowStatistics != null) ListTile( leading: const Icon(Icons.bar_chart, color: Colors.blue), title: Text(l10n.uiStatistics), onTap: () { Navigator.pop(context); widget.onShowStatistics?.call(); }, ), // 도움말 if (widget.onShowHelp != null) ListTile( leading: const Icon(Icons.help_outline, color: Colors.green), title: Text(l10n.uiHelp), onTap: () { Navigator.pop(context); widget.onShowHelp?.call(); }, ), const Divider(), // 언어 변경 ListTile( leading: const Icon(Icons.language, color: Colors.teal), title: Text(l10n.menuLanguage), trailing: Text( _getCurrentLanguageName(), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), ), onTap: () { Navigator.pop(context); _showLanguageDialog(context); }, ), // 테마 변경 if (widget.onThemeModeChange != null) ListTile( leading: Icon(_getThemeIcon(), color: Colors.purple), title: Text(l10n.menuTheme), trailing: Text( _getCurrentThemeName(), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), ), onTap: () { Navigator.pop(context); _showThemeDialog(context); }, ), // 사운드 설정 if (widget.onBgmVolumeChange != null || widget.onSfxVolumeChange != null) ListTile( leading: Icon( widget.bgmVolume == 0 && widget.sfxVolume == 0 ? Icons.volume_off : Icons.volume_up, color: Colors.indigo, ), title: Text(l10n.uiSound), trailing: Text( _getSoundStatus(), style: TextStyle( color: Theme.of(context).colorScheme.primary, ), ), onTap: () { Navigator.pop(context); _showSoundDialog(context); }, ), const Divider(), // 저장 ListTile( leading: const Icon(Icons.save, color: Colors.blue), title: Text(l10n.menuSave), onTap: () { Navigator.pop(context); widget.onSave(); widget.notificationService.showGameSaved(l10n.menuSaved); }, ), // 새로하기 (세이브 삭제) ListTile( leading: const Icon(Icons.refresh, color: Colors.orange), title: Text(l10n.menuNewGame), subtitle: Text( l10n.menuDeleteSave, style: TextStyle( fontSize: 12, color: Theme.of(context).colorScheme.outline, ), ), onTap: () { Navigator.pop(context); _showDeleteConfirmDialog(context); }, ), // 종료 ListTile( leading: const Icon(Icons.exit_to_app, color: Colors.red), title: Text(localizations.exitGame), onTap: () { Navigator.pop(context); widget.onExit(); }, ), // 치트 섹션 (디버그 모드에서만 표시) if (widget.cheatsEnabled) ...[ const Divider(), Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Text( 'DEBUG CHEATS', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 8, color: Colors.red.shade300, ), ), ), ListTile( leading: const Icon(Icons.fast_forward, color: Colors.red), title: const Text('Skip Task (L+1)'), subtitle: const Text('태스크 즉시 완료'), onTap: () { Navigator.pop(context); widget.onCheatTask?.call(); }, ), ListTile( leading: const Icon(Icons.skip_next, color: Colors.red), title: const Text('Skip Quest (Q!)'), subtitle: const Text('퀘스트 즉시 완료'), onTap: () { Navigator.pop(context); widget.onCheatQuest?.call(); }, ), ListTile( leading: const Icon(Icons.double_arrow, color: Colors.red), title: const Text('Skip Act (P!)'), subtitle: const Text('액트 즉시 완료 (명예의 전당 테스트용)'), onTap: () { Navigator.pop(context); widget.onCheatPlot?.call(); }, ), ], // 디버그 도구 섹션 (kDebugMode에서만 표시) if (kDebugMode && widget.onCreateTestCharacter != null) ...[ const Divider(), Padding( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), child: Text( 'DEBUG TOOLS', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 8, color: Colors.orange.shade300, ), ), ), ListTile( leading: const Icon(Icons.science, color: Colors.orange), title: const Text('Create Test Character'), subtitle: const Text('레벨 100 캐릭터를 명예의 전당에 등록'), onTap: () { Navigator.pop(context); _showTestCharacterDialog(context); }, ), ], const SizedBox(height: 8), ], ), ), ), ); } @override Widget build(BuildContext context) { final state = widget.state; final background = RetroColors.backgroundOf(context); final panelBg = RetroColors.panelBgOf(context); final gold = RetroColors.goldOf(context); return Scaffold( backgroundColor: background, appBar: AppBar( backgroundColor: panelBg, title: Text( L10n.of(context).progressQuestTitle(state.traits.name), style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: gold, ), ), actions: [ // 옵션 버튼 IconButton( icon: Icon(Icons.settings, color: gold), onPressed: () => _showOptionsMenu(context), tooltip: l10n.menuOptions, ), ], ), body: SafeArea( top: false, // AppBar가 상단 처리 child: Column( children: [ // 상단: 확장 애니메이션 패널 EnhancedAnimationPanel( progress: state.progress, stats: state.stats, skillSystem: state.skillSystem, speedMultiplier: widget.speedMultiplier, onSpeedCycle: widget.onSpeedCycle, isPaused: widget.isPaused, onPauseToggle: widget.onPauseToggle, specialAnimation: widget.specialAnimation, weaponName: state.equipment.weapon, shieldName: state.equipment.shield, characterLevel: state.traits.level, monsterLevel: state.progress.currentTask.monsterLevel, monsterGrade: state.progress.currentTask.monsterGrade, latestCombatEvent: state.progress.currentCombat?.recentEvents.lastOrNull, raceId: state.traits.raceId, weaponRarity: state.equipment.weaponItem.rarity, ), // 중앙: 캐로셀 (PageView) Expanded( child: PageView( controller: _pageController, onPageChanged: _onPageChanged, children: [ // 0: 스킬 SkillsPage( skillBook: state.skillBook, skillSystem: state.skillSystem, ), // 1: 인벤토리 InventoryPage( inventory: state.inventory, potionInventory: state.potionInventory, encumbrance: state.progress.encumbrance, usedPotionTypes: state.progress.currentCombat?.usedPotionTypes ?? const {}, ), // 2: 장비 EquipmentPage(equipment: state.equipment), // 3: 캐릭터시트 (기본) CharacterSheetPage( traits: state.traits, stats: state.stats, exp: state.progress.exp, ), // 4: 전투로그 CombatLogPage(entries: widget.combatLogEntries), // 5: 퀘스트 QuestPage( questHistory: state.progress.questHistory, quest: state.progress.quest, ), // 6: 스토리 StoryPage( plotStageCount: state.progress.plotStageCount, plot: state.progress.plot, ), ], ), ), // 하단: 네비게이션 바 CarouselNavBar( currentPage: _currentPage, onPageSelected: _onNavPageSelected, ), ], ), ), ); } }