Files
spellforge/scripts/autoloads/profile_manager.gd
T
joywayerandClaude Opus 5 900eaea697 feat(shop): 货架 C 购买次数进局内存档(schema 2→3)
只存次数不存加成——加成是派生物,回读时经 _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>
2026-08-03 11:27:23 +08:00

126 lines
4.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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_purchasesapply_run 的 data.get(..., {}) 已处理缺失,无需补字段
data["schema_version"] = 3
return data