feat(ui): 게임 화면 및 설정 화면 개선
- GamePlayScreen 개선 - GameSessionController 확장 - MobileCarouselLayout 기능 추가 - SettingsScreen 테스트 기능 추가
This commit is contained in:
@@ -102,6 +102,9 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
|
||||
// 사망/엔딩 상태 추적 (BGM 전환용)
|
||||
bool _wasDead = false;
|
||||
|
||||
// 사운드 디바운스 추적 (배속 시 사운드 누락 방지)
|
||||
final Map<String, int> _lastSfxPlayTime = {};
|
||||
bool _wasComplete = false;
|
||||
|
||||
// 사운드 볼륨 상태 (모바일 설정 UI용)
|
||||
@@ -344,47 +347,53 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
_wasInBattleTask = isInBattleTask;
|
||||
}
|
||||
|
||||
/// 전투 이벤트별 SFX 재생 (채널 분리)
|
||||
/// 전투 이벤트별 SFX 재생 (채널 분리 + 디바운스)
|
||||
///
|
||||
/// 플레이어 이펙트와 몬스터 이펙트를 별도 채널에서 재생하여
|
||||
/// 사운드 충돌을 방지하고 완료를 보장합니다.
|
||||
/// 배속 모드에서는 디바운스를 적용하여 사운드 누락을 방지합니다.
|
||||
void _playCombatEventSfx(CombatEvent event) {
|
||||
final audio = widget.audioService;
|
||||
if (audio == null) return;
|
||||
|
||||
switch (event.type) {
|
||||
// 플레이어 채널: 플레이어가 발생시키는 이펙트
|
||||
case CombatEventType.playerAttack:
|
||||
audio.playPlayerSfx('attack');
|
||||
case CombatEventType.playerSkill:
|
||||
audio.playPlayerSfx('skill');
|
||||
case CombatEventType.playerHeal:
|
||||
case CombatEventType.playerPotion:
|
||||
case CombatEventType.potionDrop:
|
||||
audio.playPlayerSfx('item');
|
||||
case CombatEventType.playerBuff:
|
||||
case CombatEventType.playerDebuff:
|
||||
audio.playPlayerSfx('skill');
|
||||
// 사운드 이름 결정
|
||||
final sfxName = switch (event.type) {
|
||||
CombatEventType.playerAttack => 'attack',
|
||||
CombatEventType.playerSkill => 'skill',
|
||||
CombatEventType.playerHeal => 'item',
|
||||
CombatEventType.playerPotion => 'item',
|
||||
CombatEventType.potionDrop => 'item',
|
||||
CombatEventType.playerBuff => 'skill',
|
||||
CombatEventType.playerDebuff => 'skill',
|
||||
CombatEventType.monsterAttack => 'hit',
|
||||
CombatEventType.playerEvade => 'evade',
|
||||
CombatEventType.monsterEvade => 'evade',
|
||||
CombatEventType.playerBlock => 'block',
|
||||
CombatEventType.playerParry => 'parry',
|
||||
CombatEventType.dotTick => null, // DOT 틱은 SFX 없음
|
||||
};
|
||||
|
||||
// 몬스터 채널: 몬스터가 발생시키는 이펙트 (플레이어 피격)
|
||||
case CombatEventType.monsterAttack:
|
||||
audio.playMonsterSfx('hit');
|
||||
if (sfxName == null) return;
|
||||
|
||||
// 회피/방어 SFX (Phase 11)
|
||||
case CombatEventType.playerEvade:
|
||||
audio.playPlayerSfx('evade');
|
||||
case CombatEventType.monsterEvade:
|
||||
// 몬스터 회피 = 플레이어 공격 빗나감 (evade SFX)
|
||||
audio.playPlayerSfx('evade');
|
||||
case CombatEventType.playerBlock:
|
||||
audio.playPlayerSfx('block');
|
||||
case CombatEventType.playerParry:
|
||||
audio.playPlayerSfx('parry');
|
||||
// 디바운스 체크 (배속 시 같은 사운드 100ms 내 중복 재생 방지)
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final lastTime = _lastSfxPlayTime[sfxName] ?? 0;
|
||||
final speedMultiplier = widget.controller.loop?.speedMultiplier ?? 1;
|
||||
|
||||
// SFX 없음
|
||||
case CombatEventType.dotTick:
|
||||
// DOT 틱은 SFX 없음 (너무 자주 발생)
|
||||
break;
|
||||
// 배속이 높을수록 디바운스 간격 증가 (1x=50ms, 8x=150ms)
|
||||
final debounceMs = 50 + (speedMultiplier - 1) * 15;
|
||||
|
||||
if (now - lastTime < debounceMs) {
|
||||
return; // 디바운스 기간 내 → 스킵
|
||||
}
|
||||
_lastSfxPlayTime[sfxName] = now;
|
||||
|
||||
// 채널별 재생
|
||||
final isMonsterSfx = event.type == CombatEventType.monsterAttack;
|
||||
if (isMonsterSfx) {
|
||||
audio.playMonsterSfx(sfxName);
|
||||
} else {
|
||||
audio.playPlayerSfx(sfxName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -743,6 +752,14 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
setState(() => _sfxVolume = volume);
|
||||
widget.audioService?.setSfxVolume(volume);
|
||||
},
|
||||
onCreateTestCharacter: () async {
|
||||
final navigator = Navigator.of(context);
|
||||
final success = await widget.controller.createTestCharacter();
|
||||
if (success && mounted) {
|
||||
// 프론트 화면으로 이동
|
||||
navigator.popUntil((route) => route.isFirst);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -886,6 +903,13 @@ class _GamePlayScreenState extends State<GamePlayScreen>
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
// 사망 오버레이
|
||||
if (state.isDead && state.deathInfo != null)
|
||||
|
||||
Reference in New Issue
Block a user