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'; import 'package:asciineverdie/src/shared/widgets/retro_widgets.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.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, this.autoReviveEndMs, this.speedBoostEndMs, this.isPaidUser = false, this.onSpeedBoostActivate, this.onSetSpeed, this.adSpeedMultiplier = 5, this.has2xUnlocked = false, }); final GameState state; final List combatLogEntries; final int speedMultiplier; final VoidCallback onSpeedCycle; /// 특정 속도로 직접 설정 (옵션 메뉴용) final void Function(int speed)? onSetSpeed; 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; /// 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; /// 자동부활 버프 종료 시점 (elapsedMs 기준) final int? autoReviveEndMs; /// 5배속 버프 종료 시점 (elapsedMs 기준) final int? speedBoostEndMs; /// 유료 유저 여부 final bool isPaidUser; /// 광고 배속 활성화 콜백 (광고 시청) final VoidCallback? onSpeedBoostActivate; /// 광고 배속 배율 (릴리즈: 5x, 디버그빌드+디버그모드: 20x) final int adSpeedMultiplier; /// 2x 배속 해금 여부 (명예의 전당에 캐릭터가 있으면 true) final bool has2xUnlocked; @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; } /// 언어 선택 다이얼로그 표시 void _showLanguageDialog(BuildContext context) { showDialog( context: context, builder: (context) => _RetroSelectDialog( title: l10n.menuLanguage.toUpperCase(), children: [ _buildLanguageOption(context, 'en', l10n.languageEnglish, '🇺🇸'), _buildLanguageOption(context, 'ko', l10n.languageKorean, '🇰🇷'), _buildLanguageOption(context, 'ja', l10n.languageJapanese, '🇯🇵'), ], ), ); } Widget _buildLanguageOption( BuildContext context, String locale, String label, String flag, ) { final isSelected = l10n.currentGameLocale == locale; return _RetroOptionItem( label: label.toUpperCase(), prefix: flag, isSelected: isSelected, 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) { var bgmVolume = widget.bgmVolume; var sfxVolume = widget.sfxVolume; showDialog( context: context, builder: (context) => StatefulBuilder( builder: (context, setDialogState) => _RetroSoundDialog( bgmVolume: bgmVolume, sfxVolume: sfxVolume, onBgmChanged: (double value) { setDialogState(() => bgmVolume = value); widget.onBgmVolumeChange?.call(value); }, onSfxChanged: (double value) { setDialogState(() => sfxVolume = value); widget.onSfxVolumeChange?.call(value); }, ), ), ); } /// 세이브 삭제 확인 다이얼로그 표시 void _showDeleteConfirmDialog(BuildContext context) { showDialog( context: context, builder: (context) => _RetroConfirmDialog( title: l10n.confirmDeleteTitle.toUpperCase(), message: l10n.confirmDeleteMessage, confirmText: l10n.buttonConfirm.toUpperCase(), cancelText: l10n.buttonCancel.toUpperCase(), onConfirm: () { Navigator.pop(context); widget.onDeleteSaveAndNewGame(); }, onCancel: () => Navigator.pop(context), ), ); } /// 테스트 캐릭터 생성 확인 다이얼로그 Future _showTestCharacterDialog(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => _RetroConfirmDialog( title: 'CREATE TEST CHARACTER?', message: '현재 캐릭터가 레벨 100으로 변환되어\n' '명예의 전당에 등록됩니다.\n\n' '⚠️ 현재 세이브 파일이 삭제됩니다.\n' '이 작업은 되돌릴 수 없습니다.', confirmText: 'CREATE', cancelText: 'CANCEL', onConfirm: () => Navigator.of(context).pop(true), onCancel: () => Navigator.of(context).pop(false), ), ); if (confirmed == true && mounted) { await widget.onCreateTestCharacter?.call(); } } /// 옵션 메뉴 표시 void _showOptionsMenu(BuildContext context) { final localizations = L10n.of(context); final background = RetroColors.backgroundOf(context); final gold = RetroColors.goldOf(context); final border = RetroColors.borderOf(context); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.75, ), builder: (context) => Container( decoration: BoxDecoration( color: background, border: Border.all(color: border, width: 2), borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), ), child: SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ // 핸들 바 Padding( padding: const EdgeInsets.only(top: 8, bottom: 4), child: Container( width: 60, height: 4, color: border, ), ), // 헤더 Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), width: double.infinity, decoration: BoxDecoration( color: RetroColors.panelBgOf(context), border: Border(bottom: BorderSide(color: gold, width: 2)), ), child: Row( children: [ Icon(Icons.settings, color: gold, size: 18), const SizedBox(width: 8), Text( 'OPTIONS', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: gold, ), ), const Spacer(), RetroIconButton( icon: Icons.close, onPressed: () => Navigator.pop(context), size: 28, ), ], ), ), // 메뉴 목록 Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // === 게임 제어 === const _RetroMenuSection(title: 'CONTROL'), const SizedBox(height: 8), // 일시정지/재개 _RetroMenuItem( icon: widget.isPaused ? Icons.play_arrow : Icons.pause, iconColor: widget.isPaused ? RetroColors.expOf(context) : RetroColors.warningOf(context), label: widget.isPaused ? l10n.menuResume.toUpperCase() : l10n.menuPause.toUpperCase(), onTap: () { Navigator.pop(context); widget.onPauseToggle(); }, ), const SizedBox(height: 8), // 속도 조절 _RetroMenuItem( icon: Icons.speed, iconColor: gold, label: l10n.menuSpeed.toUpperCase(), trailing: _buildRetroSpeedSelector(context), ), const SizedBox(height: 16), // === 정보 === const _RetroMenuSection(title: 'INFO'), const SizedBox(height: 8), if (widget.onShowStatistics != null) _RetroMenuItem( icon: Icons.bar_chart, iconColor: RetroColors.mpOf(context), label: l10n.uiStatistics.toUpperCase(), onTap: () { Navigator.pop(context); widget.onShowStatistics?.call(); }, ), if (widget.onShowHelp != null) ...[ const SizedBox(height: 8), _RetroMenuItem( icon: Icons.help_outline, iconColor: RetroColors.expOf(context), label: l10n.uiHelp.toUpperCase(), onTap: () { Navigator.pop(context); widget.onShowHelp?.call(); }, ), ], const SizedBox(height: 16), // === 설정 === const _RetroMenuSection(title: 'SETTINGS'), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.language, iconColor: RetroColors.mpOf(context), label: l10n.menuLanguage.toUpperCase(), value: _getCurrentLanguageName(), onTap: () { Navigator.pop(context); _showLanguageDialog(context); }, ), if (widget.onBgmVolumeChange != null || widget.onSfxVolumeChange != null) ...[ const SizedBox(height: 8), _RetroMenuItem( icon: widget.bgmVolume == 0 && widget.sfxVolume == 0 ? Icons.volume_off : Icons.volume_up, iconColor: RetroColors.textMutedOf(context), label: l10n.uiSound.toUpperCase(), value: _getSoundStatus(), onTap: () { Navigator.pop(context); _showSoundDialog(context); }, ), ], const SizedBox(height: 16), // === 저장/종료 === const _RetroMenuSection(title: 'SAVE / EXIT'), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.save, iconColor: RetroColors.mpOf(context), label: l10n.menuSave.toUpperCase(), onTap: () { Navigator.pop(context); widget.onSave(); widget.notificationService.showGameSaved( l10n.menuSaved, ); }, ), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.refresh, iconColor: RetroColors.warningOf(context), label: l10n.menuNewGame.toUpperCase(), subtitle: l10n.menuDeleteSave, onTap: () { Navigator.pop(context); _showDeleteConfirmDialog(context); }, ), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.exit_to_app, iconColor: RetroColors.hpOf(context), label: localizations.exitGame.toUpperCase(), onTap: () { Navigator.pop(context); widget.onExit(); }, ), // === 치트 섹션 (디버그 모드에서만) === if (widget.cheatsEnabled) ...[ const SizedBox(height: 16), _RetroMenuSection( title: 'DEBUG CHEATS', color: RetroColors.hpOf(context), ), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.fast_forward, iconColor: RetroColors.hpOf(context), label: 'SKIP TASK (L+1)', subtitle: '태스크 즉시 완료', onTap: () { Navigator.pop(context); widget.onCheatTask?.call(); }, ), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.skip_next, iconColor: RetroColors.hpOf(context), label: 'SKIP QUEST (Q!)', subtitle: '퀘스트 즉시 완료', onTap: () { Navigator.pop(context); widget.onCheatQuest?.call(); }, ), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.double_arrow, iconColor: RetroColors.hpOf(context), label: 'SKIP ACT (P!)', subtitle: '액트 즉시 완료', onTap: () { Navigator.pop(context); widget.onCheatPlot?.call(); }, ), ], // === 디버그 도구 섹션 === if (kDebugMode && widget.onCreateTestCharacter != null) ...[ const SizedBox(height: 16), _RetroMenuSection( title: 'DEBUG TOOLS', color: RetroColors.warningOf(context), ), const SizedBox(height: 8), _RetroMenuItem( icon: Icons.science, iconColor: RetroColors.warningOf(context), label: 'CREATE TEST CHARACTER', subtitle: '레벨 100 캐릭터를 명예의 전당에 등록', onTap: () { Navigator.pop(context); _showTestCharacterDialog(context); }, ), ], const SizedBox(height: 16), ], ), ), ), ], ), ), ), ); } /// 레트로 스타일 속도 선택기 Widget _buildRetroSpeedSelector(BuildContext context) { final currentElapsedMs = widget.state.skillSystem.elapsedMs; final speedBoostEndMs = widget.speedBoostEndMs ?? 0; final isSpeedBoostActive = speedBoostEndMs > currentElapsedMs || widget.isPaidUser; final adSpeed = widget.adSpeedMultiplier; void setSpeed(int speed) { if (widget.onSetSpeed != null) { widget.onSetSpeed!(speed); } else { widget.onSpeedCycle(); } } return Row( mainAxisSize: MainAxisSize.min, children: [ // 1x 버튼 _RetroSpeedChip( speed: 1, isSelected: widget.speedMultiplier == 1 && !isSpeedBoostActive, onTap: () { setSpeed(1); Navigator.pop(context); }, ), // 2x 버튼 (명예의 전당 해금 시) if (widget.has2xUnlocked) ...[ const SizedBox(width: 4), _RetroSpeedChip( speed: 2, isSelected: widget.speedMultiplier == 2 && !isSpeedBoostActive, onTap: () { setSpeed(2); Navigator.pop(context); }, ), ], const SizedBox(width: 4), // 광고배속 버튼 _RetroSpeedChip( speed: adSpeed, isSelected: isSpeedBoostActive, isAdBased: !isSpeedBoostActive && !widget.isPaidUser, onTap: () { if (!isSpeedBoostActive) { widget.onSpeedBoostActivate?.call(); } Navigator.pop(context); }, ), ], ); } @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: 14, 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, monsterSize: state.progress.currentTask.monsterSize, latestCombatEvent: state.progress.currentCombat?.recentEvents.lastOrNull, raceId: state.traits.raceId, weaponRarity: state.equipment.weaponItem.rarity, autoReviveEndMs: widget.autoReviveEndMs, speedBoostEndMs: widget.speedBoostEndMs, isPaidUser: widget.isPaidUser, onSpeedBoostActivate: widget.onSpeedBoostActivate, adSpeedMultiplier: widget.adSpeedMultiplier, ), // 중앙: 캐로셀 (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, ), // 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, ), ], ), ), ); } } // ═══════════════════════════════════════════════════════════════════════════ // 레트로 스타일 옵션 메뉴 위젯들 // ═══════════════════════════════════════════════════════════════════════════ /// 메뉴 섹션 타이틀 class _RetroMenuSection extends StatelessWidget { const _RetroMenuSection({required this.title, this.color}); final String title; final Color? color; @override Widget build(BuildContext context) { final gold = color ?? RetroColors.goldOf(context); return Row( children: [ Container(width: 4, height: 14, color: gold), const SizedBox(width: 8), Text( title, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 9, color: gold, letterSpacing: 1, ), ), ], ); } } /// 메뉴 아이템 class _RetroMenuItem extends StatelessWidget { const _RetroMenuItem({ required this.icon, required this.iconColor, required this.label, this.value, this.subtitle, this.trailing, this.onTap, }); final IconData icon; final Color iconColor; final String label; final String? value; final String? subtitle; final Widget? trailing; final VoidCallback? onTap; @override Widget build(BuildContext context) { final border = RetroColors.borderOf(context); final panelBg = RetroColors.panelBgOf(context); return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: panelBg, border: Border.all(color: border, width: 1), ), child: Row( children: [ Icon(icon, size: 16, color: iconColor), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: RetroColors.textPrimaryOf(context), ), ), if (subtitle != null) ...[ const SizedBox(height: 4), Text( subtitle!, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 12, color: RetroColors.textMutedOf(context), ), ), ], ], ), ), if (value != null) Text( value!, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, color: RetroColors.goldOf(context), ), ), if (trailing != null) trailing!, ], ), ), ); } } /// 속도 선택 칩 class _RetroSpeedChip extends StatelessWidget { const _RetroSpeedChip({ required this.speed, required this.isSelected, required this.onTap, this.isAdBased = false, }); final int speed; final bool isSelected; final VoidCallback onTap; final bool isAdBased; @override Widget build(BuildContext context) { final gold = RetroColors.goldOf(context); final warning = RetroColors.warningOf(context); final border = RetroColors.borderOf(context); final Color bgColor; final Color textColor; final Color borderColor; if (isSelected) { bgColor = isAdBased ? warning.withValues(alpha: 0.3) : gold.withValues(alpha: 0.3); textColor = isAdBased ? warning : gold; borderColor = isAdBased ? warning : gold; } else if (isAdBased) { bgColor = Colors.transparent; textColor = warning; borderColor = warning; } else { bgColor = Colors.transparent; textColor = RetroColors.textMutedOf(context); borderColor = border; } return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: bgColor, border: Border.all(color: borderColor, width: isSelected ? 2 : 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (isAdBased && !isSelected) Padding( padding: const EdgeInsets.only(right: 2), child: Text( '▶', style: TextStyle(fontSize: 7, color: warning), ), ), Text( '${speed}x', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 8, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: textColor, ), ), ], ), ), ); } } /// 선택 다이얼로그 class _RetroSelectDialog extends StatelessWidget { const _RetroSelectDialog({required this.title, required this.children}); final String title; final List children; @override Widget build(BuildContext context) { final background = RetroColors.backgroundOf(context); final gold = RetroColors.goldOf(context); return Dialog( backgroundColor: Colors.transparent, child: Container( constraints: const BoxConstraints(maxWidth: 320), decoration: BoxDecoration( color: background, border: Border.all(color: gold, width: 2), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 타이틀 Container( width: double.infinity, padding: const EdgeInsets.all(12), color: gold.withValues(alpha: 0.2), child: Text( title, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: gold, ), textAlign: TextAlign.center, ), ), // 옵션 목록 Padding( padding: const EdgeInsets.all(12), child: Column(children: children), ), ], ), ), ); } } /// 선택 옵션 아이템 class _RetroOptionItem extends StatelessWidget { const _RetroOptionItem({ required this.label, required this.isSelected, required this.onTap, this.prefix, }); final String label; final bool isSelected; final VoidCallback onTap; final String? prefix; @override Widget build(BuildContext context) { final gold = RetroColors.goldOf(context); final border = RetroColors.borderOf(context); return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: isSelected ? gold.withValues(alpha: 0.15) : Colors.transparent, border: Border.all( color: isSelected ? gold : border, width: isSelected ? 2 : 1, ), ), child: Row( children: [ if (prefix != null) ...[ Text(prefix!, style: const TextStyle(fontSize: 16)), const SizedBox(width: 12), ], Expanded( child: Text( label, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 18, color: isSelected ? gold : RetroColors.textPrimaryOf(context), ), ), ), if (isSelected) Icon(Icons.check, size: 16, color: gold), ], ), ), ), ); } } /// 사운드 설정 다이얼로그 class _RetroSoundDialog extends StatelessWidget { const _RetroSoundDialog({ required this.bgmVolume, required this.sfxVolume, required this.onBgmChanged, required this.onSfxChanged, }); final double bgmVolume; final double sfxVolume; final ValueChanged onBgmChanged; final ValueChanged onSfxChanged; @override Widget build(BuildContext context) { final background = RetroColors.backgroundOf(context); final gold = RetroColors.goldOf(context); return Dialog( backgroundColor: Colors.transparent, child: Container( constraints: const BoxConstraints(maxWidth: 360), decoration: BoxDecoration( color: background, border: Border.all(color: gold, width: 2), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 타이틀 Container( width: double.infinity, padding: const EdgeInsets.all(12), color: gold.withValues(alpha: 0.2), child: Text( 'SOUND', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: gold, ), textAlign: TextAlign.center, ), ), // 슬라이더 Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _buildVolumeSlider( context, icon: bgmVolume == 0 ? Icons.music_off : Icons.music_note, label: 'BGM', value: bgmVolume, onChanged: onBgmChanged, ), const SizedBox(height: 16), _buildVolumeSlider( context, icon: sfxVolume == 0 ? Icons.volume_off : Icons.volume_up, label: 'SFX', value: sfxVolume, onChanged: onSfxChanged, ), ], ), ), // 확인 버튼 Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), child: SizedBox( width: double.infinity, child: RetroTextButton( text: 'OK', onPressed: () => Navigator.pop(context), ), ), ), ], ), ), ); } Widget _buildVolumeSlider( BuildContext context, { required IconData icon, required String label, required double value, required ValueChanged onChanged, }) { final gold = RetroColors.goldOf(context); final border = RetroColors.borderOf(context); final percentage = (value * 100).round(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 18, color: gold), const SizedBox(width: 8), Text( label, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: RetroColors.textPrimaryOf(context), ), ), const Spacer(), Text( '$percentage%', style: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: gold, ), ), ], ), const SizedBox(height: 8), SliderTheme( data: SliderThemeData( trackHeight: 8, activeTrackColor: gold, inactiveTrackColor: border, thumbColor: RetroColors.goldLightOf(context), thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8), overlayColor: gold.withValues(alpha: 0.2), trackShape: const RectangularSliderTrackShape(), ), child: Slider(value: value, onChanged: onChanged, divisions: 10), ), ], ); } } /// 확인 다이얼로그 class _RetroConfirmDialog extends StatelessWidget { const _RetroConfirmDialog({ required this.title, required this.message, required this.confirmText, required this.cancelText, required this.onConfirm, required this.onCancel, }); final String title; final String message; final String confirmText; final String cancelText; final VoidCallback onConfirm; final VoidCallback onCancel; @override Widget build(BuildContext context) { final background = RetroColors.backgroundOf(context); final gold = RetroColors.goldOf(context); return Dialog( backgroundColor: Colors.transparent, child: Container( constraints: const BoxConstraints(maxWidth: 360), decoration: BoxDecoration( color: background, border: Border.all(color: gold, width: 3), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // 타이틀 Container( width: double.infinity, padding: const EdgeInsets.all(12), color: gold.withValues(alpha: 0.2), child: Text( title, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 10, color: gold, ), textAlign: TextAlign.center, ), ), // 메시지 Padding( padding: const EdgeInsets.all(16), child: Text( message, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 16, color: RetroColors.textPrimaryOf(context), height: 1.8, ), textAlign: TextAlign.center, ), ), // 버튼 Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), child: Row( children: [ Expanded( child: RetroTextButton( text: cancelText, isPrimary: false, onPressed: onCancel, ), ), const SizedBox(width: 8), Expanded( child: RetroTextButton( text: confirmText, onPressed: onConfirm, ), ), ], ), ), ], ), ), ); } }