Files
gplan-2/game/probe/screen_flow_probe.gd
T

305 lines
15 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 屏壳切换探针
# 判的是「跑起来之后,屏与场景的切换对不对」:菜单 ⇄ 营地 ⇄ 冒险 ⇄ 终局 的主流程与回退路径,
# 每一步断言 **屏级互斥**(同一时刻最多一个屏可见)+ 流程状态位落到正确值。
# 与 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_camp_esc_and_hud() # 卡⑤——Esc 四态路由/三出口/设置语境分支/HUD 读数
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.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
"营地": main.ui.modals.camp.scene != null and main.ui.modals.camp.scene.visible,
"冒险": main.run_scope != null and main.run_scope.is_adventure,
"终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.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.ui.modals.menu.debug_overlay != null and main.ui.modals.menu.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.ui.modals.camp.scene == null:
_problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕")
return
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
await _frames()
if not main.ui.modals.menu.confirm_dialog.visible:
_problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕")
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4)
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.ui.screens.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.ui.modals.menu.save_select_overlay.visible or not seals_check_single(seals):
_problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.ui.modals.menu.save_select_overlay.visible))
var back_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix")
if back_btn == null:
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
else:
back_btn.pressed.emit()
await get_tree().process_frame
if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.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.ui.modals.menu.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.ui.modals.menu.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.ui.modals.run.level_screen != null and not main.ui.modals.run.level_screen.visible:
_problems.append("④菜单→冒险:入口停点未落在关卡地图屏(level_screen 不可见)")
# ---- ⑤ 冒险内屏切换:关卡地图屏 ⇄ 战斗屏(屏内两态,屏级仍独占)----
func _step_level_to_battle_and_back() -> void:
if not main.ui.modals.run.level_screen.visible:
_problems.append("⑤冒险内切换:前置不在关卡地图屏")
return
main._exit_level_screen()
await _frames()
if main.ui.modals.run.level_screen.visible:
_problems.append("⑤冒险内切换:退出关卡屏后 level_screen 仍可见")
if main.ui.screens.battle_root != null and not main.ui.screens.battle_root.visible:
_problems.append("⑤冒险内切换:战斗屏未接手(battle_root 不可见)")
_expect_exclusive("⑤冒险内切换(关卡→战斗)", "冒险")
main._enter_level_screen()
await _frames()
if not main.ui.modals.run.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.ui.modals.run.end_overlay != null and not main.ui.modals.run.end_overlay.visible:
_problems.append("⑥放弃→终局:终局覆盖层未开")
# ---- ⑦ 终局 → 菜单(终局出口返回开始界面)----
func _step_end_back_to_menu() -> void:
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.visible:
_problems.append("⑦终局→菜单:前置不在终局")
return
main.ui.modals.run.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)")
# ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)----
func _step_camp_esc_and_hud() -> void:
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true)
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"):
_problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕")
return
await _frames()
_push_esc()
await _frames()
if not main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] 营地根态 Esc 未唤起浮窗(契约 9③)〔ESC批 V1〕〔ESC批 B1〕")
_push_esc()
await _frames()
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] 再按 Esc 未关浮窗(Q2 同键单义切换失守)〔ESC批 V1〕")
_push_esc(true) # echo 连发(真实键盘长按形态)
await _frames()
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕")
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态
await _frames()
_push_esc()
await _frames()
if main.ui.modals.camp.tree.visible:
_problems.append("[esc] 子界面开态 Esc 未关窗(camp.md v2.3 定稿——先关最上层窗)〔ESC批 V1〕")
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] 关窗同拍唤起浮窗(先关窗后浮窗——两拍应分离)〔ESC批 V1〕")
await _frames()
# HUD 余额(契约 5 时机③——夹具直写后显式触发才变;禁 band 自刷假设)
main.meta.meta_state._add_keystone(9999)
main._camp_pres.views.refresh_bus.refresh(main._camp_data()) # 显式触发刷新单口
var band: Control = main.ui.modals.camp.scene.get_node("HudBandSlot").get_child(0)
if not String(band._badge.value_label.text).begins_with("钥石 9999"):
_problems.append("[esc] HUD 余额读数≠存档单口现值(直写+显式触发未变)〔ESC批 V6〕〔ESC批 B2〕")
# 设置开在浮窗之上→Esc 无反馈且浮窗不误关(契约 9①);关设置回浮窗不回主菜单(契约 3 语境分支)
_push_esc()
await _frames()
main._on_esc_settings()
await _frames()
if not main.ui.modals.menu.settings_overlay.visible:
_problems.append("[esc] 浮窗设置出口断裂〔ESC批 V4〕")
_push_esc()
await _frames()
if not main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] 设置开态 Esc 误关浮窗(契约 9①——浮窗不隔层关闭)〔ESC批 V1〕")
main._on_settings_closed()
await _frames()
if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible:
_problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕〔ESC批 B3〕")
# 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显)
main.ui.modals.camp.esc_overlay.return_requested.emit()
await _frames()
if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
_problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕〔ESC批 B3〕")
# 非营地语境(boot 菜单)Esc→无响应(契约 9④ 回归面)
_push_esc()
await _frames()
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] 非营地语境 Esc 唤起浮窗(契约 9④ 失守)〔ESC批 V1〕")
func _push_esc(echo := false) -> void:
var ev := InputEventKey.new()
ev.keycode = KEY_ESCAPE
ev.physical_keycode = KEY_ESCAPE
ev.pressed = true
ev.echo = echo
main.get_viewport().push_input(ev, true) # 真实键盘合成路径(:153 先例——push_input 不更新 Input 单例动作态)