232 lines
10 KiB
GDScript
232 lines
10 KiB
GDScript
# 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)")
|