feat(ui): 도움말 다이얼로그 및 UI 개선

- HelpDialog 추가
- 게임 화면에 통계/도움말 버튼 추가
- CombatLog에 디버프 이벤트 표시
- AudioService mp3 확장자 지원
- 설정 텍스트 l10n 추가
This commit is contained in:
JiWoong Sul
2025-12-30 15:58:40 +09:00
parent d64b9654a3
commit 18af93824b
10 changed files with 1028 additions and 32 deletions

View File

@@ -32,7 +32,10 @@ import 'package:askiineverdie/src/features/game/widgets/task_progress_panel.dart
import 'package:askiineverdie/src/features/game/widgets/active_buff_panel.dart';
import 'package:askiineverdie/src/features/game/layouts/mobile_carousel_layout.dart';
import 'package:askiineverdie/src/features/settings/settings_screen.dart';
import 'package:askiineverdie/src/features/game/widgets/statistics_dialog.dart';
import 'package:askiineverdie/src/features/game/widgets/help_dialog.dart';
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
import 'package:askiineverdie/src/core/audio/audio_service.dart';
/// 게임 진행 화면 (Main.dfm 기반 3패널 레이아웃)
///
@@ -41,6 +44,7 @@ class GamePlayScreen extends StatefulWidget {
const GamePlayScreen({
super.key,
required this.controller,
this.audioService,
this.forceCarouselLayout = false,
this.forceDesktopLayout = false,
this.onThemeModeChange,
@@ -49,6 +53,9 @@ class GamePlayScreen extends StatefulWidget {
final GameSessionController controller;
/// 오디오 서비스 (BGM/SFX 재생)
final AudioService? audioService;
/// 테스트 모드: 웹에서도 모바일 캐로셀 레이아웃 강제 사용
final bool forceCarouselLayout;
@@ -89,6 +96,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 전투 이벤트 추적 (마지막 처리된 이벤트 수)
int _lastProcessedEventCount = 0;
// 오디오 상태 추적
bool _wasInCombat = false;
// 사운드 볼륨 상태 (모바일 설정 UI용)
double _bgmVolume = 0.7;
double _sfxVolume = 0.8;
void _checkSpecialEvents(GameState state) {
// Phase 8: 태스크 변경 시 로그 추가
final currentCaption = state.progress.currentTask.caption;
@@ -102,6 +116,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 전투 이벤트 처리 (Combat Events)
_processCombatEvents(state);
// 오디오: 전투 상태 변경 시 BGM 전환
_updateBgmForCombatState(state);
// 레벨업 감지
if (state.traits.level > _lastLevel && _lastLevel > 0) {
_specialAnimation = AsciiAnimationType.levelUp;
@@ -110,6 +127,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
'${game_l10n.uiLevelUp} Lv.${state.traits.level}',
CombatLogType.levelUp,
);
// 오디오: 레벨업 SFX
widget.audioService?.playSfx('level_up');
_resetSpecialAnimationAfterFrame();
// Phase 9: Act 변경 감지 (레벨 기반)
@@ -147,6 +166,8 @@ class _GamePlayScreenState extends State<GamePlayScreen>
CombatLogType.questComplete,
);
}
// 오디오: 퀘스트 완료 SFX
widget.audioService?.playSfx('quest_complete');
_resetSpecialAnimationAfterFrame();
}
_lastQuestCount = state.progress.questCount;
@@ -192,11 +213,74 @@ class _GamePlayScreenState extends State<GamePlayScreen>
for (final event in newEvents) {
final (message, type) = _formatCombatEvent(event);
_addCombatLog(message, type);
// 오디오: 전투 이벤트에 따른 SFX 재생
_playCombatEventSfx(event);
}
_lastProcessedEventCount = events.length;
}
/// 전투 상태에 따른 BGM 전환
void _updateBgmForCombatState(GameState state) {
final audio = widget.audioService;
if (audio == null) return;
final combat = state.progress.currentCombat;
final isInCombat = combat != null && combat.isActive;
if (isInCombat && !_wasInCombat) {
// 전투 시작: 보스 여부에 따라 BGM 선택
// 몬스터 레벨이 플레이어보다 5 이상 높으면 보스로 간주
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
final playerLevel = state.traits.level;
final isBoss = monsterLevel >= playerLevel + 5;
if (isBoss) {
audio.playBgm('boss');
} else {
audio.playBgm('battle');
}
} else if (!isInCombat && _wasInCombat) {
// 전투 종료: 마을 BGM으로 복귀
audio.playBgm('town');
}
_wasInCombat = isInCombat;
}
/// 전투 이벤트에 따른 SFX 재생
void _playCombatEventSfx(CombatEvent event) {
final audio = widget.audioService;
if (audio == null) return;
switch (event.type) {
case CombatEventType.playerAttack:
audio.playSfx('attack');
case CombatEventType.monsterAttack:
audio.playSfx('hit');
case CombatEventType.playerSkill:
audio.playSfx('skill');
case CombatEventType.playerHeal:
case CombatEventType.playerPotion:
audio.playSfx('item');
case CombatEventType.potionDrop:
audio.playSfx('item');
case CombatEventType.playerBuff:
case CombatEventType.playerDebuff:
audio.playSfx('skill');
case CombatEventType.dotTick:
// DOT 틱은 SFX 없음 (너무 자주 발생)
break;
case CombatEventType.playerEvade:
case CombatEventType.monsterEvade:
case CombatEventType.playerBlock:
case CombatEventType.playerParry:
// 회피/방어는 별도 SFX 없음
break;
}
}
/// 전투 이벤트를 메시지와 타입으로 변환
(String, CombatLogType) _formatCombatEvent(CombatEvent event) {
final target = event.targetName ?? '';
@@ -256,6 +340,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
game_l10n.combatBuffActivated(skillName),
CombatLogType.buff,
),
CombatEventType.playerDebuff => (
game_l10n.combatDebuffApplied(skillName, target),
CombatLogType.debuff,
),
CombatEventType.dotTick => (
game_l10n.combatDotTick(skillName, event.damage),
CombatLogType.dotTick,
@@ -361,6 +449,29 @@ class _GamePlayScreenState extends State<GamePlayScreen>
_lastQuestCount = state.progress.questCount;
_lastPlotStageCount = state.progress.plotStageCount;
_lastAct = getActForLevel(state.traits.level);
// 초기 전투 상태 확인
final combat = state.progress.currentCombat;
_wasInCombat = combat != null && combat.isActive;
}
// 누적 통계 로드
widget.controller.loadCumulativeStats();
// 초기 BGM 재생 (마을 테마)
widget.audioService?.playBgm('town');
// 오디오 볼륨 초기화
_initAudioVolumes();
}
/// 오디오 볼륨 초기화 (설정에서 로드)
Future<void> _initAudioVolumes() async {
final audio = widget.audioService;
if (audio != null) {
_bgmVolume = audio.bgmVolume;
_sfxVolume = audio.sfxVolume;
if (mounted) setState(() {});
}
}
@@ -465,6 +576,15 @@ class _GamePlayScreenState extends State<GamePlayScreen>
return platform == TargetPlatform.iOS || platform == TargetPlatform.android;
}
/// 통계 다이얼로그 표시
void _showStatisticsDialog(BuildContext context) {
StatisticsDialog.show(
context,
session: widget.controller.sessionStats,
cumulative: widget.controller.cumulativeStats,
);
}
/// 설정 화면 표시
void _showSettingsScreen(BuildContext context) {
final settingsRepo = SettingsRepository();
@@ -614,6 +734,17 @@ class _GamePlayScreenState extends State<GamePlayScreen>
},
currentThemeMode: widget.currentThemeMode,
onThemeModeChange: widget.onThemeModeChange,
// 사운드 설정
bgmVolume: _bgmVolume,
sfxVolume: _sfxVolume,
onBgmVolumeChange: (volume) {
setState(() => _bgmVolume = volume);
widget.audioService?.setBgmVolume(volume);
},
onSfxVolumeChange: (volume) {
setState(() => _sfxVolume = volume);
widget.audioService?.setSfxVolume(volume);
},
),
// 사망 오버레이
if (state.isDead && state.deathInfo != null)
@@ -666,6 +797,18 @@ class _GamePlayScreenState extends State<GamePlayScreen>
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),