Compare commits

...

3 Commits

Author SHA1 Message Date
JiWoong Sul
d71f065745 feat(game): 상황별 BGM 재생 로직 추가
- Act/엘리트/보스별 동적 BGM 선택
2026-01-08 18:18:14 +09:00
JiWoong Sul
929b8a7f96 feat(audio): BgmType 확장 및 파일명 매핑 추가
- battleAct4, battleAct5: Act별 전투 BGM
- actBoss: Act 보스 전용 BGM
- elite: 엘리트 몬스터 BGM
- death: 사망 BGM
- actCinematic: Act 전환 시네마틱
- ending: 엔딩 BGM
2026-01-08 18:18:08 +09:00
JiWoong Sul
38b9955b73 feat(audio): 추가 BGM 파일 추가
- act_boss: Act 보스 전용 BGM
- battle_act4/5: Act IV/V 전투 BGM
- elite: 엘리트 몬스터 BGM
- death: 사망 BGM
- act_cinemetic: Act 전환 시네마틱 BGM
- ending: 엔딩 BGM
2026-01-08 18:18:02 +09:00
9 changed files with 106 additions and 17 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/audio/bgm/death.mp3 Normal file

Binary file not shown.

BIN
assets/audio/bgm/elite.mp3 Normal file

Binary file not shown.

BIN
assets/audio/bgm/ending.mp3 Normal file

Binary file not shown.

View File

@@ -443,8 +443,15 @@ enum BgmType {
title, title,
town, town,
battle, battle,
battleAct4, // Act IV 전용 전투 BGM
battleAct5, // Act V 전용 전투 BGM
boss, boss,
actBoss, // Act 보스 전용 BGM
elite, // 엘리트 몬스터 전투 BGM
victory, victory,
death, // 사망 BGM
actCinematic, // Act 전환 시네마틱 BGM
ending, // 엔딩 BGM
} }
/// SFX 타입 열거형 /// SFX 타입 열거형
@@ -463,7 +470,20 @@ enum SfxType {
/// BgmType을 파일명으로 변환 /// BgmType을 파일명으로 변환
extension BgmTypeExtension on BgmType { extension BgmTypeExtension on BgmType {
String get fileName => name; String get fileName => switch (this) {
BgmType.title => 'title',
BgmType.town => 'town',
BgmType.battle => 'battle',
BgmType.battleAct4 => 'battle_act4',
BgmType.battleAct5 => 'battle_act5',
BgmType.boss => 'boss',
BgmType.actBoss => 'act_boss',
BgmType.elite => 'elite',
BgmType.victory => 'victory',
BgmType.death => 'death',
BgmType.actCinematic => 'act_cinemetic', // 파일명 오타 유지
BgmType.ending => 'ending',
};
} }
/// SfxType을 파일명으로 변환 /// SfxType을 파일명으로 변환

View File

@@ -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/model/combat_event.dart';
import 'package:asciineverdie/src/core/l10n/game_data_l10n.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/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/model/skill.dart';
import 'package:asciineverdie/src/core/notification/notification_service.dart'; import 'package:asciineverdie/src/core/notification/notification_service.dart';
import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic; import 'package:asciineverdie/src/core/util/pq_logic.dart' as pq_logic;
@@ -99,6 +100,10 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 오디오 상태 추적 (TaskType 기반) // 오디오 상태 추적 (TaskType 기반)
bool _wasInBattleTask = false; bool _wasInBattleTask = false;
// 사망/엔딩 상태 추적 (BGM 전환용)
bool _wasDead = false;
bool _wasComplete = false;
// 사운드 볼륨 상태 (모바일 설정 UI용) // 사운드 볼륨 상태 (모바일 설정 UI용)
double _bgmVolume = 0.7; double _bgmVolume = 0.7;
double _sfxVolume = 0.8; double _sfxVolume = 0.8;
@@ -177,6 +182,38 @@ class _GamePlayScreenState extends State<GamePlayScreen>
_resetSpecialAnimationAfterFrame(); _resetSpecialAnimationAfterFrame();
} }
_lastPlotStageCount = state.progress.plotStageCount; _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) /// Phase 8: 전투 로그 추가 (Add Combat Log Entry)
@@ -227,16 +264,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
final isInBattleTask = taskType == TaskType.kill; final isInBattleTask = taskType == TaskType.kill;
if (isInBattleTask) { if (isInBattleTask) {
// 전투 태스크: 보스 여부에 따라 BGM 선택 audio.playBgm(_getBattleBgm(state));
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 { } else {
// 비전투 태스크: 마을 BGM // 비전투 태스크: 마을 BGM
audio.playBgm('town'); audio.playBgm('town');
@@ -245,6 +273,48 @@ class _GamePlayScreenState extends State<GamePlayScreen>
_wasInBattleTask = isInBattleTask; _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 전환 (애니메이션과 동기화)
/// ///
/// 애니메이션은 TaskType으로 결정되므로, BGM도 동일한 기준 사용 /// 애니메이션은 TaskType으로 결정되므로, BGM도 동일한 기준 사용
@@ -258,11 +328,7 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 전투 태스크 상태 결정 // 전투 태스크 상태 결정
if (isInBattleTask) { if (isInBattleTask) {
// 전투 태스크: 보스 여부에 따라 BGM 선택 final expectedBgm = _getBattleBgm(state);
final monsterLevel = state.progress.currentTask.monsterLevel ?? 0;
final playerLevel = state.traits.level;
final isBoss = monsterLevel >= playerLevel + 5;
final expectedBgm = isBoss ? 'boss' : 'battle';
// 전환 시점이거나 현재 BGM이 일치하지 않으면 재생 // 전환 시점이거나 현재 BGM이 일치하지 않으면 재생
if (!_wasInBattleTask || audio.currentBgm != expectedBgm) { if (!_wasInBattleTask || audio.currentBgm != expectedBgm) {
@@ -408,11 +474,14 @@ class _GamePlayScreenState extends State<GamePlayScreen>
// 게임 일시 정지 // 게임 일시 정지
await widget.controller.pause(saveOnStop: false); await widget.controller.pause(saveOnStop: false);
// 시네마틱 BGM 재생
widget.audioService?.playBgm('act_cinemetic');
if (mounted) { if (mounted) {
await showActCinematic(context, act); await showActCinematic(context, act);
} }
// 게임 재개 // 게임 재개 (BGM은 _updateBgmForTaskType에서 복원됨)
if (mounted) { if (mounted) {
await widget.controller.resume(); await widget.controller.resume();
} }