Files
shenleiandClaude Sonnet 5 9732f2799e 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
2026-09-02 15:31:58 +09:00

154 lines
6.3 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# bgm_test —— §9.1 地图 BGM 选曲 + §9.2 前后台音量存/恢复的 headless 自检。
# godot --headless --path project --script bgm_test.gd
# exit 0 = pass。
extends SceneTree
const BgmDirector = preload("res://bgm_director.gd")
const Audio = preload("res://audio.gd")
const AppLifecycle = preload("res://app_lifecycle.gd")
const MusicInfo = preload("res://musicinfo.gd")
const DEFAULT_VOL := 1.0 / 5.0 * 0.1 # 0.02
# 假 M2Client:只发 §9.1 关心的两个信号。
class FakeClient extends Node:
signal bgm_changed(name: String, volume: float)
signal world_reset()
# 假 Audio:记录 play_bgm / stop_bgm 调用与音量,够 director 断言用。
class FakeAudio extends Node:
var master_bgm := 0.6
var master_sfx := 0.9
var _bgm_name := ""
var played: Array[String] = []
var stopped := 0
func play_bgm(name: String, _fade := 1.2) -> void:
if name == _bgm_name:
return
_bgm_name = name
played.append(name)
func stop_bgm(_fade := 1.0) -> void:
_bgm_name = ""
stopped += 1
func set_music_volume(v: float) -> void:
master_bgm = clampf(v, 0.0, 1.0)
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
_test_select_server_volume()
_test_select_default_volume()
_test_select_empty_falls_back()
_test_world_reset_reruns()
_test_musicinfo_lastplay_roundtrip()
_test_background_save_restore()
if _fail == 0:
print("PASS: bgm_test (§9.1 map BGM select + §9.2 volume save/restore)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _mk_director() -> Array:
var client := FakeClient.new()
var audio := FakeAudio.new()
var bd := BgmDirector.new()
get_root().add_child(client)
get_root().add_child(audio)
get_root().add_child(bd)
bd.bind(client, audio)
return [client, audio, bd]
# §9.1:服务端给了名字 + 音量 -> 用服务端音量并播放该曲。
func _test_select_server_volume() -> void:
var t := _mk_director()
var client: FakeClient = t[0]
var audio: FakeAudio = t[1]
client.bgm_changed.emit("pvp_bgm.mp3", 0.5)
_ck(audio.played == ["pvp_bgm.mp3"], "server name played, got %s" % [audio.played])
_ck(is_equal_approx(audio.master_bgm, 0.5), "server volume applied, got %f" % audio.master_bgm)
_ck(audio.stopped == 0, "no stop_bgm on server-provided track")
t[0].free(); t[1].free(); t[2].free()
# §9.1:服务端给了名字但音量 <0GC_MAIN_CHARACTER3_BGM,未收 BGM4-> 默认 0.02。
func _test_select_default_volume() -> void:
var t := _mk_director()
var client: FakeClient = t[0]
var audio: FakeAudio = t[1]
client.bgm_changed.emit("field_a.mp3", -1.0)
_ck(audio.played == ["field_a.mp3"], "name played on unspecified volume, got %s" % [audio.played])
_ck(is_equal_approx(audio.master_bgm, DEFAULT_VOL), "default 0.02 applied, got %f" % audio.master_bgm)
t[0].free(); t[1].free(); t[2].free()
# §9.1:服务端名为空 -> 回落 musicInfo.fieldMusicMETIN2THEMA = "M2BG.mp3")。
func _test_select_empty_falls_back() -> void:
var t := _mk_director()
var client: FakeClient = t[0]
var audio: FakeAudio = t[1]
client.bgm_changed.emit("", -1.0)
_ck(audio.played == [String(MusicInfo.fieldMusic)],
"empty name falls back to fieldMusic, got %s" % [audio.played])
_ck(String(MusicInfo.fieldMusic) == "M2BG.mp3", "METIN2THEMA constant is M2BG.mp3")
_ck(is_equal_approx(audio.master_bgm, DEFAULT_VOL), "default 0.02 on fallback, got %f" % audio.master_bgm)
t[0].free(); t[1].free(); t[2].free()
# §9.1 / §1.3:换图(world_reset)重跑选曲逻辑。
func _test_world_reset_reruns() -> void:
var t := _mk_director()
var client: FakeClient = t[0]
var audio: FakeAudio = t[1]
var bd: BgmDirector = t[2]
_ck(client.world_reset.is_connected(bd._on_world_reset), "director connected to world_reset")
client.bgm_changed.emit("map_a.mp3", 0.4)
# warpworld_reset 先到(旧曲目仍在 store 里),随后新图的 BGM 包。
client.world_reset.emit()
_ck(audio.played == ["map_a.mp3"], "world_reset keeps current track, got %s" % [audio.played])
client.bgm_changed.emit("map_b.mp3", 0.2)
_ck(audio.played == ["map_a.mp3", "map_b.mp3"], "new map BGM played, got %s" % [audio.played])
_ck(is_equal_approx(audio.master_bgm, 0.2), "new map volume applied, got %f" % audio.master_bgm)
t[0].free(); t[1].free(); t[2].free()
# §9.1musicInfo.SaveLastPlayFieldMusic / LoadLastPlayFieldMusic 往返。
func _test_musicinfo_lastplay_roundtrip() -> void:
MusicInfo.save_last_play_field_music("last_field.mp3")
_ck(MusicInfo.load_last_play_field_music() == "last_field.mp3",
"lastplay.inf roundtrip, got %s" % MusicInfo.load_last_play_field_music())
# §9.2:后台保存音量 + 静音,前台恢复;_bgm_name 不变,同一首继续播。
func _test_background_save_restore() -> void:
var client := FakeClient.new()
var audio := Audio.new()
var life := AppLifecycle.new()
get_root().add_child(audio)
audio.setup("res://__no_assets__") # 无资源,play_bgm 只置名不出声
get_root().add_child(life)
life.pause_tree_on_background = false # 别把测试树暂停了
life.background_max_fps = 0
life.bind(null, audio)
audio.master_bgm = 0.6
audio.master_sfx = 0.9
# 模拟「某首战场曲目正在播放」(headless 无资源,不实际解码)。
audio._bgm_name = "M2BG.mp3"
var name_before := audio._bgm_name
_ck(name_before == "M2BG.mp3", "bgm name set before background, got %s" % name_before)
life._enter_background()
_ck(is_equal_approx(audio.master_bgm, 0.0), "music muted in background, got %f" % audio.master_bgm)
_ck(audio._volume_saved, "save_volume ran (backup taken)")
_ck(is_equal_approx(audio._backup_music_volume, 0.6), "backup music volume, got %f" % audio._backup_music_volume)
_ck(is_equal_approx(audio._backup_sound_volume, 0.9), "backup sound volume, got %f" % audio._backup_sound_volume)
_ck(audio._bgm_name == name_before, "_bgm_name unchanged in background, got %s" % audio._bgm_name)
life._exit_background()
_ck(is_equal_approx(audio.master_bgm, 0.6), "music volume restored, got %f" % audio.master_bgm)
_ck(is_equal_approx(audio.master_sfx, 0.9), "sound volume restored, got %f" % audio.master_sfx)
_ck(audio._bgm_name == name_before, "_bgm_name still unchanged after restore, got %s" % audio._bgm_name)
_ck(not audio._volume_saved, "backup flag cleared after restore")
client.free(); life.free(); audio.free()