From 72676485d37da7cb9eb95a22389e1bedb3be7285 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 31 Dec 2025 01:33:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(audio):=20=ED=99=94=EB=A9=B4=EB=93=A4=20?= =?UTF-8?q?=EC=B1=84=EB=84=90=EB=B3=84=20SFX=20API=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - game_play_screen: playPlayerSfx/playMonsterSfx 분리 사용 - settings_screen: 오디오 설정 UI 개선 --- lib/src/features/game/game_play_screen.dart | 39 +++++++++++++------ .../features/settings/settings_screen.dart | 18 ++++++++- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index 4429437..415f4e1 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -129,8 +129,8 @@ class _GamePlayScreenState extends State '${game_l10n.uiLevelUp} Lv.${state.traits.level}', CombatLogType.levelUp, ); - // 오디오: 레벨업 SFX - widget.audioService?.playSfx('level_up'); + // 오디오: 레벨업 SFX (플레이어 채널) + widget.audioService?.playPlayerSfx('level_up'); _resetSpecialAnimationAfterFrame(); // Phase 9: Act 변경 감지 (레벨 기반) @@ -168,8 +168,8 @@ class _GamePlayScreenState extends State CombatLogType.questComplete, ); } - // 오디오: 퀘스트 완료 SFX - widget.audioService?.playSfx('quest_complete'); + // 오디오: 퀘스트 완료 SFX (플레이어 채널) + widget.audioService?.playPlayerSfx('quest_complete'); _resetSpecialAnimationAfterFrame(); } _lastQuestCount = state.progress.questCount; @@ -283,26 +283,33 @@ class _GamePlayScreenState extends State _wasInBattleTask = isInBattleTask; } - /// 전투 이벤트에 따른 SFX 재생 + /// 전투 이벤트별 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'); + audio.playPlayerSfx('attack'); case CombatEventType.playerSkill: - audio.playSfx('skill'); + audio.playPlayerSfx('skill'); case CombatEventType.playerHeal: case CombatEventType.playerPotion: - audio.playSfx('item'); case CombatEventType.potionDrop: - audio.playSfx('item'); + audio.playPlayerSfx('item'); case CombatEventType.playerBuff: case CombatEventType.playerDebuff: - audio.playSfx('skill'); + audio.playPlayerSfx('skill'); + + // 몬스터 채널: 몬스터가 발생시키는 이펙트 (플레이어 피격) + case CombatEventType.monsterAttack: + audio.playMonsterSfx('hit'); + + // SFX 없음 case CombatEventType.dotTick: // DOT 틱은 SFX 없음 (너무 자주 발생) break; @@ -685,6 +692,14 @@ class _GamePlayScreenState extends State ); } }, + onBgmVolumeChange: (volume) { + setState(() => _bgmVolume = volume); + widget.audioService?.setBgmVolume(volume); + }, + onSfxVolumeChange: (volume) { + setState(() => _sfxVolume = volume); + widget.audioService?.setSfxVolume(volume); + }, ); } diff --git a/lib/src/features/settings/settings_screen.dart b/lib/src/features/settings/settings_screen.dart index e20f364..8831ee9 100644 --- a/lib/src/features/settings/settings_screen.dart +++ b/lib/src/features/settings/settings_screen.dart @@ -13,6 +13,8 @@ class SettingsScreen extends StatefulWidget { required this.currentThemeMode, required this.onThemeModeChange, this.onLocaleChange, + this.onBgmVolumeChange, + this.onSfxVolumeChange, }); final SettingsRepository settingsRepository; @@ -20,6 +22,12 @@ class SettingsScreen extends StatefulWidget { final void Function(ThemeMode mode) onThemeModeChange; final void Function(String locale)? onLocaleChange; + /// BGM 볼륨 변경 콜백 (AudioService 연동용) + final void Function(double volume)? onBgmVolumeChange; + + /// SFX 볼륨 변경 콜백 (AudioService 연동용) + final void Function(double volume)? onSfxVolumeChange; + @override State createState() => _SettingsScreenState(); @@ -30,6 +38,8 @@ class SettingsScreen extends StatefulWidget { required ThemeMode currentThemeMode, required void Function(ThemeMode mode) onThemeModeChange, void Function(String locale)? onLocaleChange, + void Function(double volume)? onBgmVolumeChange, + void Function(double volume)? onSfxVolumeChange, }) { return showModalBottomSheet( context: context, @@ -45,6 +55,8 @@ class SettingsScreen extends StatefulWidget { currentThemeMode: currentThemeMode, onThemeModeChange: onThemeModeChange, onLocaleChange: onLocaleChange, + onBgmVolumeChange: onBgmVolumeChange, + onSfxVolumeChange: onSfxVolumeChange, ), ), ); @@ -147,6 +159,7 @@ class _SettingsScreenState extends State { onChanged: (value) { setState(() => _bgmVolume = value); widget.settingsRepository.saveBgmVolume(value); + widget.onBgmVolumeChange?.call(value); }, ), const SizedBox(height: 8), @@ -157,6 +170,7 @@ class _SettingsScreenState extends State { onChanged: (value) { setState(() => _sfxVolume = value); widget.settingsRepository.saveSfxVolume(value); + widget.onSfxVolumeChange?.call(value); }, ), const SizedBox(height: 24), @@ -241,7 +255,7 @@ class _SettingsScreenState extends State { Icon( icon, color: isSelected - ? theme.colorScheme.primary + ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.onSurface, ), const SizedBox(height: 4), @@ -251,7 +265,7 @@ class _SettingsScreenState extends State { fontSize: 12, fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, color: isSelected - ? theme.colorScheme.primary + ? theme.colorScheme.onPrimaryContainer : theme.colorScheme.onSurface, ), ),