## 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 = 3 # v3:新增 attr_purchases(货架 C 属性购买次数) 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 # 必须早于 load_save_data:后者设 hp,而 _apply_attr_purchases 会经 _recompute_attrs # 触发 hp = minf(hp, hp_max)。顺序颠倒会拿未加成的 hp_max 去钳,静默吞血。 ShopManager.apply_attr_purchases_save(data.get("attr_purchases", {})) 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(), "attr_purchases": ShopManager.get_attr_purchases_save(), } 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 if ver < 3: # v2→v3: 旧档无 attr_purchases;apply_run 的 data.get(..., {}) 已处理缺失,无需补字段 data["schema_version"] = 3 return data