refactor(ui): 화면 및 위젯 정리
- GamePlayScreen build() 메서드 분할 (300→15 LOC) - 애니메이션/프로그레스 패널 개선 - 설정 화면 정리
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:asciineverdie/data/game_text_l10n.dart' as game_l10n;
|
||||
|
||||
/// 알림 타입 (Notification Type)
|
||||
enum NotificationType {
|
||||
levelUp, // 레벨업
|
||||
@@ -62,8 +64,8 @@ class NotificationService {
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.levelUp,
|
||||
title: 'LEVEL UP!',
|
||||
subtitle: 'Level $newLevel',
|
||||
title: game_l10n.notifyLevelUp,
|
||||
subtitle: game_l10n.notifyLevel(newLevel),
|
||||
data: {'level': newLevel},
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
@@ -75,7 +77,7 @@ class NotificationService {
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.questComplete,
|
||||
title: 'QUEST COMPLETE!',
|
||||
title: game_l10n.notifyQuestComplete,
|
||||
subtitle: questName,
|
||||
data: {'quest': questName},
|
||||
duration: const Duration(seconds: 2),
|
||||
@@ -87,8 +89,8 @@ class NotificationService {
|
||||
/// actNumber: 0=프롤로그, 1=Act I, 2=Act II, ...
|
||||
void showActComplete(int actNumber) {
|
||||
final title = actNumber == 0
|
||||
? 'PROLOGUE COMPLETE!'
|
||||
: 'ACT $actNumber COMPLETE!';
|
||||
? game_l10n.notifyPrologueComplete
|
||||
: game_l10n.notifyActComplete(actNumber);
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.actComplete,
|
||||
@@ -103,7 +105,7 @@ class NotificationService {
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.newSpell,
|
||||
title: 'NEW SPELL!',
|
||||
title: game_l10n.notifyNewSpell,
|
||||
subtitle: spellName,
|
||||
data: {'spell': spellName},
|
||||
duration: const Duration(seconds: 2),
|
||||
@@ -116,7 +118,7 @@ class NotificationService {
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.newEquipment,
|
||||
title: 'NEW EQUIPMENT!',
|
||||
title: game_l10n.notifyNewEquipment,
|
||||
subtitle: equipmentName,
|
||||
data: {'equipment': equipmentName, 'slot': slot},
|
||||
duration: const Duration(seconds: 2),
|
||||
@@ -129,7 +131,7 @@ class NotificationService {
|
||||
show(
|
||||
GameNotification(
|
||||
type: NotificationType.bossDefeat,
|
||||
title: 'BOSS DEFEATED!',
|
||||
title: game_l10n.notifyBossDefeated,
|
||||
subtitle: bossName,
|
||||
data: {'boss': bossName},
|
||||
duration: const Duration(seconds: 3),
|
||||
|
||||
@@ -217,9 +217,9 @@ class _AnimationPanel extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome, color: RetroColors.gold, size: 18),
|
||||
const SizedBox(width: 10),
|
||||
const Text(
|
||||
'ASCII NEVER DIE',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
L10n.of(context).appTitle.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 14,
|
||||
color: RetroColors.gold,
|
||||
@@ -272,7 +272,7 @@ class _ActionButtons extends StatelessWidget {
|
||||
final l10n = L10n.of(context);
|
||||
|
||||
return RetroPanel(
|
||||
title: 'MENU',
|
||||
title: L10n.of(context).menuTitle,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
|
||||
@@ -551,150 +551,135 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
|
||||
// 로케일 변경 시 전체 위젯 트리 강제 리빌드를 위한 Key
|
||||
final localeKey = ValueKey(game_l10n.currentGameLocale);
|
||||
|
||||
// 캐로셀 레이아웃 사용 여부 확인
|
||||
if (_shouldUseCarouselLayout(context)) {
|
||||
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 {
|
||||
// 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 _buildMobileLayout(context, state, localeKey);
|
||||
}
|
||||
|
||||
// 기존 데스크톱 레이아웃 (레트로 스타일)
|
||||
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(
|
||||
key: localeKey,
|
||||
notificationService: _notificationService,
|
||||
@@ -710,135 +695,16 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
}
|
||||
}
|
||||
},
|
||||
// 웹/데스크톱 키보드 단축키 지원
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) => _handleKeyboardShortcut(event, context),
|
||||
child: Scaffold(
|
||||
backgroundColor: RetroColors.deepBrown,
|
||||
appBar: 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
appBar: _buildDesktopAppBar(context, state),
|
||||
body: Stack(
|
||||
children: [
|
||||
// 메인 게임 UI
|
||||
Column(
|
||||
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,
|
||||
),
|
||||
_buildDesktopMainContent(state),
|
||||
..._buildOverlays(state),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -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: 설정
|
||||
KeyEventResult _handleKeyboardShortcut(KeyEvent event, BuildContext context) {
|
||||
|
||||
@@ -785,7 +785,9 @@ class _AsciiAnimationCardState extends State<AsciiAnimationCard> {
|
||||
child: AsciiDisintegrateWidget(
|
||||
characterLines: _deathAnimationMonsterLines!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
textColor: widget.monsterGrade?.displayColor,
|
||||
textColor: widget.monsterGrade?.displayColorCode != null
|
||||
? Color(widget.monsterGrade!.displayColorCode!)
|
||||
: null,
|
||||
onComplete: _onDeathAnimationComplete,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -756,9 +756,10 @@ class _EnhancedAnimationPanelState extends State<EnhancedAnimationPanel>
|
||||
final gradePrefix = (isKillTask && grade != null)
|
||||
? grade.displayPrefix
|
||||
: '';
|
||||
final gradeColor = (isKillTask && grade != null)
|
||||
? grade.displayColor
|
||||
final gradeColorCode = (isKillTask && grade != null)
|
||||
? grade.displayColorCode
|
||||
: null;
|
||||
final gradeColor = gradeColorCode != null ? Color(gradeColorCode) : null;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
||||
@@ -169,9 +169,10 @@ class TaskProgressPanel extends StatelessWidget {
|
||||
final gradePrefix = (isKillTask && grade != null)
|
||||
? grade.displayPrefix
|
||||
: '';
|
||||
final gradeColor = (isKillTask && grade != null)
|
||||
? grade.displayColor
|
||||
final gradeColorCode = (isKillTask && grade != null)
|
||||
? grade.displayColorCode
|
||||
: null;
|
||||
final gradeColor = gradeColorCode != null ? Color(gradeColorCode) : null;
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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/storage/settings_repository.dart';
|
||||
import 'package:asciineverdie/src/shared/retro_colors.dart';
|
||||
@@ -145,7 +146,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// 디버그 섹션 (디버그 모드에서만 표시)
|
||||
if (kDebugMode) ...[
|
||||
const SizedBox(height: 16),
|
||||
const _RetroSectionTitle(title: 'DEBUG'),
|
||||
_RetroSectionTitle(title: L10n.of(context).debugTitle),
|
||||
const SizedBox(height: 8),
|
||||
_buildDebugSection(context),
|
||||
],
|
||||
@@ -308,7 +309,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'DEVELOPER TOOLS',
|
||||
L10n.of(context).debugDeveloperTools,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 10,
|
||||
@@ -322,8 +323,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// IAP 시뮬레이션 토글
|
||||
_RetroDebugToggle(
|
||||
icon: Icons.shopping_cart,
|
||||
label: 'IAP PURCHASED',
|
||||
description: 'ON: 유료 유저로 동작 (광고 제거)',
|
||||
label: L10n.of(context).debugIapPurchased,
|
||||
description: L10n.of(context).debugIapPurchasedDesc,
|
||||
value: _debugIapSimulated,
|
||||
onChanged: (value) async {
|
||||
await DebugSettingsService.instance.setIapSimulated(value);
|
||||
@@ -346,7 +347,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
// 테스트 캐릭터 생성
|
||||
if (widget.onCreateTestCharacter != null) ...[
|
||||
Text(
|
||||
'현재 캐릭터를 레벨 100으로 수정하여\n명예의 전당에 등록합니다.',
|
||||
L10n.of(context).debugTestCharacterDesc,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 7,
|
||||
@@ -358,7 +359,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: RetroTextButton(
|
||||
text: 'CREATE TEST CHARACTER',
|
||||
text: L10n.of(context).debugCreateTestCharacter,
|
||||
icon: Icons.science,
|
||||
onPressed: _handleCreateTestCharacter,
|
||||
),
|
||||
@@ -388,7 +389,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'OFFLINE HOURS',
|
||||
L10n.of(context).debugOfflineHours,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 8,
|
||||
@@ -397,7 +398,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'복귀 보상 테스트 (재시작 시 적용)',
|
||||
L10n.of(context).debugOfflineHoursDesc,
|
||||
style: TextStyle(
|
||||
fontFamily: 'PressStart2P',
|
||||
fontSize: 6,
|
||||
@@ -435,14 +436,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => _RetroConfirmDialog(
|
||||
title: 'CREATE TEST CHARACTER?',
|
||||
message:
|
||||
'현재 캐릭터가 레벨 100으로 변환되어\n'
|
||||
'명예의 전당에 등록됩니다.\n\n'
|
||||
'⚠️ 현재 세이브 파일이 삭제됩니다.\n'
|
||||
'이 작업은 되돌릴 수 없습니다.',
|
||||
confirmText: 'CREATE',
|
||||
cancelText: 'CANCEL',
|
||||
title: L10n.of(context).debugCreateTestCharacterTitle,
|
||||
message: L10n.of(context).debugCreateTestCharacterMessage,
|
||||
confirmText: L10n.of(context).createButton,
|
||||
cancelText: L10n.of(context).cancel.toUpperCase(),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user