只存次数不存加成——加成是派生物,回读时经 _apply_attr_purchases 重建。 这也躲开了 E3-① 记录的坑:_modifiers 是 Array[Dictionary],而 JSON 回读的 无类型 Array 直接 = 赋值是运行时类型错误,必须 .assign();存派生源头则不涉及。 回读顺序:attr_purchases 必须早于 load_save_data。后者设 hp,而重建加成会经 _recompute_attrs 触发 hp = minf(hp, hp_max);顺序颠倒会拿未加成的 hp_max 去钳, 静默吞血(hp_max 可买到 180 后即可复现)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
126 lines
4.2 KiB
GDScript
126 lines
4.2 KiB
GDScript
## 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
|