## SettingsManager — 全局设置 + 难度(Autoload: SettingsManager) ## 权威来源:development_plan.md S6 P1(难度三档持久化)、certification_checklist.md ST-03(清除本地数据) ## 持久化 user://save_data.json:{ difficulty:int, locale:String, master_volume:float } extends Node const SAVE_PATH: String = "user://save_data.json" enum Difficulty { BEGINNER = 0, STANDARD = 1, CHALLENGE = 2 } var difficulty: int = Difficulty.STANDARD var locale: String = "zh_CN" var master_volume: float = 1.0 var ftue_done: bool = false # 首次启动难度引导是否已完成(FTUE) func _ready() -> void: _load() _load_balance() _apply_audio() if Locale: Locale.set_locale(locale) ## 是否需要首启难度引导(FTUE) func needs_ftue() -> bool: return not ftue_done ## 完成 FTUE:设定难度并持久化标志 func complete_ftue(chosen_difficulty: int) -> void: difficulty = clampi(chosen_difficulty, 0, 2) ftue_done = true _save() ## 难度乘子 + Boss 血量:纯数据驱动,唯一来源 data/balance.json(游戏设计器「平衡」面板维护) ## get() 的默认仅为防崩溃,非内容副本 var _MULTS: Dictionary = {} # key → [初学者, 标准, 挑战] var _BOSS_HP: Dictionary = {} # 原型→基础血量 const BALANCE_JSON: String = "res://data/balance.json" func _mult(key: String) -> float: var arr: Array = _MULTS.get(key, [1.0, 1.0, 1.0]) return float(arr[clampi(difficulty, 0, arr.size() - 1)]) func enemy_hp_mult() -> float: return _mult("enemy_hp") func player_dmg_taken_mult() -> float: return _mult("player_dmg") func wave_count_mult() -> float: return _mult("wave_count") func boss_hp_mult() -> float: return _mult("boss_hp") ## Boss 基础血量(WaveManager._spawn_boss 查询;balance.json 可调) func get_boss_base_hp(boss_type: int) -> float: return float(_BOSS_HP.get(str(boss_type), 500.0)) ## 数据驱动:加载 balance.json(难度乘子 + Boss 血量) func _load_balance() -> void: if not FileAccess.file_exists(BALANCE_JSON): push_error("SettingsManager: 缺失 res://data/balance.json(难度乘子/Boss血量)") return var data = JSON.parse_string(FileAccess.get_file_as_string(BALANCE_JSON)) if not (data is Dictionary): push_error("SettingsManager: balance.json 格式错误") return if data.has("difficulty_mults") and data["difficulty_mults"] is Dictionary: for k in data["difficulty_mults"]: _MULTS[k] = data["difficulty_mults"][k] if data.has("boss_hp") and data["boss_hp"] is Dictionary: _BOSS_HP = data["boss_hp"] func difficulty_name() -> String: return ["DIFF_BEGINNER", "DIFF_STANDARD", "DIFF_CHALLENGE"][difficulty] # ── 设置变更(即时保存)────────────────────────────────────── func set_difficulty(d: int) -> void: difficulty = clampi(d, 0, 2) _save() func cycle_difficulty() -> int: set_difficulty((difficulty + 1) % 3) return difficulty func set_locale(loc: String) -> void: locale = loc if Locale: Locale.set_locale(loc) _save() func set_master_volume(v: float) -> void: master_volume = clampf(v, 0.0, 1.0) _apply_audio() _save() func _apply_audio() -> void: var bus: int = AudioServer.get_bus_index("Master") if bus >= 0: AudioServer.set_bus_volume_db(bus, linear_to_db(maxf(0.0001, master_volume))) # ── ST-03:清除所有本地数据 ───────────────────────────────── func clear_all_local_data() -> void: var files: Array = [ "user://crash_log.txt", "user://crash_log_prev.txt", "user://run_a.json", "user://run_b.json", "user://endless_records.json", "user://save_data.json", ] for path in files: if FileAccess.file_exists(path): DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) _remove_dir_recursive("user://runs") # 重置内存态 difficulty = Difficulty.STANDARD master_volume = 1.0 ftue_done = false # 清数据后重新触发首启难度引导 # 用路径引用避免编译时 autoload 依赖(打破 SettingsManager→ProfileManager→WaveManager→SettingsManager 循环) var pm: Node = get_node_or_null("/root/ProfileManager") if pm and pm.has_method("clear_run"): pm.clear_run() _apply_audio() func _remove_dir_recursive(path: String) -> void: var d := DirAccess.open(path) if d == null: return d.list_dir_begin() var fn := d.get_next() while fn != "": if not d.current_is_dir(): DirAccess.remove_absolute(ProjectSettings.globalize_path(path + "/" + fn)) fn = d.get_next() d.list_dir_end() DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) # ── 持久化 ────────────────────────────────────────────────── func _save() -> void: var data: Dictionary = { "difficulty": difficulty, "locale": locale, "master_volume": master_volume, "ftue_done": ftue_done, } var f := FileAccess.open(SAVE_PATH, FileAccess.WRITE) if f: f.store_string(JSON.stringify(data)) f.close() func _load() -> void: if not FileAccess.file_exists(SAVE_PATH): return var parsed = JSON.parse_string(FileAccess.get_file_as_string(SAVE_PATH)) if not (parsed is Dictionary): return difficulty = clampi(int(parsed.get("difficulty", 1)), 0, 2) locale = String(parsed.get("locale", "zh_CN")) master_volume = clampf(float(parsed.get("master_volume", 1.0)), 0.0, 1.0) ftue_done = bool(parsed.get("ftue_done", false))