diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index 67baab64..5ddceca9 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -26,11 +26,13 @@ const Narrator := preload("res://presentation/battle_narrator.gd") const ModalStack := preload("res://presentation/modal_stack.gd") const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") +const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) var db var settlement = null # 当前战斗编排器(关卡与调试模式统一读它) var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner()) var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接) +var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构) var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) var _level_active := false @@ -76,6 +78,10 @@ func _ready() -> void: if not ok: _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return + # 存档读盘(存档序列化批):拒读/损坏均已在 SaveManager 内部兜底,失败不阻断启动 + # (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。 + save_manager = SaveManagerScript.new() + save_manager.load_from_disk() _debug = DebugSessionScript.new(db, get_node("/root/RngService"), Callable(self, "_debug_hero_override"), Callable(self, "_seed_value")) _debug.battle_created.connect(_on_debug_battle_created) @@ -349,6 +355,11 @@ func _on_adventure_finished(summary: Dictionary) -> void: _end_overlay.open_defeat(Narrator.adventure_summary_text(summary), "返回", board_view.get_global_rect()) logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) + # 存档入账(存档序列化批):正式冒险收束唯一写档点——域更新原语 + 原子写盘。 + # 单关测试入口与调试两层不经过本处理器,天然不写档(契约:测试模式不污染生涯数据)。 + if save_manager != null: + save_manager.apply_adventure_summary(summary) + save_manager.write_to_disk() if runner != null: runner.restore_hero_base() strip_view.refresh(runner) @@ -357,7 +368,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: func _on_battle_end_continue() -> void: - # 终局出口:返回开始界面(冒险摘要留档试玩日志——钥石持久化归存档/元进度批) + # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批) _close_modal(_end_overlay) _end_overlay.close() _debug.reset() diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index 0a49d081..1478e30e 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -3106,16 +3106,16 @@ func _smoke_battle_log_view() -> void: # ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/ -# 损坏兜底/迁移链/域界干净/摘要 cap)。测试文件统一 user://smoke_save_*.json 隔离,段首清场+ -# 段尾清理——绝不碰 user://save.json(红线)。摘要集成断言(run_adventure 驱动,B1 冒险结构批产物) -# 待其落 master 后补段。 +# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成)。测试文件统一 user://smoke_save_*.json 隔离, +# 段首清场+段尾清理——绝不碰 user://save.json(红线)。 func _smoke_save() -> void: print("--- SaveManager 存档读写/迁移/兜底 ---") var sm_script = load("res://core/save_manager.gd") var paths := ["user://smoke_save_roundtrip.json", "user://smoke_save_fresh.json", "user://smoke_save_spend.json", "user://smoke_save_highver.json", "user://smoke_save_corrupt.json", "user://smoke_save_migrate.json", - "user://smoke_save_domain.json", "user://smoke_save_cap.json"] + "user://smoke_save_domain.json", "user://smoke_save_cap.json", + "user://smoke_save_integration.json"] var stale: Array = [] for p in paths: stale.append(p) @@ -3220,6 +3220,30 @@ func _smoke_save() -> void: check(int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2, "最新在前(头 seed 11 / 尾 seed 2)") + # ⑨ 摘要集成(冒险结构批产物驱动):run_adventure 真局摘要 → apply 原语 → 域读数 → 写盘读回保持 + var autoplay = load("res://smoke/level_autoplay.gd") + var st9: Dictionary = autoplay.run_adventure(_db, 42, 200, 100) + var f9 = sm_script.new(paths[8]) + f9.apply_adventure_summary(st9.get("summary", {})) + var kpl := int(_db.adventure.keystone_per_level) + check(String(st9.get("result", "")) == "completed", "摘要集成:冒险驱动满配通关(seed 42)") + check(f9.keystone_balance() == 3 * kpl, "集成钥石余额 = 3 × keystone_per_level(%d)" % f9.keystone_balance()) + check(f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1, "集成通关次数表全 1") + check(f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3), + "集成三关首杀标记齐") + check(f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42, + "集成 recent 1 条且 seed 42") + var st9b: Dictionary = autoplay.run_adventure(_db, 42, 200, 100) + f9.apply_adventure_summary(st9b.get("summary", {})) + check(f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2, "第二局后通关次数全 2") + check(f9.keystone_balance() == 6 * kpl, "第二局后余额翻倍(6 × keystone_per_level)") + check(f9.recent_summaries().size() == 2, "集成 recent 长度 2") + check(f9.write_to_disk(), "集成写盘成功") + var r9 = sm_script.new(paths[8]) + check(r9.load_from_disk() and r9.keystone_balance() == 6 * kpl and r9.clear_count(3) == 2 + and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3), + "集成写盘读回全量保持") + # 段尾清理(测试文件绝不残留) for p in stale: if FileAccess.file_exists(p):