refactor(ui): 화면 및 위젯 정리
- GamePlayScreen build() 메서드 분할 (300→15 LOC) - 애니메이션/프로그레스 패널 개선 - 설정 화면 정리
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||||
|
|
||||||
/// 알림 타입 (Notification Type)
|
/// 알림 타입 (Notification Type)
|
||||||
enum NotificationType {
|
enum NotificationType {
|
||||||
levelUp, // 레벨업
|
levelUp, // 레벨업
|
||||||
@@ -62,8 +64,8 @@ class NotificationService {
|
|||||||
show(
|
show(
|
||||||
GameNotification(
|
GameNotification(
|
||||||
type: NotificationType.levelUp,
|
type: NotificationType.levelUp,
|
||||||
title: 'LEVEL UP!',
|
title: game_l10n.notifyLevelUp,
|
||||||
subtitle: 'Level $newLevel',
|
subtitle: game_l10n.notifyLevel(newLevel),
|
||||||
data: {'level': newLevel},
|
data: {'level': newLevel},
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
@@ -75,7 +77,7 @@ class NotificationService {
|
|||||||
show(
|
show(
|
||||||
GameNotification(
|
GameNotification(
|
||||||
type: NotificationType.questComplete,
|
type: NotificationType.questComplete,
|
||||||
title: 'QUEST COMPLETE!',
|
title: game_l10n.notifyQuestComplete,
|
||||||
subtitle: questName,
|
subtitle: questName,
|
||||||
data: {'quest': questName},
|
data: {'quest': questName},
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
@@ -87,8 +89,8 @@ class NotificationService {
|
|||||||
/// actNumber: 0=프롤로그, 1=Act I, 2=Act II, ...
|
/// actNumber: 0=프롤로그, 1=Act I, 2=Act II, ...
|
||||||
void showActComplete(int actNumber) {
|
void showActComplete(int actNumber) {
|
||||||
final title = actNumber == 0
|
final title = actNumber == 0
|
||||||
? 'PROLOGUE COMPLETE!'
|
? game_l10n.notifyPrologueComplete
|
||||||
: 'ACT $actNumber COMPLETE!';
|
: game_l10n.notifyActComplete(actNumber);
|
||||||
show(
|
show(
|
||||||
GameNotification(
|
GameNotification(
|
||||||
type: NotificationType.actComplete,
|
type: NotificationType.actComplete,
|
||||||
@@ -103,7 +105,7 @@ class NotificationService {
|
|||||||
show(
|
show(
|
||||||
GameNotification(
|
GameNotification(
|
||||||
type: NotificationType.newSpell,
|
type: NotificationType.newSpell,
|
||||||
title: 'NEW SPELL!',
|
title: game_l10n.notifyNewSpell,
|
||||||
subtitle: spellName,
|
subtitle: spellName,
|
||||||
data: {'spell': spellName},
|
data: {'spell': spellName},
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
@@ -116,7 +118,7 @@ class NotificationService {
|
|||||||
show(
|
show(
|
||||||
GameNotification(
|
GameNotification(
|
||||||
type: NotificationType.newEquipment,
|
type: NotificationType.newEquipment,
|
||||||
title: 'NEW EQUIPMENT!',
|
title: game_l10n.notifyNewEquipment,
|
||||||
subtitle: equipmentName,
|
subtitle: equipmentName,
|
||||||
data: {'equipment': equipmentName, 'slot': slot},
|
data: {'equipment': equipmentName, 'slot': slot},
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(seconds: 2),
|
||||||
@@ -129,7 +131,7 @@ class NotificationService {
|
|||||||
show(
|
show(
|
||||||
GameNotification(
|
GameNotification(
|
||||||
type: NotificationType.bossDefeat,
|
type: NotificationType.bossDefeat,
|
||||||
title: 'BOSS DEFEATED!',
|
title: game_l10n.notifyBossDefeated,
|
||||||
subtitle: bossName,
|
subtitle: bossName,
|
||||||
data: {'boss': bossName},
|
data: {'boss': bossName},
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
|
|||||||
@@ -217,9 +217,9 @@ class _AnimationPanel extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 18),
|
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 18),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text(
|
Text(
|
||||||
'ASCII NEVER DIE',
|
L10n.of(context).appTitle.toUpperCase(),
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: RetroColors.gold,
|
color: RetroColors.gold,
|
||||||
@@ -272,7 +272,7 @@ class _ActionButtons extends StatelessWidget {
|
|||||||
final l10n = L10n.of(context);
|
final l10n = L10n.of(context);
|
||||||
|
|
||||||
return RetroPanel(
|
return RetroPanel(
|
||||||
title: 'MENU',
|
title: L10n.of(context).menuTitle,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
|||||||
@@ -551,150 +551,135 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 로케일 변경 시 전체 위젯 트리 강제 리빌드를 위한 Key
|
|
||||||
final localeKey = ValueKey(game_l10n.currentGameLocale);
|
final localeKey = ValueKey(game_l10n.currentGameLocale);
|
||||||
|
|
||||||
// 캐로셀 레이아웃 사용 여부 확인
|
|
||||||
if (_shouldUseCarouselLayout(context)) {
|
if (_shouldUseCarouselLayout(context)) {
|
||||||
return NotificationOverlay(
|
return _buildMobileLayout(context, state, localeKey);
|
||||||
key: localeKey,
|
|
||||||
notificationService: _notificationService,
|
|
||||||
child: PopScope(
|
|
||||||
canPop: false,
|
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
|
||||||
if (didPop) return;
|
|
||||||
final shouldPop = await _onPopInvoked();
|
|
||||||
if (shouldPop && context.mounted) {
|
|
||||||
await widget.controller.pause(saveOnStop: false);
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
MobileCarouselLayout(
|
|
||||||
state: state,
|
|
||||||
combatLogEntries: _combatLogController.entries,
|
|
||||||
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
|
||||||
onSpeedCycle: () {
|
|
||||||
widget.controller.loop?.cycleSpeed();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
onSetSpeed: (speed) {
|
|
||||||
widget.controller.loop?.setSpeed(speed);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
// 특수 애니메이션 중에는 일시정지 상태로 표시하지 않음
|
|
||||||
isPaused:
|
|
||||||
!widget.controller.isRunning && _specialAnimation == null,
|
|
||||||
onPauseToggle: () async {
|
|
||||||
await widget.controller.togglePause();
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
onSave: _saveGameState,
|
|
||||||
onExit: () async {
|
|
||||||
final shouldExit = await _onPopInvoked();
|
|
||||||
if (shouldExit && context.mounted) {
|
|
||||||
await widget.controller.pause(saveOnStop: false);
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
notificationService: _notificationService,
|
|
||||||
specialAnimation: _specialAnimation,
|
|
||||||
onLanguageChange: (locale) async {
|
|
||||||
// navigator 참조를 async gap 전에 저장
|
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
// 1. 현재 상태 저장
|
|
||||||
await widget.controller.pause(saveOnStop: true);
|
|
||||||
// 2. 로케일 변경
|
|
||||||
game_l10n.setGameLocale(locale);
|
|
||||||
// 3. 화면 재생성 (전체 UI 재구성)
|
|
||||||
if (context.mounted) {
|
|
||||||
await widget.controller.resume();
|
|
||||||
navigator.pushReplacement(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (_) => GamePlayScreen(
|
|
||||||
controller: widget.controller,
|
|
||||||
audioService: widget.audioService,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDeleteSaveAndNewGame: () async {
|
|
||||||
// 게임 루프 중지
|
|
||||||
await widget.controller.pause(saveOnStop: false);
|
|
||||||
// 세이브 파일 삭제
|
|
||||||
await widget.controller.saveManager.deleteSave();
|
|
||||||
// 캐릭터 생성 화면으로 돌아가기
|
|
||||||
if (context.mounted) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 사운드 설정
|
|
||||||
bgmVolume: _audioController.bgmVolume,
|
|
||||||
sfxVolume: _audioController.sfxVolume,
|
|
||||||
onBgmVolumeChange: (volume) {
|
|
||||||
_audioController.setBgmVolume(volume);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
onSfxVolumeChange: (volume) {
|
|
||||||
_audioController.setSfxVolume(volume);
|
|
||||||
setState(() {});
|
|
||||||
},
|
|
||||||
// 통계 및 도움말
|
|
||||||
onShowStatistics: () => _showStatisticsDialog(context),
|
|
||||||
onShowHelp: () => HelpDialog.show(context),
|
|
||||||
// 치트 (디버그 모드)
|
|
||||||
cheatsEnabled: widget.controller.cheatsEnabled,
|
|
||||||
onCheatTask: () => widget.controller.loop?.cheatCompleteTask(),
|
|
||||||
onCheatQuest: () =>
|
|
||||||
widget.controller.loop?.cheatCompleteQuest(),
|
|
||||||
onCheatPlot: () => widget.controller.loop?.cheatCompletePlot(),
|
|
||||||
onCreateTestCharacter: () async {
|
|
||||||
final navigator = Navigator.of(context);
|
|
||||||
final success = await widget.controller.createTestCharacter();
|
|
||||||
if (success && mounted) {
|
|
||||||
navigator.popUntil((route) => route.isFirst);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 수익화 버프 (자동부활, 광고배속)
|
|
||||||
autoReviveEndMs: widget.controller.monetization.autoReviveEndMs,
|
|
||||||
speedBoostEndMs: widget.controller.monetization.speedBoostEndMs,
|
|
||||||
isPaidUser: widget.controller.monetization.isPaidUser,
|
|
||||||
onSpeedBoostActivate: _handleSpeedBoost,
|
|
||||||
isSpeedBoostActive: widget.controller.isSpeedBoostActive,
|
|
||||||
adSpeedMultiplier: widget.controller.adSpeedMultiplier,
|
|
||||||
has2xUnlocked: widget.controller.has2xUnlocked,
|
|
||||||
),
|
|
||||||
// 사망 오버레이
|
|
||||||
if (state.isDead && state.deathInfo != null)
|
|
||||||
DeathOverlay(
|
|
||||||
deathInfo: state.deathInfo!,
|
|
||||||
traits: state.traits,
|
|
||||||
onResurrect: _handleResurrect,
|
|
||||||
onAdRevive: _handleAdRevive,
|
|
||||||
isPaidUser: IAPService.instance.isAdRemovalPurchased,
|
|
||||||
),
|
|
||||||
// 승리 오버레이 (게임 클리어)
|
|
||||||
if (widget.controller.isComplete)
|
|
||||||
VictoryOverlay(
|
|
||||||
traits: state.traits,
|
|
||||||
stats: state.stats,
|
|
||||||
progress: state.progress,
|
|
||||||
elapsedMs: state.skillSystem.elapsedMs,
|
|
||||||
onComplete: _handleVictoryComplete,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 기존 데스크톱 레이아웃 (레트로 스타일)
|
return _buildDesktopLayout(context, state, localeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 모바일 캐로셀 레이아웃
|
||||||
|
Widget _buildMobileLayout(
|
||||||
|
BuildContext context,
|
||||||
|
GameState state,
|
||||||
|
ValueKey<String> localeKey,
|
||||||
|
) {
|
||||||
|
return NotificationOverlay(
|
||||||
|
key: localeKey,
|
||||||
|
notificationService: _notificationService,
|
||||||
|
child: PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
|
if (didPop) return;
|
||||||
|
final shouldPop = await _onPopInvoked();
|
||||||
|
if (shouldPop && context.mounted) {
|
||||||
|
await widget.controller.pause(saveOnStop: false);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
MobileCarouselLayout(
|
||||||
|
state: state,
|
||||||
|
combatLogEntries: _combatLogController.entries,
|
||||||
|
speedMultiplier: widget.controller.loop?.speedMultiplier ?? 1,
|
||||||
|
onSpeedCycle: () {
|
||||||
|
widget.controller.loop?.cycleSpeed();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onSetSpeed: (speed) {
|
||||||
|
widget.controller.loop?.setSpeed(speed);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
isPaused:
|
||||||
|
!widget.controller.isRunning && _specialAnimation == null,
|
||||||
|
onPauseToggle: () async {
|
||||||
|
await widget.controller.togglePause();
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onSave: _saveGameState,
|
||||||
|
onExit: () async {
|
||||||
|
final shouldExit = await _onPopInvoked();
|
||||||
|
if (shouldExit && context.mounted) {
|
||||||
|
await widget.controller.pause(saveOnStop: false);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
notificationService: _notificationService,
|
||||||
|
specialAnimation: _specialAnimation,
|
||||||
|
onLanguageChange: (locale) async {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
await widget.controller.pause(saveOnStop: true);
|
||||||
|
game_l10n.setGameLocale(locale);
|
||||||
|
if (context.mounted) {
|
||||||
|
await widget.controller.resume();
|
||||||
|
navigator.pushReplacement(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => GamePlayScreen(
|
||||||
|
controller: widget.controller,
|
||||||
|
audioService: widget.audioService,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleteSaveAndNewGame: () async {
|
||||||
|
await widget.controller.pause(saveOnStop: false);
|
||||||
|
await widget.controller.saveManager.deleteSave();
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bgmVolume: _audioController.bgmVolume,
|
||||||
|
sfxVolume: _audioController.sfxVolume,
|
||||||
|
onBgmVolumeChange: (volume) {
|
||||||
|
_audioController.setBgmVolume(volume);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onSfxVolumeChange: (volume) {
|
||||||
|
_audioController.setSfxVolume(volume);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
onShowStatistics: () => _showStatisticsDialog(context),
|
||||||
|
onShowHelp: () => HelpDialog.show(context),
|
||||||
|
cheatsEnabled: widget.controller.cheatsEnabled,
|
||||||
|
onCheatTask: () => widget.controller.loop?.cheatCompleteTask(),
|
||||||
|
onCheatQuest: () => widget.controller.loop?.cheatCompleteQuest(),
|
||||||
|
onCheatPlot: () => widget.controller.loop?.cheatCompletePlot(),
|
||||||
|
onCreateTestCharacter: () async {
|
||||||
|
final navigator = Navigator.of(context);
|
||||||
|
final success = await widget.controller.createTestCharacter();
|
||||||
|
if (success && mounted) {
|
||||||
|
navigator.popUntil((route) => route.isFirst);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoReviveEndMs: widget.controller.monetization.autoReviveEndMs,
|
||||||
|
speedBoostEndMs: widget.controller.monetization.speedBoostEndMs,
|
||||||
|
isPaidUser: widget.controller.monetization.isPaidUser,
|
||||||
|
onSpeedBoostActivate: _handleSpeedBoost,
|
||||||
|
isSpeedBoostActive: widget.controller.isSpeedBoostActive,
|
||||||
|
adSpeedMultiplier: widget.controller.adSpeedMultiplier,
|
||||||
|
has2xUnlocked: widget.controller.has2xUnlocked,
|
||||||
|
),
|
||||||
|
..._buildOverlays(state),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 데스크톱 3패널 레이아웃
|
||||||
|
Widget _buildDesktopLayout(
|
||||||
|
BuildContext context,
|
||||||
|
GameState state,
|
||||||
|
ValueKey<String> localeKey,
|
||||||
|
) {
|
||||||
return NotificationOverlay(
|
return NotificationOverlay(
|
||||||
key: localeKey,
|
key: localeKey,
|
||||||
notificationService: _notificationService,
|
notificationService: _notificationService,
|
||||||
@@ -710,135 +695,16 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 웹/데스크톱 키보드 단축키 지원
|
|
||||||
child: Focus(
|
child: Focus(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
|
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: RetroColors.deepBrown,
|
backgroundColor: RetroColors.deepBrown,
|
||||||
appBar: AppBar(
|
appBar: _buildDesktopAppBar(context, state),
|
||||||
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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// 메인 게임 UI
|
_buildDesktopMainContent(state),
|
||||||
Column(
|
..._buildOverlays(state),
|
||||||
children: [
|
|
||||||
// 상단: ASCII 애니메이션 + Task Progress (Phase 7: 고정 4색 팔레트)
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
|
|
||||||
// 메인 3패널 영역
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
// 좌측 패널: Character Sheet
|
|
||||||
Expanded(flex: 2, child: _buildCharacterPanel(state)),
|
|
||||||
|
|
||||||
// 중앙 패널: Equipment/Inventory
|
|
||||||
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
|
||||||
|
|
||||||
// 우측 패널: Plot/Quest
|
|
||||||
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
// 사망 오버레이
|
|
||||||
if (state.isDead && state.deathInfo != null)
|
|
||||||
DeathOverlay(
|
|
||||||
deathInfo: state.deathInfo!,
|
|
||||||
traits: state.traits,
|
|
||||||
onResurrect: _handleResurrect,
|
|
||||||
onAdRevive: _handleAdRevive,
|
|
||||||
isPaidUser: IAPService.instance.isAdRemovalPurchased,
|
|
||||||
),
|
|
||||||
// 승리 오버레이 (게임 클리어)
|
|
||||||
if (widget.controller.isComplete)
|
|
||||||
VictoryOverlay(
|
|
||||||
traits: state.traits,
|
|
||||||
stats: state.stats,
|
|
||||||
progress: state.progress,
|
|
||||||
elapsedMs: state.skillSystem.elapsedMs,
|
|
||||||
onComplete: _handleVictoryComplete,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -847,6 +713,118 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 데스크톱 앱바
|
||||||
|
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: _buildCharacterPanel(state)),
|
||||||
|
Expanded(flex: 3, child: _buildEquipmentPanel(state)),
|
||||||
|
Expanded(flex: 2, child: _buildQuestPanel(state)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 공통 오버레이 (사망, 승리)
|
||||||
|
List<Widget> _buildOverlays(GameState state) {
|
||||||
|
return [
|
||||||
|
if (state.isDead && state.deathInfo != null)
|
||||||
|
DeathOverlay(
|
||||||
|
deathInfo: state.deathInfo!,
|
||||||
|
traits: state.traits,
|
||||||
|
onResurrect: _handleResurrect,
|
||||||
|
onAdRevive: _handleAdRevive,
|
||||||
|
isPaidUser: IAPService.instance.isAdRemovalPurchased,
|
||||||
|
),
|
||||||
|
if (widget.controller.isComplete)
|
||||||
|
VictoryOverlay(
|
||||||
|
traits: state.traits,
|
||||||
|
stats: state.stats,
|
||||||
|
progress: state.progress,
|
||||||
|
elapsedMs: state.skillSystem.elapsedMs,
|
||||||
|
onComplete: _handleVictoryComplete,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/// 키보드 단축키 핸들러 (웹/데스크톱)
|
/// 키보드 단축키 핸들러 (웹/데스크톱)
|
||||||
/// Space: 일시정지/재개, S: 속도 변경, H: 도움말, Esc: 설정
|
/// Space: 일시정지/재개, S: 속도 변경, H: 도움말, Esc: 설정
|
||||||
KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) {
|
KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) {
|
||||||
|
|||||||
@@ -785,7 +785,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
|||||||
child: AsciiDisintegrateWidget(
|
child: AsciiDisintegrateWidget(
|
||||||
characterLines: _deathAnimationMonsterLines!,
|
characterLines: _deathAnimationMonsterLines!,
|
||||||
duration: const Duration(milliseconds: 800),
|
duration: const Duration(milliseconds: 800),
|
||||||
textColor: widget.monsterGrade?.displayColor,
|
textColor: widget.monsterGrade?.displayColorCode != null
|
||||||
|
? Color(widget.monsterGrade!.displayColorCode!)
|
||||||
|
: null,
|
||||||
onComplete: _onDeathAnimationComplete,
|
onComplete: _onDeathAnimationComplete,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -756,9 +756,10 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
|||||||
final gradePrefix = (isKillTask && grade != null)
|
final gradePrefix = (isKillTask && grade != null)
|
||||||
? grade.displayPrefix
|
? grade.displayPrefix
|
||||||
: '';
|
: '';
|
||||||
final gradeColor = (isKillTask && grade != null)
|
final gradeColorCode = (isKillTask && grade != null)
|
||||||
? grade.displayColor
|
? grade.displayColorCode
|
||||||
: null;
|
: null;
|
||||||
|
final gradeColor = gradeColorCode != null ? Color(gradeColorCode) : null;
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
|||||||
@@ -169,9 +169,10 @@ class TaskProgressPanel extends StatelessWidget {
|
|||||||
final gradePrefix = (isKillTask && grade != null)
|
final gradePrefix = (isKillTask && grade != null)
|
||||||
? grade.displayPrefix
|
? grade.displayPrefix
|
||||||
: '';
|
: '';
|
||||||
final gradeColor = (isKillTask && grade != null)
|
final gradeColorCode = (isKillTask && grade != null)
|
||||||
? grade.displayColor
|
? grade.displayColorCode
|
||||||
: null;
|
: null;
|
||||||
|
final gradeColor = gradeColorCode != null ? Color(gradeColorCode) : null;
|
||||||
|
|
||||||
return Text.rich(
|
return Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
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/l10n/app_localizations.dart';
|
||||||
import 'package:asciineverdie/src/core/engine/debug_settings_service.dart';
|
import 'package:asciineverdie/src/core/engine/debug_settings_service.dart';
|
||||||
import 'package:asciineverdie/src/core/storage/settings_repository.dart';
|
import 'package:asciineverdie/src/core/storage/settings_repository.dart';
|
||||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||||
@@ -145,7 +146,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
// 디버그 섹션 (디버그 모드에서만 표시)
|
// 디버그 섹션 (디버그 모드에서만 표시)
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const _RetroSectionTitle(title: 'DEBUG'),
|
_RetroSectionTitle(title: L10n.of(context).debugTitle),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildDebugSection(context),
|
_buildDebugSection(context),
|
||||||
],
|
],
|
||||||
@@ -308,7 +309,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'DEVELOPER TOOLS',
|
L10n.of(context).debugDeveloperTools,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
@@ -322,8 +323,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
// IAP 시뮬레이션 토글
|
// IAP 시뮬레이션 토글
|
||||||
_RetroDebugToggle(
|
_RetroDebugToggle(
|
||||||
icon: Icons.shopping_cart,
|
icon: Icons.shopping_cart,
|
||||||
label: 'IAP PURCHASED',
|
label: L10n.of(context).debugIapPurchased,
|
||||||
description: 'ON: 유료 유저로 동작 (광고 제거)',
|
description: L10n.of(context).debugIapPurchasedDesc,
|
||||||
value: _debugIapSimulated,
|
value: _debugIapSimulated,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await DebugSettingsService.instance.setIapSimulated(value);
|
await DebugSettingsService.instance.setIapSimulated(value);
|
||||||
@@ -346,7 +347,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
// 테스트 캐릭터 생성
|
// 테스트 캐릭터 생성
|
||||||
if (widget.onCreateTestCharacter != null) ...[
|
if (widget.onCreateTestCharacter != null) ...[
|
||||||
Text(
|
Text(
|
||||||
'현재 캐릭터를 레벨 100으로 수정하여\n명예의 전당에 등록합니다.',
|
L10n.of(context).debugTestCharacterDesc,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 7,
|
fontSize: 7,
|
||||||
@@ -358,7 +359,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: RetroTextButton(
|
child: RetroTextButton(
|
||||||
text: 'CREATE TEST CHARACTER',
|
text: L10n.of(context).debugCreateTestCharacter,
|
||||||
icon: Icons.science,
|
icon: Icons.science,
|
||||||
onPressed: _handleCreateTestCharacter,
|
onPressed: _handleCreateTestCharacter,
|
||||||
),
|
),
|
||||||
@@ -388,7 +389,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'OFFLINE HOURS',
|
L10n.of(context).debugOfflineHours,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 8,
|
fontSize: 8,
|
||||||
@@ -397,7 +398,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'복귀 보상 테스트 (재시작 시 적용)',
|
L10n.of(context).debugOfflineHoursDesc,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontFamily: 'PressStart2P',
|
fontFamily: 'PressStart2P',
|
||||||
fontSize: 6,
|
fontSize: 6,
|
||||||
@@ -435,14 +436,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
|||||||
final confirmed = await showDialog<bool>(
|
final confirmed = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => _RetroConfirmDialog(
|
builder: (context) => _RetroConfirmDialog(
|
||||||
title: 'CREATE TEST CHARACTER?',
|
title: L10n.of(context).debugCreateTestCharacterTitle,
|
||||||
message:
|
message: L10n.of(context).debugCreateTestCharacterMessage,
|
||||||
'현재 캐릭터가 레벨 100으로 변환되어\n'
|
confirmText: L10n.of(context).createButton,
|
||||||
'명예의 전당에 등록됩니다.\n\n'
|
cancelText: L10n.of(context).cancel.toUpperCase(),
|
||||||
'⚠️ 현재 세이브 파일이 삭제됩니다.\n'
|
|
||||||
'이 작업은 되돌릴 수 없습니다.',
|
|
||||||
confirmText: 'CREATE',
|
|
||||||
cancelText: 'CANCEL',
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user