From d71f06574584e249436d1f3f0766da2705124df7 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Thu, 8 Jan 2026 18:18:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(game):=20=EC=83=81=ED=99=A9=EB=B3=84=20BGM?= =?UTF-8?q?=20=EC=9E=AC=EC=83=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Act/엘리트/보스별 동적 BGM 선택 --- lib/src/features/game/game_play_screen.dart | 101 ++++++++++++++++---- 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/lib/src/features/game/game_play_screen.dart b/lib/src/features/game/game_play_screen.dart index e0b07b2..ef4dab5 100644 --- a/lib/src/features/game/game_play_screen.dart +++ b/lib/src/features/game/game_play_screen.dart @@ -13,6 +13,7 @@ import 'package:asciineverdie/src/core/engine/story_service.dart'; import 'package:asciineverdie/src/core/model/combat_event.dart'; import 'package:asciineverdie/src/core/l10n/game_data_l10n.dart'; import 'package:asciineverdie/src/core/model/game_state.dart'; +import 'package:asciineverdie/src/core/model/monster_grade.dart'; import 'package:asciineverdie/src/core/model/skill.dart'; import 'package:asciineverdie/src/core/notification/notification_service.dart'; import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic; @@ -99,6 +100,10 @@ class _GamePlayScreenState extends State // 오디오 상태 추적 (TaskType 기반) bool _wasInBattleTask = false; + // 사망/엔딩 상태 추적 (BGM 전환용) + bool _wasDead = false; + bool _wasComplete = false; + // 사운드 볼륨 상태 (모바일 설정 UI용) double _bgmVolume = 0.7; double _sfxVolume = 0.8; @@ -177,6 +182,38 @@ class _GamePlayScreenState extends State _resetSpecialAnimationAfterFrame(); } _lastPlotStageCount = state.progress.plotStageCount; + + // 사망/엔딩 BGM 전환 (Death/Ending BGM Transition) + _updateDeathEndingBgm(state); + } + + /// 사망/엔딩 BGM 전환 처리 + void _updateDeathEndingBgm(GameState state) { + final audio = widget.audioService; + if (audio == null) return; + + final isDead = state.isDead; + final isComplete = widget.controller.isComplete; + + // 엔딩 BGM (게임 클리어 시) + if (isComplete && !_wasComplete) { + audio.playBgm('ending'); + _wasComplete = true; + return; + } + + // 사망 BGM (isDead 상태 진입 시) + if (isDead && !_wasDead) { + audio.playBgm('death'); + _wasDead = true; + return; + } + + // 부활 시 사망 상태 리셋 (다음 사망 감지 가능하도록) + if (!isDead && _wasDead) { + _wasDead = false; + // 부활 후 BGM은 _updateBgmForTaskType에서 처리됨 + } } /// Phase 8: 전투 로그 추가 (Add Combat Log Entry) @@ -227,16 +264,7 @@ class _GamePlayScreenState extends State final isInBattleTask = taskType == TaskType.kill; if (isInBattleTask) { - // 전투 태스크: 보스 여부에 따라 BGM 선택 - 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'); - } + audio.playBgm(_getBattleBgm(state)); } else { // 비전투 태스크: 마을 BGM audio.playBgm('town'); @@ -245,6 +273,48 @@ class _GamePlayScreenState extends State _wasInBattleTask = isInBattleTask; } + /// 전투 BGM 결정 (몬스터 등급 + 레벨 + Act 고려) + /// + /// 우선순위: + /// 1. MonsterGrade.boss → 'act_boss' + /// 2. 레벨 기반 보스 (monsterLevel >= playerLevel + 5) → 'boss' + /// 3. MonsterGrade.elite → 'elite' + /// 4. Act별 일반 전투 → 'battle', 'battle_act4', 'battle_act5' + String _getBattleBgm(GameState state) { + final task = state.progress.currentTask; + final monsterGrade = task.monsterGrade; + final monsterLevel = task.monsterLevel ?? 0; + final playerLevel = state.traits.level; + + // 1. 등급 보스 (3% 확률로 등장하는 특수 보스) + if (monsterGrade == MonsterGrade.boss) { + return 'act_boss'; + } + + // 2. 레벨 기반 보스 (강적) + if (monsterLevel >= playerLevel + 5) { + return 'boss'; + } + + // 3. 엘리트 몬스터 (12% 확률) + if (monsterGrade == MonsterGrade.elite) { + return 'elite'; + } + + // 4. 일반 전투 (Act별 분기) + return _getBattleBgmForLevel(playerLevel); + } + + /// 레벨에 따른 전투 BGM 파일명 반환 (Act별 분기) + String _getBattleBgmForLevel(int playerLevel) { + final act = getActForLevel(playerLevel); + return switch (act) { + StoryAct.act4 => 'battle_act4', + StoryAct.act5 => 'battle_act5', + _ => 'battle', + }; + } + /// TaskType 기반 BGM 전환 (애니메이션과 동기화) /// /// 애니메이션은 TaskType으로 결정되므로, BGM도 동일한 기준 사용 @@ -258,11 +328,7 @@ class _GamePlayScreenState extends State // 전투 태스크 상태 결정 if (isInBattleTask) { - // 전투 태스크: 보스 여부에 따라 BGM 선택 - final monsterLevel = state.progress.currentTask.monsterLevel ?? 0; - final playerLevel = state.traits.level; - final isBoss = monsterLevel >= playerLevel + 5; - final expectedBgm = isBoss ? 'boss' : 'battle'; + final expectedBgm = _getBattleBgm(state); // 전환 시점이거나 현재 BGM이 일치하지 않으면 재생 if (!_wasInBattleTask || audio.currentBgm != expectedBgm) { @@ -408,11 +474,14 @@ class _GamePlayScreenState extends State // 게임 일시 정지 await widget.controller.pause(saveOnStop: false); + // 시네마틱 BGM 재생 + widget.audioService?.playBgm('act_cinemetic'); + if (mounted) { await showActCinematic(context, act); } - // 게임 재개 + // 게임 재개 (BGM은 _updateBgmForTaskType에서 복원됨) if (mounted) { await widget.controller.resume(); }