# screen_flow_probe.gd — L3 屏壳切换探针 # 判的是「跑起来之后,屏与场景的切换对不对」:菜单 ⇄ 营地 ⇄ 冒险 ⇄ 终局 的主流程与回退路径, # 每一步断言 **屏级互斥**(同一时刻最多一个屏可见)+ 流程状态位落到正确值。 # 与 ui_click_probe 的分工:那件走的是「点某个按钮有没有反应」的行为走查(含战斗推进与结算数值面); # 本件只走屏壳与流程状态,不推进战斗、不碰结算数值——屏级互斥是最容易被装配顺序改动悄悄破坏 # 又最难被单测覆盖的面(全库 change_scene 零命中,屏切换全靠 visible 互斥)。 # 运行:Godot_console.exe --headless --path . res://probe/screen_flow_probe.tscn extends Node const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") var main: Node var _problems: Array[String] = [] func _ready() -> void: await get_tree().process_frame main = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame await get_tree().process_frame # 隔离存档(全 worktree 共享同一份 user://save.json——探针必须换注,禁碰真实档) ProbeUiUtil.isolate_save(main, "smoke_screen_flow", false, false, false) await get_tree().process_frame await _step_boot_menu() await _step_menu_to_camp() await _step_camp_back_to_menu() await _step_save_select_open_back() # 迭代 33 卡③ sl10——选档开合+返回路径+ESC 语境不响应 await _step_menu_to_adventure() await _step_level_to_battle_and_back() await _step_abandon_to_end() await _step_end_back_to_menu() if _problems.is_empty(): print("[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见)") get_tree().quit(0) return print("[screen_flow_probe] FAIL — %d 处问题:" % _problems.size()) for p in _problems: print("[screen_flow_probe] - " + p) get_tree().quit(1) # ---- 屏级互斥判据 ---- # 屏标记取法:菜单/营地/终局看各自主内容层的 visible;冒险取流程状态位 run_scope.is_adventure #(战斗容器 battle_root 在局外也 visible——裸看 visible 会把 boot 态误判成「菜单与冒险同时在场」)。 # **合法叠加只有一对**:终局覆盖层盖在冒险内容之上(终局态 is_adventure 尚未收束,是设计如此)。 # 其余任意两屏同时为真=互斥失守。 func _screen_seals() -> Dictionary: return { "菜单": main._start_overlay != null and main._start_overlay.visible, "营地": main.camp_overlay != null and main.camp_overlay.visible, "冒险": main.run_scope != null and main.run_scope.is_adventure, "终局": main._end_overlay != null and main._end_overlay.visible, } func _seals_text(f: Dictionary) -> String: var on: Array[String] = [] for k in ["菜单", "营地", "冒险", "终局"]: if bool(f[k]): on.append(k) return "/".join(on) if not on.is_empty() else "无" # ---- 流程状态位(与可见性是两回事,逐步单独判)---- func _expect_adventure_seal(step: String, expect: bool) -> void: var got: bool = main.run_scope != null and main.run_scope.is_adventure if got != expect: _problems.append("%s:流程状态位 is_adventure 应为 %s,实测 %s" % [step, str(expect), str(got)]) func _expect_exclusive(step: String, expect: String) -> void: var f: Dictionary = _screen_seals() var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"]] for pair in pairs: var a: String = pair[0] var b: String = pair[1] if bool(f[a]) and bool(f[b]): _problems.append("%s:屏级互斥失守(%s 与 %s 同时在场)" % [step, a, b]) if not bool(f[expect]): _problems.append("%s:期望屏「%s」不在场(实测 %s)" % [step, expect, _seals_text(f)]) return for k in ["菜单", "营地", "冒险", "终局"]: if k == expect: continue if k == "冒险" and expect == "终局": continue # 合法叠加:终局覆盖在冒险内容之上 if bool(f[k]): _problems.append("%s:屏「%s」不该在场(实测 %s)" % [step, k, _seals_text(f)]) func _frames(n: int = 2) -> void: for _i in n: await get_tree().process_frame # ---- ① boot:菜单独占 ---- func _step_boot_menu() -> void: _expect_exclusive("①boot", "菜单") _expect_adventure_seal("①boot", false) if main.debug_overlay != null and main.debug_overlay.visible: _problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)") # ---- ② 菜单 → 营地(真实按钮信号路径)---- func _step_menu_to_camp() -> void: # 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间) if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_camp"): _problems.append("②菜单→营地:选档层新档钮缺失(新路径驱动中断)") return await _frames() _expect_exclusive("②菜单→营地", "营地") # ---- ③ 营地 → 菜单(回退路径:营地关闭发自覆盖层自身信号)---- func _step_camp_back_to_menu() -> void: if main.camp_overlay == null: _problems.append("③营地→菜单:营地覆盖层句柄缺失") return main.camp_overlay.closed.emit() await _frames() main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点) await _frames() _expect_exclusive("③营地→菜单(回退)", "菜单") # ---- ④ 菜单 → 冒险(开新冒险 → 入口停点落关卡地图屏)---- # ---- 选档界面开合(卡③ sl10):函数直调开层(「进入存档」钮归卡② mn3)→屏级互斥采样→ # 返回钮真实信号路径关闭→ESC 语境不响应(卡⑤ 契约 2 范围外——零实现即断言)。---- func _step_save_select_open_back() -> void: ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true) main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json") var esb: Button = ProbeUiUtil.find_button(main._start_overlay, "进入存档", "prefix") if esb == null: _problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)") return esb.pressed.emit() # 真实按钮信号路径 → _open_save_select(卡② mn3 点击链——卡③ 直调中间态就此闭环) await get_tree().process_frame var seals := _screen_seals() if not main.save_select_overlay.visible or not seals_check_single(seals): _problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.save_select_overlay.visible)) var back_btn: Button = ProbeUiUtil.find_button(main.save_select_overlay, "返回", "prefix") if back_btn == null: _problems.append("[savesel] sl10 返回钮缺失(页壳顶条)") else: back_btn.pressed.emit() await get_tree().process_frame if main.save_select_overlay.visible or not main._start_overlay.visible: _problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)") # ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言) main._open_save_select() await get_tree().process_frame var ev := InputEventKey.new() ev.keycode = KEY_ESCAPE ev.physical_keycode = KEY_ESCAPE ev.pressed = true main.get_viewport().push_input(ev, true) await get_tree().process_frame if not main.save_select_overlay.visible: _problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)") main._on_save_select_closed() await get_tree().process_frame # 互斥判据(本步局部——四屏 seal 之外新增选档位的同形判据) func seals_check_single(f: Dictionary) -> bool: var on := 0 for k in f: if bool(f[k]): on += 1 if main.save_select_overlay.visible: on += 1 # 选档自身计入(调用点已开层) return on == 1 func _step_menu_to_adventure() -> void: # 开始冒险钮已随三入口化撤出——函数直调中间态(卡② mn10;卡④ 大门两分支换代) main._start_adventure_from_menu() await _frames(3) _expect_exclusive("④菜单→冒险", "冒险") _expect_adventure_seal("④菜单→冒险", true) if main.run_scope != null and main.run_scope.adventure == null: _problems.append("④菜单→冒险:冒险实例未建立(入口停点未达)") if main.level_screen != null and not main.level_screen.visible: _problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)") # ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)---- func _step_level_to_battle_and_back() -> void: if not main.level_screen.visible: _problems.append("⑤冒险内切换:前置不在关卡地图屏") return main._exit_level_screen() await _frames() if main.level_screen.visible: _problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见") if main._battle_root != null and not main._battle_root.visible: _problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)") _expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险") main._enter_level_screen() await _frames() if not main.level_screen.visible: _problems.append("⑤冒险内切换:回到关卡地图屏失败") _expect_exclusive("⑤冒险内切换(战斗→关卡)", "冒险") # ---- ⑥ 放弃 → 终局(放弃路径=回退路径的非正常出口)---- func _step_abandon_to_end() -> void: if main.run_scope == null or main.run_scope.adventure == null: _problems.append("⑥放弃→终局:前置无冒险实例") return main._on_abandon_requested() await _frames(4) _expect_exclusive("⑥放弃→终局", "终局") if main._end_overlay != null and not main._end_overlay.visible: _problems.append("⑥放弃→终局:终局覆盖层未开") # ---- ⑦ 终局 → 菜单(终局出口返回开始界面)---- func _step_end_back_to_menu() -> void: if main._end_overlay == null or not main._end_overlay.visible: _problems.append("⑦终局→菜单:前置不在终局") return main._end_overlay.continue_pressed.emit() await _frames(3) _expect_exclusive("⑦终局→菜单", "菜单") if main.run_scope != null and main.run_scope.is_adventure: _problems.append("⑦终局→菜单:冒险态未收束(run_scope 仍标 is_adventure)")