40250 classic: increment 50 (W4) — map BGM selection + background volume save/restore

§9.1 map BGM selection (bgm_director.gd):
- server bgm_changed(name, volume): non-empty name plays it as field music;
  empty name falls back to musicInfo.fieldMusic (METIN2THEMA = "M2BG.mp3")
- volume < 0 (server unspecified, GC_MAIN_CHARACTER3_BGM) -> default
  1/5*0.1 = 0.02; otherwise server volume; volume applied before play_bgm()
  fade-in (matches game.py:190 SetMusicVolume/SetSoundVolume then FadeInMusic)
- connect M2Client.world_reset -> re-run selection on map change
- new musicinfo.gd: METIN2THEMA/loginMusic/createMusic/selectMusic/fieldMusic
  constants + save/load_last_play_field_music -> user://BGM/lastplay.inf

§9.2 background/foreground volume save-restore:
- audio.gd: save_volume()/restore_volume() (backup fields echo reference
  m_fBackupMusicVolume / m_fBackupSoundVolume) + set_music_volume()
- app_lifecycle.gd: _enter_background() uses save_volume() + mute instead of
  stop_bgm(); _exit_background() calls restore_volume(); _bgm_name never cleared

§9.3 lifecycle ownership: DOC ONLY — design confirmed, deferred to Phase 2 / W0
(AppFlow sole lifecycle owner; M2Client drops NOTIFICATION_* keeps suspend/resume).
game_scene.gd / app_flow.gd / m2_client.* left untouched per workflow constraints.

Tests: bgm_test.gd (new) PASS; gamescene_test / netplay_test / p9_test green.
Docs: CLIENT-GAP.md increment 50 bullet + W4 progress row; CLIENT-GAP-FIX.md
§9.1 §9.2 状态/改动/测试/验收, §9.3 deferred note, C.5 batch record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014yvAPqPivoY7vmBbzgqK4W
This commit is contained in:
shenlei
2026-09-02 15:31:58 +09:00
co-authored by Claude Sonnet 5
parent f32064c74a
commit 9732f2799e
10 changed files with 393 additions and 11 deletions
+50 -8
View File
@@ -1,8 +1,14 @@
# BgmDirector —— 把 M2Client.bgm_changed 信号翻译成 Audio 的 BGM 播放。
#
# 存在的理由:§9.1 的地图 BGM 消费点,从 net_world.gd 里拆出来,避免 W2(可见性)
# 和 W4(音频)同时改 net_world.gd。真正的「地图 → 曲目解析和音量应用由 W4
# 这里补全(见 docs/CLIENT-GAP-FIX.md §9.1)。
# 和 W4(音频)同时改 net_world.gd。地图 → 曲目解析和音量应用在这里(§9.1)。
#
# 选曲逻辑对标 game.py:190GameWindow.Open 内):
# snd.SetMusicVolume(...); snd.SetSoundVolume(...) # 先应用音量
# name = net.GetFieldMusicFileName()
# if name: snd.FadeInMusic("BGM/" + name)
# elif musicInfo.fieldMusic: snd.FadeInMusic("BGM/" + musicInfo.fieldMusic)
# 默认音量 gs_fieldMusic_volume = 1.0f/5.0f*0.1f = 0.02f(服务器未下发时)。
#
# 用法:
# var bd := preload("res://bgm_director.gd").new()
@@ -10,6 +16,11 @@
# bd.bind(client, audio)
extends Node
const MusicInfo = preload("res://musicinfo.gd")
# gs_fieldMusic_volume 默认值(PythonNetworkStreamPhaseLoading.cpp:299)。
const DEFAULT_FIELD_MUSIC_VOLUME := 1.0 / 5.0 * 0.1 # = 0.02
var _client: Node
var _audio: Node
@@ -22,18 +33,49 @@ func bind(client: Node, audio: Node) -> void:
_audio = audio
if _client and _client.has_signal("bgm_changed"):
_client.bgm_changed.connect(_on_bgm_changed)
# 换图(§1.3):world_reset 后重跑选曲逻辑。新图的 BGM 包到达前沿用当前曲目。
if _client and _client.has_signal("world_reset"):
_client.world_reset.connect(_on_world_reset)
func _on_bgm_changed(name: String, volume: float) -> void:
# M2Client 已在 EntityStore 侧做音量保留(BGM3 只带名、BGM4 带名+音量),
# 这里拿到的 volume 就是「有效音量」,<0 仅表示从未收到过 BGM4。
current_track = name
if volume >= 0.0:
current_volume = volume
if _audio and "master_bgm" in _audio:
_audio.master_bgm = clampf(volume, 0.0, 1.0)
current_volume = volume
_select()
func _on_world_reset() -> void:
_select()
# game.py:190 的三行选曲 + 先置音量。
func _select() -> void:
if _audio == null:
return
if name == "":
# 1) 选曲:服务端名优先,否则回落 musicInfo.fieldMusicMETIN2THEMA = "M2BG.mp3")。
var track := current_track
if track == "":
track = String(MusicInfo.fieldMusic)
# 2) 音量:服务端未指定(<0)用默认 0.02,否则用服务端值。
var vol := current_volume if current_volume >= 0.0 else DEFAULT_FIELD_MUSIC_VOLUME
vol = clampf(vol, 0.0, 1.0)
# 3) 先应用音量再淡入:Audio.play_bgm() 的淡入目标是 linear_to_db(master_bgm)
# 对标 game.py 先 SetMusicVolume/SetSoundVolume 再 FadeInMusic。
# 声音(SFX)音量在参考里取自系统设置(systemSetting.GetSoundVolume),
# 不随 BGM 包下发,这里不改 master_sfx。
if "master_bgm" in _audio:
_audio.master_bgm = vol
if _audio.has_method("set_music_volume"):
_audio.set_music_volume(vol)
if track == "":
if _audio.has_method("stop_bgm"):
_audio.stop_bgm()
return
if _audio.has_method("play_bgm"):
_audio.play_bgm(name)
_audio.play_bgm(track)
# musicInfo.SaveLastPlayFieldMusic 语义:记录本次战场曲目。
MusicInfo.save_last_play_field_music(track)