diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 96605e33..b48e27a2 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -713,7 +713,14 @@ func _on_battle_finished(payload: Dictionary) -> void: # 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;账本语义零改动,纯呈现读数): # 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账 # 连接先于组合根 handler——此读数即战后入账完毕的总量)。 +# runner 缺位守卫(2026-10-06 用户实机 main.gd:717 崩值定位——续玩停点恢复分支:adventure_runner +# resume_adventure 的 bonus_pending 分支在 level_started 之前同步 curio_choice_presented.emit +# 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照 +# 此刻必在档,语义=续玩恢复面的合计即存档合计;与 _bonus_summary_text 同款守卫式) func _gp_summary_text(payload: Dictionary) -> String: + if runner == null: + var bp: Dictionary = save_manager.breakpoint_snapshot() if save_manager != null else {} + return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(bp.get("gp", 0))] return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)] diff --git a/game/smoke/ui_struct_probe.gd b/game/smoke/ui_struct_probe.gd index a64f806a..743775e2 100644 --- a/game/smoke/ui_struct_probe.gd +++ b/game/smoke/ui_struct_probe.gd @@ -99,6 +99,28 @@ func _ready() -> void: _settle() await _audit("shop", [main.shop_overlay]) + # ---- ⑦resume 专项:续玩停点恢复(2026-10-06 用户实机 main.gd:717 报错回归闸)---- + # 三选一停点续玩分支在 level_started 之前同步 curio_choice_presented.emit(adventure_runner + # resume 提前 return)——GP 总结 lambda 求值时 runner 尚未赋值,修复前 717 炸 null。 + ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_resume", true) + main.save_manager.set_breakpoint(_resume_bp_fixture()) + main._refresh_resume_button() + main.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) + main._resume_from_menu() + _settle(8) + if not main._end_overlay.visible: + problems.append("[resume] 续玩恢复后胜利总结面未开(三选一停点分支中断)") + else: + var found := {"ok": false} + _find_label_text(main._end_overlay, "合计 24", found) + if not found.ok: + problems.append("[resume] 胜利总结面 GP 行未见快照合计「合计 24」(runner 缺位守卫缺位——717 回归)") + var s: String = main._gp_summary_text({"gp_dropped": 0}) + if not s.contains("合计 24"): + problems.append("[resume] _gp_summary_text runner=null 守卫未取快照 gp(%s)" % s) + # 恢复现场(resume 改写了 main 的 adventure/save 态——后续无屏依赖,仅为整洁) + main._level_active = false + # ---- 判定 ---- if problems.is_empty(): print("[ui_struct_probe] PASS — 全屏结构断言零红") @@ -109,6 +131,25 @@ func _ready() -> void: get_tree().quit(1) +# ⑦resume 夹具:最小合法断点快照——bonus_pending 非空=三选一停点分支(合计 gp=24 供守卫兜底断言) +func _resume_bp_fixture() -> Dictionary: + return { + "master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0, + "level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [], + "bonus_aux_carry": [], "keystone_drops": [], "bonus_taken": {}, + "bonus_pending": {"candidates": [{"kind": "heal", "tier": "common"}], "payload": {"gp_dropped": 0}}, + } + + +# 递归找含目标子串的 Label(⑦resume 断言读数面) +func _find_label_text(node: Node, needle: String, found: Dictionary) -> void: + for ch in node.get_children(): + if ch is Label and String(ch.text).contains(needle): + found.ok = true + return + _find_label_text(ch, needle, found) + + func _pick_battle_node(main: Node) -> int: # 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)—— # 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口)