Files
gplan-2/game/smoke/screen_flow_probe.gd
T
DKChan 1458d94b0e feat(迭代29 T4): L3 屏壳切换探针 screen_flow_probe,入轻闸默认段
新建 game/smoke/screen_flow_probe.gd/.tscn:走 菜单→营地→菜单→冒险→(关卡屏⇄战斗屏)→放弃→终局→菜单
六步主流程与两条回退路径,每步断言屏级互斥 + 流程状态位。与 ui_click_probe 分工:那件走「点某个按钮
有没有反应」的行为走查(含战斗推进与结算数值面),本件只走屏壳与流程状态,不推进战斗不碰结算数值
——全库 change_scene 零命中、屏切换全靠 visible 互斥,这正是最容易被装配顺序改动悄悄破坏的面。

判据两处订正(首跑实测抓出,都写进注释防复发):
① 屏标记不能裸看 visible——战斗容器 battle_root 在局外也 visible,会把 boot 态误判成「菜单与冒险
   同时在场」;冒险屏改取流程状态位 run_scope.is_adventure。
② 互斥不能用「可见屏计数 ≤1」——终局覆盖层合法叠在冒险内容之上(终局态 is_adventure 尚未收束,
   是设计如此);改为冲突对判定,合法叠加仅「终局+冒险」这一对。
变异验证:boot 后强置 camp_overlay.visible=true → 精确命中「菜单与营地同时在场」两条断言见红。

run_all.gd:负面套件(negative=true)全文输出折叠、只回显结论行——它输出里的 Parse Error 是预期产物,
回显出去会让编排层的日志扫描把集成/负面用例的正常输出误判成真实脚本错误(首跑即撞)。

入轻闸默认段(ui_contrast 之后);轻闸 --quick 墙钟 363s → 405s(新探针约 40s),轻闸上限内。
2026-10-16 15:41:01 +08:00

187 lines
7.8 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.
# screen_flow_probe.gd — L3 屏壳切换探针(迭代 29 T4)
# 判的是「跑起来之后,屏与场景的切换对不对」:菜单 ⇄ 营地 ⇄ 冒险 ⇄ 终局 的主流程与回退路径,
# 每一步断言 **屏级互斥**(同一时刻最多一个屏可见)+ 流程状态位落到正确值。
# 与 ui_click_probe 的分工:那件走的是「点某个按钮有没有反应」的行为走查(含战斗推进与结算数值面);
# 本件只走屏壳与流程状态,不推进战斗、不碰结算数值——屏级互斥是最容易被装配顺序改动悄悄破坏
# 又最难被单测覆盖的面(全库 change_scene 零命中,屏切换全靠 visible 互斥)。
# 运行:Godot_console.exe --headless --path . res://smoke/screen_flow_probe.tscn
extends Node
const ProbeUiUtil := preload("res://smoke/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, false)
await get_tree().process_frame
await _step_boot_menu()
await _step_menu_to_camp()
await _step_camp_back_to_menu()
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_flags() -> 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 _flags_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_flag(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_flags()
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, _flags_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, _flags_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_flag("①boot", false)
if main.debug_overlay != null and main.debug_overlay.visible:
_problems.append("①boot:调试浮窗 boot 自动开(菜单优先失守)")
# ---- ② 菜单 → 营地(真实按钮信号路径)----
func _step_menu_to_camp() -> void:
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix")
if campbtn == null:
_problems.append("②菜单→营地:营地按钮缺失")
return
campbtn.pressed.emit()
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()
_expect_exclusive("③营地→菜单(回退)", "菜单")
# ---- ④ 菜单 → 冒险(开新冒险 → 入口停点落关卡地图屏)----
func _step_menu_to_adventure() -> void:
var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains")
if lvbtn == null:
lvbtn = ProbeUiUtil.find_button(main._start_overlay, "冒险", "contains")
if lvbtn == null:
_problems.append("④菜单→冒险:开始冒险按钮缺失")
return
lvbtn.pressed.emit()
await _frames(3)
_expect_exclusive("④菜单→冒险", "冒险")
_expect_adventure_flag("④菜单→冒险", 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)")