refactor(game): 데스크톱 레이아웃을 DesktopGameLayout 위젯으로 분리

- game_play_screen.dart 873줄 → 745줄 (128줄 감소)
- desktop_game_layout.dart 193줄 신규 생성
- 앱바, 3패널 레이아웃, 키보드 단축키를 독립 위젯으로 추출
- MobileCarouselLayout과 동일한 패턴 적용
This commit is contained in:
JiWoong Sul
2026-03-19 15:00:54 +09:00
parent 7fcae4893e
commit a45eafa8fc
2 changed files with 217 additions and 160 deletions

View File

@@ -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,120 +683,35 @@ class _GamePlayScreenState extends State<GamePlayScreen>
}
}
},
child: Focus(
autofocus: true,
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
child: Scaffold(
backgroundColor: RetroColors.deepBrown,
appBar: _buildDesktopAppBar(context, state),
body: Stack(
child: Stack(
children: [
_buildDesktopMainContent(state),
..._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,
DesktopGameLayout(
state: state,
combatLogEntries: _combatLogController.entries,
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
onSpeedCycle: () {
widget.controller.loop?.cycleSpeed();
setState(() {});
},
isPaused: !widget.controller.isRunning && _specialAnimation == null,
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,
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),
),
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)),
..._buildOverlays(state),
],
),
),
],
);
}
@@ -826,47 +736,4 @@ class _GamePlayScreenState extends State<GamePlayScreen>
),
];
}
/// 키보드 단축키 핸들러 (웹/데스크톱)
/// 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;
}
}

View File

@@ -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<CombatLogEntry> 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;
}
}