## ProfileManager — 局内 Run 存档 A/B 双槽(Autoload: ProfileManager) ## 权威来源:architecture_design.md ADR-A2 ## 下面实现了: ## - A/B 双槽交替写入防崩溃 ## - schema_version 字段必须 ## - 所有读取必须经过 _migrate_run() ## - NOTIFICATION_WM_CLOSE_REQUEST 尽力写最后一笔 extends Node const SCHEMA_VERSION: int = 2 # v2:新增 wand(Core/deck/bench)持久化 const PATH_A: String = "user://run_a.json" const PATH_B: String = "user://run_b.json" var _current_slot: String = PATH_A # 上次写入的槽 var _dirty: bool = false ## 法杖状态提供者(CombatManager 在 _ready 注册);须有 get_wand_save_data/apply_wand_save_data var _wand_provider: Object = null func set_wand_provider(p: Object) -> void: _wand_provider = p func _ready() -> void: get_tree().root.connect("close_requested", _on_close_request) func _on_close_request() -> void: if _dirty: save_run() _dirty = false ## 写入当前 Run 状态(交替槽位) func save_run() -> void: var data: Dictionary = _collect_run_data() data["schema_version"] = SCHEMA_VERSION data["saved_at"] = Time.get_unix_time_from_system() var json_str: String = JSON.stringify(data) var target: String = _next_slot() var file := FileAccess.open(target, FileAccess.WRITE) if file: file.store_string(json_str) file.close() _current_slot = target _dirty = false ## 加载 Run,自动选择未损坏的最新槽 func load_run() -> Dictionary: var data_a: Dictionary = _read_slot(PATH_A) var data_b: Dictionary = _read_slot(PATH_B) var best: Dictionary if data_a.is_empty() and data_b.is_empty(): return {} elif data_a.is_empty(): best = data_b elif data_b.is_empty(): best = data_a else: var ta: int = int(data_a.get("saved_at", 0)) var tb: int = int(data_b.get("saved_at", 0)) best = data_a if ta > tb else data_b return _migrate_run(best) ## 应用局内状态 func apply_run(data: Dictionary) -> void: if data.is_empty(): return PlayerStats.load_save_data(data.get("player_stats", {})) WaveManager.current_wave = int(data.get("wave_num", 0)) var shop_seed: int = int(data.get("shop_seed", 0)) if shop_seed > 0: ShopManager._shop_seed = shop_seed # 恢复法杖(Core/deck/bench) if data.has("wand") and is_instance_valid(_wand_provider) and _wand_provider.has_method("apply_wand_save_data"): _wand_provider.apply_wand_save_data(data.get("wand", {})) ## 清除 Run(死亡 / 新开局) func clear_run() -> void: for path in [PATH_A, PATH_B]: if FileAccess.file_exists(path): DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) _dirty = false func mark_dirty() -> void: _dirty = true func _next_slot() -> String: return PATH_B if _current_slot == PATH_A else PATH_A func _collect_run_data() -> Dictionary: var d: Dictionary = { "wave_num": WaveManager.current_wave, "shop_seed": ShopManager.get_shop_seed(), "player_stats": PlayerStats.get_save_data(), } if is_instance_valid(_wand_provider) and _wand_provider.has_method("get_wand_save_data"): d["wand"] = _wand_provider.get_wand_save_data() return d func _read_slot(path: String) -> Dictionary: if not FileAccess.file_exists(path): return {} var text: String = FileAccess.get_file_as_string(path) if text.is_empty(): return {} var parsed = JSON.parse_string(text) if parsed is Dictionary: return parsed return {} ## 迁移旧存档 func _migrate_run(data: Dictionary) -> Dictionary: var ver: int = int(data.get("schema_version", 0)) if ver < 1: # v0→v1: 无破坏性字段变更 data["schema_version"] = 1 if ver < 2: # v1→v2: 旧档无 wand 字段;apply_run 检测缺失即保留默认法杖,无需补字段 data["schema_version"] = 2 return data