## 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 = {} # 原型→基础血量 var _AETHER_THRESHOLDS: Array = [] # [[到达波次, 碎片], ...] 升序 var _MANA_HEAT: Dictionary = {"per_cast": 0.01, "decay_per_sec": 0.1, "max": 1.0} var _INFINITE: Dictionary = {"hp_per_shot": 1.0, "hp_cost_cap_ratio": 0.15} 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)) ## 到达波次 → 以太碎片(阶梯查表:取"波次阈值 ≤ wave"的最高阶梯值;升序表) func aether_for_wave(wave: int) -> int: var table: Array = _AETHER_THRESHOLDS if table.is_empty(): table = [[1, 0], [5, 3], [10, 8], [15, 14], [20, 20]] var result: int = 0 for entry in table: if entry is Array and entry.size() >= 2 and int(entry[0]) <= wave: result = maxi(result, int(entry[1])) # 取 ≤wave 阶梯中的最大值,容忍无序表(设计师手填不必排序) return result func mana_heat_per_cast() -> float: return float(_MANA_HEAT.get("per_cast", 0.01)) func mana_heat_decay() -> float: return float(_MANA_HEAT.get("decay_per_sec", 0.1)) func mana_heat_max() -> float: return float(_MANA_HEAT.get("max", 1.0)) func infinite_hp_per_shot() -> float: return float(_INFINITE.get("hp_per_shot", 1.0)) func hp_cost_cap_ratio() -> float: return float(_INFINITE.get("hp_cost_cap_ratio", 0.15)) ## 数据驱动:加载 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"] if data.has("aether_thresholds") and data["aether_thresholds"] is Array: _AETHER_THRESHOLDS = data["aether_thresholds"] if data.has("mana_heat") and data["mana_heat"] is Dictionary: for k in data["mana_heat"]: _MANA_HEAT[k] = data["mana_heat"][k] if data.has("infinite_spells") and data["infinite_spells"] is Dictionary: for k in data["infinite_spells"]: _INFINITE[k] = data["infinite_spells"][k] 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() MetaProgress.clear() _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))