diff --git a/lib/src/features/game/layouts/mobile_carousel_layout.dart b/lib/src/features/game/layouts/mobile_carousel_layout.dart index 12a27b7..4f9c664 100644 --- a/lib/src/features/game/layouts/mobile_carousel_layout.dart +++ b/lib/src/features/game/layouts/mobile_carousel_layout.dart @@ -15,7 +15,11 @@ 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/dialogs/retro_confirm_dialog.dart'; +import 'package:asciineverdie/src/features/game/widgets/dialogs/retro_select_dialog.dart'; +import 'package:asciineverdie/src/features/game/widgets/dialogs/retro_sound_dialog.dart'; import 'package:asciineverdie/src/features/game/widgets/enhanced_animation_panel.dart'; +import 'package:asciineverdie/src/features/game/widgets/menu/retro_menu_widgets.dart'; import 'package:asciineverdie/src/shared/retro_colors.dart'; import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart'; @@ -177,7 +181,7 @@ class _MobileCarouselLayoutState extends State { void _showLanguageDialog(BuildContext context) { showDialog( context: context, - builder: (context) => _RetroSelectDialog( + builder: (context) => RetroSelectDialog( title: l10n.menuLanguage.toUpperCase(), children: [ _buildLanguageOption(context, 'en', l10n.languageEnglish, 'πŸ‡ΊπŸ‡Έ'), @@ -195,7 +199,7 @@ class _MobileCarouselLayoutState extends State { String flag, ) { final isSelected = l10n.currentGameLocale == locale; - return _RetroOptionItem( + return RetroOptionItem( label: label.toUpperCase(), prefix: flag, isSelected: isSelected, @@ -224,7 +228,7 @@ class _MobileCarouselLayoutState extends State { showDialog( context: context, builder: (context) => StatefulBuilder( - builder: (context, setDialogState) => _RetroSoundDialog( + builder: (context, setDialogState) => RetroSoundDialog( bgmVolume: bgmVolume, sfxVolume: sfxVolume, onBgmChanged: (double value) { @@ -244,7 +248,7 @@ class _MobileCarouselLayoutState extends State { void _showDeleteConfirmDialog(BuildContext context) { showDialog( context: context, - builder: (context) => _RetroConfirmDialog( + builder: (context) => RetroConfirmDialog( title: l10n.confirmDeleteTitle.toUpperCase(), message: l10n.confirmDeleteMessage, confirmText: l10n.buttonConfirm.toUpperCase(), @@ -262,14 +266,11 @@ class _MobileCarouselLayoutState extends State { 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', + builder: (context) => RetroConfirmDialog( + title: L10n.of(context).debugCreateTestCharacterTitle, + message: L10n.of(context).debugCreateTestCharacterMessage, + confirmText: L10n.of(context).createButton, + cancelText: L10n.of(context).cancel.toUpperCase(), onConfirm: () => Navigator.of(context).pop(true), onCancel: () => Navigator.of(context).pop(false), ), @@ -326,7 +327,7 @@ class _MobileCarouselLayoutState extends State { Icon(Icons.settings, color: gold, size: 18), const SizedBox(width: 8), Text( - 'OPTIONS', + L10n.of(context).optionsTitle, style: TextStyle( fontFamily: 'PressStart2P', fontSize: 14, @@ -350,10 +351,10 @@ class _MobileCarouselLayoutState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // === κ²Œμž„ μ œμ–΄ === - const _RetroMenuSection(title: 'CONTROL'), + RetroMenuSection(title: L10n.of(context).controlSection), const SizedBox(height: 8), // μΌμ‹œμ •μ§€/재개 - _RetroMenuItem( + RetroMenuItem( icon: widget.isPaused ? Icons.play_arrow : Icons.pause, iconColor: widget.isPaused ? RetroColors.expOf(context) @@ -368,7 +369,7 @@ class _MobileCarouselLayoutState extends State { ), const SizedBox(height: 8), // 속도 쑰절 - _RetroMenuItem( + RetroMenuItem( icon: Icons.speed, iconColor: gold, label: l10n.menuSpeed.toUpperCase(), @@ -377,10 +378,10 @@ class _MobileCarouselLayoutState extends State { const SizedBox(height: 16), // === 정보 === - const _RetroMenuSection(title: 'INFO'), + RetroMenuSection(title: L10n.of(context).infoSection), const SizedBox(height: 8), if (widget.onShowStatistics != null) - _RetroMenuItem( + RetroMenuItem( icon: Icons.bar_chart, iconColor: RetroColors.mpOf(context), label: l10n.uiStatistics.toUpperCase(), @@ -391,7 +392,7 @@ class _MobileCarouselLayoutState extends State { ), if (widget.onShowHelp != null) ...[ const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.help_outline, iconColor: RetroColors.expOf(context), label: l10n.uiHelp.toUpperCase(), @@ -404,9 +405,9 @@ class _MobileCarouselLayoutState extends State { const SizedBox(height: 16), // === μ„€μ • === - const _RetroMenuSection(title: 'SETTINGS'), + RetroMenuSection(title: L10n.of(context).settingsSection), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.language, iconColor: RetroColors.mpOf(context), label: l10n.menuLanguage.toUpperCase(), @@ -419,7 +420,7 @@ class _MobileCarouselLayoutState extends State { if (widget.onBgmVolumeChange != null || widget.onSfxVolumeChange != null) ...[ const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: widget.bgmVolume == 0 && widget.sfxVolume == 0 ? Icons.volume_off : Icons.volume_up, @@ -435,9 +436,9 @@ class _MobileCarouselLayoutState extends State { const SizedBox(height: 16), // === μ €μž₯/μ’…λ£Œ === - const _RetroMenuSection(title: 'SAVE / EXIT'), + RetroMenuSection(title: L10n.of(context).saveExitSection), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.save, iconColor: RetroColors.mpOf(context), label: l10n.menuSave.toUpperCase(), @@ -450,7 +451,7 @@ class _MobileCarouselLayoutState extends State { }, ), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.refresh, iconColor: RetroColors.warningOf(context), label: l10n.menuNewGame.toUpperCase(), @@ -461,7 +462,7 @@ class _MobileCarouselLayoutState extends State { }, ), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.exit_to_app, iconColor: RetroColors.hpOf(context), label: localizations.exitGame.toUpperCase(), @@ -474,38 +475,38 @@ class _MobileCarouselLayoutState extends State { // === 치트 μ„Ήμ…˜ (디버그 λͺ¨λ“œμ—μ„œλ§Œ) === if (widget.cheatsEnabled) ...[ const SizedBox(height: 16), - _RetroMenuSection( - title: 'DEBUG CHEATS', + RetroMenuSection( + title: L10n.of(context).debugCheatsTitle, color: RetroColors.hpOf(context), ), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.fast_forward, iconColor: RetroColors.hpOf(context), - label: 'SKIP TASK (L+1)', - subtitle: 'νƒœμŠ€ν¬ μ¦‰μ‹œ μ™„λ£Œ', + label: L10n.of(context).debugSkipTask, + subtitle: L10n.of(context).debugSkipTaskDesc, onTap: () { Navigator.pop(context); widget.onCheatTask?.call(); }, ), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.skip_next, iconColor: RetroColors.hpOf(context), - label: 'SKIP QUEST (Q!)', - subtitle: 'ν€˜μŠ€νŠΈ μ¦‰μ‹œ μ™„λ£Œ', + label: L10n.of(context).debugSkipQuest, + subtitle: L10n.of(context).debugSkipQuestDesc, onTap: () { Navigator.pop(context); widget.onCheatQuest?.call(); }, ), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.double_arrow, iconColor: RetroColors.hpOf(context), - label: 'SKIP ACT (P!)', - subtitle: 'μ•‘νŠΈ μ¦‰μ‹œ μ™„λ£Œ', + label: L10n.of(context).debugSkipAct, + subtitle: L10n.of(context).debugSkipActDesc, onTap: () { Navigator.pop(context); widget.onCheatPlot?.call(); @@ -516,16 +517,16 @@ class _MobileCarouselLayoutState extends State { // === 디버그 도ꡬ μ„Ήμ…˜ === if (kDebugMode && widget.onCreateTestCharacter != null) ...[ const SizedBox(height: 16), - _RetroMenuSection( - title: 'DEBUG TOOLS', + RetroMenuSection( + title: L10n.of(context).debugToolsTitle, color: RetroColors.warningOf(context), ), const SizedBox(height: 8), - _RetroMenuItem( + RetroMenuItem( icon: Icons.science, iconColor: RetroColors.warningOf(context), - label: 'CREATE TEST CHARACTER', - subtitle: '레벨 100 캐릭터λ₯Ό λͺ…μ˜ˆμ˜ 전당에 등둝', + label: L10n.of(context).debugCreateTestCharacter, + subtitle: L10n.of(context).debugCreateTestCharacterDesc, onTap: () { Navigator.pop(context); _showTestCharacterDialog(context); @@ -553,7 +554,7 @@ class _MobileCarouselLayoutState extends State { final isSpeedBoostActive = widget.isSpeedBoostActive; final adSpeed = widget.adSpeedMultiplier; - return _RetroSpeedChip( + return RetroSpeedChip( speed: adSpeed, isSelected: isSpeedBoostActive, isAdBased: !isSpeedBoostActive && !widget.isPaidUser, @@ -685,539 +686,4 @@ class _MobileCarouselLayoutState extends State { ), ); } - -} - -// ═══════════════════════════════════════════════════════════════════════════ -// 레트둜 μŠ€νƒ€μΌ μ˜΅μ…˜ 메뉴 μœ„μ ―λ“€ -// ═══════════════════════════════════════════════════════════════════════════ - -/// 메뉴 μ„Ήμ…˜ 타이틀 -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, - this.isDisabled = false, - }); - - final int speed; - final bool isSelected; - final VoidCallback onTap; - final bool isAdBased; - - /// λΉ„ν™œμ„± μƒνƒœ (반투λͺ…, νƒ­ λ¬΄μ‹œ) - final bool isDisabled; - - @override - Widget build(BuildContext context) { - final gold = RetroColors.goldOf(context); - final warning = RetroColors.warningOf(context); - final border = RetroColors.borderOf(context); - - // λΉ„ν™œμ„± μƒνƒœλ©΄ 반투λͺ… 처리 - final opacity = isDisabled ? 0.4 : 1.0; - - final Color bgColor; - final Color textColor; - final Color borderColor; - - if (isSelected) { - bgColor = isAdBased - ? warning.withValues(alpha: 0.3 * opacity) - : gold.withValues(alpha: 0.3 * opacity); - textColor = (isAdBased ? warning : gold).withValues(alpha: opacity); - borderColor = (isAdBased ? warning : gold).withValues(alpha: opacity); - } else if (isAdBased) { - bgColor = Colors.transparent; - textColor = warning.withValues(alpha: opacity); - borderColor = warning.withValues(alpha: opacity); - } else { - bgColor = Colors.transparent; - textColor = RetroColors.textMutedOf(context).withValues(alpha: opacity); - borderColor = border.withValues(alpha: opacity); - } - - return GestureDetector( - // λΉ„ν™œμ„± μƒνƒœλ©΄ νƒ­ λ¬΄μ‹œ - onTap: isDisabled ? null : 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 && !isDisabled) - 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, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } } diff --git a/lib/src/features/game/widgets/dialogs/retro_confirm_dialog.dart b/lib/src/features/game/widgets/dialogs/retro_confirm_dialog.dart new file mode 100644 index 0000000..bb3d662 --- /dev/null +++ b/lib/src/features/game/widgets/dialogs/retro_confirm_dialog.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/src/shared/retro_colors.dart'; +import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart'; + +/// 레트둜 μŠ€νƒ€μΌ 확인 λ‹€μ΄μ–Όλ‘œκ·Έ +class RetroConfirmDialog extends StatelessWidget { + const RetroConfirmDialog({ + super.key, + 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, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/game/widgets/dialogs/retro_select_dialog.dart b/lib/src/features/game/widgets/dialogs/retro_select_dialog.dart new file mode 100644 index 0000000..68db200 --- /dev/null +++ b/lib/src/features/game/widgets/dialogs/retro_select_dialog.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/src/shared/retro_colors.dart'; + +/// 레트둜 μŠ€νƒ€μΌ 선택 λ‹€μ΄μ–Όλ‘œκ·Έ +class RetroSelectDialog extends StatelessWidget { + const RetroSelectDialog({ + super.key, + 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({ + super.key, + 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), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/features/game/widgets/dialogs/retro_sound_dialog.dart b/lib/src/features/game/widgets/dialogs/retro_sound_dialog.dart new file mode 100644 index 0000000..602d612 --- /dev/null +++ b/lib/src/features/game/widgets/dialogs/retro_sound_dialog.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/l10n/app_localizations.dart'; +import 'package:asciineverdie/src/shared/retro_colors.dart'; +import 'package:asciineverdie/src/shared/widgets/retro_widgets.dart'; + +/// 레트둜 μŠ€νƒ€μΌ μ‚¬μš΄λ“œ μ„€μ • λ‹€μ΄μ–Όλ‘œκ·Έ +class RetroSoundDialog extends StatelessWidget { + const RetroSoundDialog({ + super.key, + 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( + L10n.of(context).soundTitle, + 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: L10n.of(context).bgmLabel, + value: bgmVolume, + onChanged: onBgmChanged, + ), + const SizedBox(height: 16), + _buildVolumeSlider( + context, + icon: sfxVolume == 0 ? Icons.volume_off : Icons.volume_up, + label: L10n.of(context).sfxLabel, + value: sfxVolume, + onChanged: onSfxChanged, + ), + ], + ), + ), + // 확인 λ²„νŠΌ + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: SizedBox( + width: double.infinity, + child: RetroTextButton( + text: L10n.of(context).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), + ), + ], + ); + } +} diff --git a/lib/src/features/game/widgets/menu/retro_menu_widgets.dart b/lib/src/features/game/widgets/menu/retro_menu_widgets.dart new file mode 100644 index 0000000..c886399 --- /dev/null +++ b/lib/src/features/game/widgets/menu/retro_menu_widgets.dart @@ -0,0 +1,204 @@ +import 'package:flutter/material.dart'; + +import 'package:asciineverdie/src/shared/retro_colors.dart'; + +/// 메뉴 μ„Ήμ…˜ 타이틀 +class RetroMenuSection extends StatelessWidget { + const RetroMenuSection({ + super.key, + 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({ + super.key, + 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({ + super.key, + required this.speed, + required this.isSelected, + required this.onTap, + this.isAdBased = false, + this.isDisabled = false, + }); + + final int speed; + final bool isSelected; + final VoidCallback onTap; + final bool isAdBased; + + /// λΉ„ν™œμ„± μƒνƒœ (반투λͺ…, νƒ­ λ¬΄μ‹œ) + final bool isDisabled; + + @override + Widget build(BuildContext context) { + final gold = RetroColors.goldOf(context); + final warning = RetroColors.warningOf(context); + final border = RetroColors.borderOf(context); + + // λΉ„ν™œμ„± μƒνƒœλ©΄ 반투λͺ… 처리 + final opacity = isDisabled ? 0.4 : 1.0; + + final Color bgColor; + final Color textColor; + final Color borderColor; + + if (isSelected) { + bgColor = isAdBased + ? warning.withValues(alpha: 0.3 * opacity) + : gold.withValues(alpha: 0.3 * opacity); + textColor = (isAdBased ? warning : gold).withValues(alpha: opacity); + borderColor = (isAdBased ? warning : gold).withValues(alpha: opacity); + } else if (isAdBased) { + bgColor = Colors.transparent; + textColor = warning.withValues(alpha: opacity); + borderColor = warning.withValues(alpha: opacity); + } else { + bgColor = Colors.transparent; + textColor = RetroColors.textMutedOf(context).withValues(alpha: opacity); + borderColor = border.withValues(alpha: opacity); + } + + return GestureDetector( + // λΉ„ν™œμ„± μƒνƒœλ©΄ νƒ­ λ¬΄μ‹œ + onTap: isDisabled ? null : 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 && !isDisabled) + 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, + ), + ), + ], + ), + ), + ); + } +}