diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index 0db1aa5..85f9142 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform, TargetPlatform; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase; -import 'package:flutter/services.dart' show KeyDownEvent, LogicalKeyboardKey; import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; import 'package:asciineverdie/src/core/engine/iap_service.dart'; @@ -17,12 +16,9 @@ import 'package:asciineverdie/src/features/game/game_session_controller.dart'; import 'package:asciineverdie/src/features/hall_of_fame/hall_of_fame_screen.dart'; import 'package:asciineverdie/src/features/game/widgets/cinematic_view.dart'; import 'package:asciineverdie/src/features/game/widgets/death_overlay.dart'; -import 'package:asciineverdie/src/features/game/widgets/desktop_character_panel.dart'; -import 'package:asciineverdie/src/features/game/widgets/desktop_equipment_panel.dart'; -import 'package:asciineverdie/src/features/game/widgets/desktop_quest_panel.dart'; import 'package:asciineverdie/src/features/game/widgets/victory_overlay.dart'; import 'package:asciineverdie/src/features/game/widgets/notification_overlay.dart'; -import 'package:asciineverdie/src/features/game/widgets/task_progress_panel.dart'; +import 'package:asciineverdie/src/features/game/layouts/desktop_game_layout.dart'; import 'package:asciineverdie/src/features/game/widgets/return_rewards_dialog.dart'; import 'package:asciineverdie/src/features/game/layouts/mobile_carousel_layout.dart'; import 'package:asciineverdie/src/features/settings/settings_screen.dart'; @@ -32,7 +28,6 @@ import 'package:asciineverdie/src/core/storage/settings_repository.dart'; import 'package:asciineverdie/src/core/audio/audio_service.dart'; import 'package:asciineverdie/src/features/game/controllers/combat_log_controller.dart'; import 'package:asciineverdie/src/features/game/controllers/game_audio_controller.dart'; -import 'package:asciineverdie/src/shared/retro_colors.dart'; /// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃) /// @@ -688,123 +683,38 @@ class _GamePlayScreenState extends State } } }, - child: Focus( - autofocus: true, - onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context), - child: Scaffold( - backgroundColor: RetroColors.deepBrown, - appBar: _buildDesktopAppBar(context, state), - body: Stack( - children: [ - _buildDesktopMainContent(state), - ..._buildOverlays(state), - ], + child: Stack( + children: [ + DesktopGameLayout( + state: state, + combatLogEntries: _combatLogController.entries, + speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1, + onSpeedCycle: () { + widget.controller.loop?.cycleSpeed(); + setState(() {}); + }, + isPaused: + !widget.controller.isRunning && _specialAnimation == null, + onPauseToggle: () async { + await widget.controller.togglePause(); + setState(() {}); + }, + specialAnimation: _specialAnimation, + cheatsEnabled: widget.controller.cheatsEnabled, + onCheatTask: () => widget.controller.loop?.cheatCompleteTask(), + onCheatQuest: () => widget.controller.loop?.cheatCompleteQuest(), + onCheatPlot: () => widget.controller.loop?.cheatCompletePlot(), + onShowStatistics: () => _showStatisticsDialog(context), + onShowHelp: () => HelpDialog.show(context), + onShowSettings: () => _showSettingsScreen(context), ), - ), + ..._buildOverlays(state), + ], ), ), ); } - /// 데스크톱 앱바 - PreferredSizeWidget _buildDesktopAppBar( - BuildContext context, - GameState state, - ) { - return AppBar( - backgroundColor: RetroColors.darkBrown, - title: Text( - L10n.of(context).progressQuestTitle(state.traits.name), - style: const TextStyle( - fontFamily: 'PressStart2P', - fontSize: 15, - color: RetroColors.gold, - ), - ), - actions: [ - if (widget.controller.cheatsEnabled) ...[ - IconButton( - icon: const Text('L+1'), - tooltip: L10n.of(context).levelUp, - onPressed: () => widget.controller.loop?.cheatCompleteTask(), - ), - IconButton( - icon: const Text('Q!'), - tooltip: L10n.of(context).completeQuest, - onPressed: () => widget.controller.loop?.cheatCompleteQuest(), - ), - IconButton( - icon: const Text('P!'), - tooltip: L10n.of(context).completePlot, - onPressed: () => widget.controller.loop?.cheatCompletePlot(), - ), - ], - IconButton( - icon: const Icon(Icons.bar_chart), - tooltip: game_l10n.uiStatistics, - onPressed: () => _showStatisticsDialog(context), - ), - IconButton( - icon: const Icon(Icons.help_outline), - tooltip: game_l10n.uiHelp, - onPressed: () => HelpDialog.show(context), - ), - IconButton( - icon: const Icon(Icons.settings), - tooltip: game_l10n.uiSettings, - onPressed: () => _showSettingsScreen(context), - ), - ], - ); - } - - /// 데스크톱 메인 컨텐츠 (3패널) - Widget _buildDesktopMainContent(GameState state) { - return Column( - children: [ - TaskProgressPanel( - progress: state.progress, - speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1, - onSpeedCycle: () { - widget.controller.loop?.cycleSpeed(); - setState(() {}); - }, - isPaused: !widget.controller.isRunning && _specialAnimation == null, - onPauseToggle: () async { - await widget.controller.togglePause(); - setState(() {}); - }, - specialAnimation: _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, - ), - Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded(flex: 2, child: DesktopCharacterPanel(state: state)), - Expanded( - flex: 3, - child: DesktopEquipmentPanel( - state: state, - combatLogEntries: _combatLogController.entries, - ), - ), - Expanded(flex: 2, child: DesktopQuestPanel(state: state)), - ], - ), - ), - ], - ); - } - /// 공통 오버레이 (사망, 승리) List _buildOverlays(GameState state) { return [ @@ -826,47 +736,4 @@ class _GamePlayScreenState extends State ), ]; } - - /// 키보드 단축키 핸들러 (웹/데스크톱) - /// Space: 일시정지/재개, S: 속도 변경, H: 도움말, Esc: 설정 - KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) { - // KeyDown 이벤트만 처리 - if (event is! KeyDownEvent) return KeyEventResult.ignored; - - final key = event.logicalKey; - - // Space: 일시정지/재개 - if (key == LogicalKeyboardKey.space) { - widget.controller.togglePause(); - setState(() {}); - return KeyEventResult.handled; - } - - // S: 속도 변경 - if (key == LogicalKeyboardKey.keyS) { - widget.controller.loop?.cycleSpeed(); - setState(() {}); - return KeyEventResult.handled; - } - - // H 또는 F1: 도움말 - if (key == LogicalKeyboardKey.keyH || key == LogicalKeyboardKey.f1) { - HelpDialog.show(context); - return KeyEventResult.handled; - } - - // Escape: 설정 - if (key == LogicalKeyboardKey.escape) { - _showSettingsScreen(context); - return KeyEventResult.handled; - } - - // I: 통계 - if (key == LogicalKeyboardKey.keyI) { - _showStatisticsDialog(context); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - } } diff --git a/lib/src/features/game/layouts/desktop_game_layout.dart b/lib/src/features/game/layouts/desktop_game_layout.dart new file mode 100644 index 0000000..4478017 --- /dev/null +++ b/lib/src/features/game/layouts/desktop_game_layout.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show KeyDownEvent, LogicalKeyboardKey; + +import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n; +import 'package:asciineverdie/l10n/app_localizations.dart'; +import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/features/game/widgets/combat_log.dart'; +import 'package:asciineverdie/src/features/game/widgets/desktop_character_panel.dart'; +import 'package:asciineverdie/src/features/game/widgets/desktop_equipment_panel.dart'; +import 'package:asciineverdie/src/features/game/widgets/desktop_quest_panel.dart'; +import 'package:asciineverdie/src/features/game/widgets/task_progress_panel.dart'; +import 'package:asciineverdie/src/shared/animation/ascii_animation_type.dart'; +import 'package:asciineverdie/src/shared/retro_colors.dart'; + +/// 데스크톱 3패널 레이아웃 +/// +/// 웹/데스크톱용 레이아웃: +/// - 상단: 앱바 (타이틀, 치트/통계/도움말/설정 버튼) +/// - 중앙 상단: 태스크 진행 패널 (ASCII 애니메이션, 진행 바) +/// - 중앙 하단: 3패널 (캐릭터 | 장비+전투로그 | 퀘스트) +class DesktopGameLayout extends StatelessWidget { + const DesktopGameLayout({ + super.key, + required this.state, + required this.combatLogEntries, + required this.speedMultiplier, + required this.onSpeedCycle, + required this.isPaused, + required this.onPauseToggle, + this.specialAnimation, + this.cheatsEnabled = false, + this.onCheatTask, + this.onCheatQuest, + this.onCheatPlot, + this.onShowStatistics, + this.onShowHelp, + this.onShowSettings, + }); + + final GameState state; + final List combatLogEntries; + final int speedMultiplier; + final VoidCallback onSpeedCycle; + final bool isPaused; + final VoidCallback onPauseToggle; + final AsciiAnimationType? specialAnimation; + final bool cheatsEnabled; + final VoidCallback? onCheatTask; + final VoidCallback? onCheatQuest; + final VoidCallback? onCheatPlot; + final VoidCallback? onShowStatistics; + final VoidCallback? onShowHelp; + final VoidCallback? onShowSettings; + + @override + Widget build(BuildContext context) { + return Focus( + autofocus: true, + onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context), + child: Scaffold( + backgroundColor: RetroColors.deepBrown, + appBar: _buildAppBar(context), + body: _buildMainContent(), + ), + ); + } + + PreferredSizeWidget _buildAppBar(BuildContext context) { + return AppBar( + backgroundColor: RetroColors.darkBrown, + title: Text( + L10n.of(context).progressQuestTitle(state.traits.name), + style: const TextStyle( + fontFamily: 'PressStart2P', + fontSize: 15, + color: RetroColors.gold, + ), + ), + actions: [ + if (cheatsEnabled) ...[ + IconButton( + icon: const Text('L+1'), + tooltip: L10n.of(context).levelUp, + onPressed: onCheatTask, + ), + IconButton( + icon: const Text('Q!'), + tooltip: L10n.of(context).completeQuest, + onPressed: onCheatQuest, + ), + IconButton( + icon: const Text('P!'), + tooltip: L10n.of(context).completePlot, + onPressed: onCheatPlot, + ), + ], + IconButton( + icon: const Icon(Icons.bar_chart), + tooltip: game_l10n.uiStatistics, + onPressed: onShowStatistics, + ), + IconButton( + icon: const Icon(Icons.help_outline), + tooltip: game_l10n.uiHelp, + onPressed: onShowHelp, + ), + IconButton( + icon: const Icon(Icons.settings), + tooltip: game_l10n.uiSettings, + onPressed: onShowSettings, + ), + ], + ); + } + + Widget _buildMainContent() { + return Column( + children: [ + TaskProgressPanel( + progress: state.progress, + speedMultiplier: speedMultiplier, + onSpeedCycle: onSpeedCycle, + isPaused: isPaused, + onPauseToggle: onPauseToggle, + specialAnimation: 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, + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(flex: 2, child: DesktopCharacterPanel(state: state)), + Expanded( + flex: 3, + child: DesktopEquipmentPanel( + state: state, + combatLogEntries: combatLogEntries, + ), + ), + Expanded(flex: 2, child: DesktopQuestPanel(state: state)), + ], + ), + ), + ], + ); + } + + /// 키보드 단축키 핸들러 (웹/데스크톱) + /// Space: 일시정지/재개, S: 속도 변경, H: 도움말, Esc: 설정 + KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) { + if (event is! KeyDownEvent) return KeyEventResult.ignored; + + final key = event.logicalKey; + + if (key == LogicalKeyboardKey.space) { + onPauseToggle(); + return KeyEventResult.handled; + } + + if (key == LogicalKeyboardKey.keyS) { + onSpeedCycle(); + return KeyEventResult.handled; + } + + if (key == LogicalKeyboardKey.keyH || key == LogicalKeyboardKey.f1) { + onShowHelp?.call(); + return KeyEventResult.handled; + } + + if (key == LogicalKeyboardKey.escape) { + onShowSettings?.call(); + return KeyEventResult.handled; + } + + if (key == LogicalKeyboardKey.keyI) { + onShowStatistics?.call(); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + } +}