extends Node ## Persistent player-facing settings (AnchorV1.0 §9.5 difficulty windows, ## §20.5 music/sfx volumes). Sole writer of the Music / SFX buses and of the ## RhythmManager judgement windows; every menu goes through this node. signal difficulty_changed(difficulty: StringName) signal volumes_changed(music_volume: int, sfx_volume: int) const SETTINGS_PATH := "user://settings.cfg" const MUSIC_BUS_NAME := "Music" const SFX_BUS_NAME := "SFX" const DIFFICULTY_ORDER: Array[StringName] = [&"easy", &"normal", &"hard"] ## Windows in seconds per AnchorV1.0 §21.8. const DIFFICULTY_WINDOWS := { &"easy": Vector3(0.090, 0.160, 0.260), &"normal": Vector3(0.060, 0.115, 0.215), &"hard": Vector3(0.030, 0.070, 0.170), } ## 小怪血量难度倍率:普通 3 倍、困难 5 倍,简单保持基准值。 const DIFFICULTY_MINION_HEALTH_MULTIPLIER := { &"easy": 1.0, &"normal": 3.0, &"hard": 5.0, } const DEFAULT_DIFFICULTY: StringName = &"normal" const DEFAULT_MUSIC_VOLUME := 85 const DEFAULT_SFX_VOLUME := 80 var difficulty: StringName = DEFAULT_DIFFICULTY var music_volume := DEFAULT_MUSIC_VOLUME var sfx_volume := DEFAULT_SFX_VOLUME func _ready() -> void: _ensure_audio_buses() _load_settings() apply_difficulty_windows() _apply_bus_volumes() func set_difficulty(next: StringName) -> void: if not DIFFICULTY_WINDOWS.has(next): return if next == difficulty: return difficulty = next apply_difficulty_windows() _save_settings() difficulty_changed.emit(difficulty) func set_music_volume(value: int) -> void: music_volume = clampi(value, 0, 100) _apply_bus_volumes() _save_settings() volumes_changed.emit(music_volume, sfx_volume) func set_sfx_volume(value: int) -> void: sfx_volume = clampi(value, 0, 100) _apply_bus_volumes() _save_settings() volumes_changed.emit(music_volume, sfx_volume) func difficulty_windows(for_difficulty: StringName = &"") -> Vector3: var key := for_difficulty if DIFFICULTY_WINDOWS.has(for_difficulty) else difficulty return DIFFICULTY_WINDOWS.get(key, DIFFICULTY_WINDOWS[DEFAULT_DIFFICULTY]) func minion_health_multiplier(for_difficulty: StringName = &"") -> float: var key := for_difficulty if DIFFICULTY_MINION_HEALTH_MULTIPLIER.has(for_difficulty) else difficulty return float(DIFFICULTY_MINION_HEALTH_MULTIPLIER.get(key, 1.0)) func difficulty_display_name(value: StringName = &"") -> String: match (value if not value.is_empty() else difficulty): &"easy": return "简单" &"hard": return "困难" return "普通" func apply_difficulty_windows() -> void: var rhythm := _rhythm_manager_or_null() if rhythm == null or not rhythm.has_method("apply_judgement_windows"): return var windows := difficulty_windows() rhythm.call("apply_judgement_windows", windows.x, windows.y, windows.z) func _ensure_audio_buses() -> void: for bus_name: String in [MUSIC_BUS_NAME, SFX_BUS_NAME]: if AudioServer.get_bus_index(bus_name) != -1: continue AudioServer.add_bus() var bus_index := AudioServer.bus_count - 1 AudioServer.set_bus_name(bus_index, bus_name) AudioServer.set_bus_send(bus_index, "Master") func _apply_bus_volumes() -> void: _set_bus_volume(MUSIC_BUS_NAME, music_volume) _set_bus_volume(SFX_BUS_NAME, sfx_volume) func _set_bus_volume(bus_name: String, value: int) -> void: var bus_index := AudioServer.get_bus_index(bus_name) if bus_index == -1: return var linear := clampf(float(value) / 100.0, 0.0, 1.0) AudioServer.set_bus_mute(bus_index, linear <= 0.0) AudioServer.set_bus_volume_db(bus_index, linear_to_db(maxf(0.0001, linear))) func _load_settings() -> void: var config := ConfigFile.new() if config.load(SETTINGS_PATH) != OK: return var stored_difficulty := StringName(str(config.get_value("gameplay", "difficulty", str(DEFAULT_DIFFICULTY)))) if DIFFICULTY_WINDOWS.has(stored_difficulty): difficulty = stored_difficulty music_volume = clampi(int(config.get_value("audio", "music_volume", DEFAULT_MUSIC_VOLUME)), 0, 100) sfx_volume = clampi(int(config.get_value("audio", "sfx_volume", DEFAULT_SFX_VOLUME)), 0, 100) func _save_settings() -> void: var config := ConfigFile.new() config.set_value("gameplay", "difficulty", str(difficulty)) config.set_value("audio", "music_volume", music_volume) config.set_value("audio", "sfx_volume", sfx_volume) config.save(SETTINGS_PATH) func _rhythm_manager_or_null() -> Node: if not is_inside_tree(): return null return get_tree().root.get_node_or_null("RhythmManager")