refactor(game): 데스크톱 레이아웃을 DesktopGameLayout 위젯으로 분리
- game_play_screen.dart 873줄 → 745줄 (128줄 감소) - desktop_game_layout.dart 193줄 신규 생성 - 앱바, 3패널 레이아웃, 키보드 단축키를 독립 위젯으로 추출 - MobileCarouselLayout과 동일한 패턴 적용
This commit is contained in:
@@ -2,7 +2,6 @@ import 'package:flutter/foundation.dart'
|
|||||||
show kIsWeb, defaultTargetPlatform, TargetPlatform;
|
show kIsWeb, defaultTargetPlatform, TargetPlatform;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart' show SchedulerBinding, SchedulerPhase;
|
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/data/game_text_l10n.dart' as game_l10n;
|
||||||
import 'package:asciineverdie/src/core/engine/iap_service.dart';
|
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/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/cinematic_view.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/death_overlay.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/victory_overlay.dart';
|
||||||
import 'package:asciineverdie/src/features/game/widgets/notification_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/widgets/return_rewards_dialog.dart';
|
||||||
import 'package:asciineverdie/src/features/game/layouts/mobile_carousel_layout.dart';
|
import 'package:asciineverdie/src/features/game/layouts/mobile_carousel_layout.dart';
|
||||||
import 'package:asciineverdie/src/features/settings/settings_screen.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/core/audio/audio_service.dart';
|
||||||
import 'package:asciineverdie/src/features/game/controllers/combat_log_controller.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/features/game/controllers/game_audio_controller.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
|
||||||
|
|
||||||
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
|
||||||
///
|
///
|
||||||
@@ -688,123 +683,38 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Focus(
|
child: Stack(
|
||||||
autofocus: true,
|
children: [
|
||||||
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
|
DesktopGameLayout(
|
||||||
child: Scaffold(
|
state: state,
|
||||||
backgroundColor: RetroColors.deepBrown,
|
combatLogEntries: _combatLogController.entries,
|
||||||
appBar: _buildDesktopAppBar(context, state),
|
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
||||||
body: Stack(
|
onSpeedCycle: () {
|
||||||
children: [
|
widget.controller.loop?.cycleSpeed();
|
||||||
_buildDesktopMainContent(state),
|
setState(() {});
|
||||||
..._buildOverlays(state),
|
},
|
||||||
],
|
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<Widget> _buildOverlays(GameState state) {
|
List<Widget> _buildOverlays(GameState state) {
|
||||||
return [
|
return [
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
190
lib/src/features/game/layouts/desktop_game_layout.dart
Normal file
190
lib/src/features/game/layouts/desktop_game_layout.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user