Files
2026-07-06 22:59:47 -07:00

146 lines
4.4 KiB
GDScript

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")