509 lines
25 KiB
GDScript
509 lines
25 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_camp_esc_and_hud() # 卡⑤——Esc 四态路由/三出口/设置语境分支/HUD 读数
|
||
await _step_camp_window_shell() # 真窗口化批——五窗统一窗壳形态 V1–V8(插 Esc 段之后、选档段之前)
|
||
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 await 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
|
||
ProbeUiUtil.push_click(main, esb) # 真输入(push_input 管线)→ _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 await 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〕")
|
||
|
||
|
||
# ---- ③'' 营地五窗统一窗壳(真窗口化批 V1–V8——真实进档入口+真实信号路由+真实钮/键事件)----
|
||
# 判的是呈现形态换壳(近全屏面板→居中定尺寸窗壳 720×640):窗几何/场景根留底/遮罩 0.6/关闭钮/
|
||
# 互斥单开/ESC 优先序/窗内滚动/生涯同壳——行为面(开合路由)零改,只钉窗壳可观测提交面。
|
||
func _step_camp_window_shell() -> void:
|
||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_winshell", false, false, true)
|
||
if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_winshell"):
|
||
_problems.append("[winshell] 营地入口缺失(选档层新档钮驱动中断)〔窗口化批 V1〕")
|
||
return
|
||
await _frames()
|
||
var views := {
|
||
&"forge": main.ui.modals.camp.forge,
|
||
&"chest": main.ui.modals.camp.treasure,
|
||
&"dummy": main.ui.modals.camp.tree,
|
||
&"board": main.ui.modals.camp.codex,
|
||
}
|
||
var career: Control = main.ui.modals.menu.career_overlay
|
||
var five: Array[Control] = [views[&"forge"], views[&"chest"], views[&"dummy"], views[&"board"], career]
|
||
var vp: Vector2 = main.get_viewport().get_visible_rect().size
|
||
var opened: Array[StringName] = []
|
||
for k: StringName in [&"forge", &"chest", &"dummy", &"board"]:
|
||
# 每窗先经真实关闭钮关前窗(V4 四窗半边——轮转间钉关闭链)
|
||
if not opened.is_empty():
|
||
var prev: Control = views[opened.back()]
|
||
var prev_back: Button = ProbeUiUtil.find_button(prev, "返回营地")
|
||
if prev_back == null:
|
||
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(opened.back()))
|
||
else:
|
||
prev_back.pressed.emit()
|
||
await _frames()
|
||
if prev.visible or not main.ui.modals.camp.scene.visible:
|
||
_problems.append("[winshell] %s 关闭钮按下未隐窗或场景根失守〔窗口化批 V4〕" % String(opened.back()))
|
||
main.ui.modals.camp.scene.object_pressed.emit(k)
|
||
await _frames()
|
||
opened.append(k)
|
||
var view: Control = views[k]
|
||
var panel: PanelContainer = _window_panel(view)
|
||
if panel == null:
|
||
_problems.append("[winshell] %s 窗板缺失(CenterContainer 下无 PanelContainer)〔窗口化批 V1〕" % String(k))
|
||
continue
|
||
var rect: Rect2 = panel.get_global_rect()
|
||
# V1 四窗几何:位置 ≈ (280, 80)、尺寸 ≈ (720, 640)(浮点容差 0.01——禁裸 ==)
|
||
if absf(rect.position.x - 280.0) > 0.01 or absf(rect.position.y - 80.0) > 0.01 \
|
||
or absf(rect.size.x - 720.0) > 0.01 or absf(rect.size.y - 640.0) > 0.01:
|
||
_problems.append("[winshell] %s 窗板几何偏移(实测 %s 应≈P(280,80) S(720,640))〔窗口化批 V1〕" % [String(k), str(rect)])
|
||
# V2 场景根留底:场景根 visible 且窗板四边距视口边 ≥80px
|
||
if not main.ui.modals.camp.scene.visible:
|
||
_problems.append("[winshell] %s 开态场景根不可见(栈语义失守)〔窗口化批 V2〕" % String(k))
|
||
elif rect.position.y < 80.0 - 0.01 or vp.y - rect.end.y < 80.0 - 0.01 \
|
||
or rect.position.x < 80.0 - 0.01 or vp.x - rect.end.x < 80.0 - 0.01:
|
||
_problems.append("[winshell] %s 窗板四边距视口边 <80px(场景留底不足)〔窗口化批 V2〕" % String(k))
|
||
# V3 遮罩 0.6:视图首个子节点 ColorRect alpha≈0.6 且全屏(容差 0.01)
|
||
var dim := view.get_child(0)
|
||
if not (dim is ColorRect):
|
||
_problems.append("[winshell] %s 首子节点非遮罩件(ColorRect 缺位)〔窗口化批 V3〕" % String(k))
|
||
else:
|
||
var cr := dim as ColorRect
|
||
if absf(cr.color.a - 0.6) > 0.01 or absf(cr.size.x - vp.x) > 0.01 or absf(cr.size.y - vp.y) > 0.01:
|
||
_problems.append("[winshell] %s 遮罩 alpha/尺寸偏离(实测 a=%.3f size=%s)〔窗口化批 V3〕" % [String(k), cr.color.a, str(cr.size)])
|
||
# V5a 互斥单开:五窗集 visible 计数 == 1
|
||
var vis_n := 0
|
||
for w in five:
|
||
if w.visible:
|
||
vis_n += 1
|
||
if vis_n != 1:
|
||
_problems.append("[winshell] %s 开态五窗 visible 计数=%d 应=1(互斥单开失守)〔窗口化批 V5〕" % [String(k), vis_n])
|
||
# 树窗点选遮挡(V5b——树窗开态向锻炉命中区推真实鼠标点:被遮罩挡=零新窗零响应)
|
||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||
await _frames()
|
||
var tree_view: Control = views[&"dummy"]
|
||
if not tree_view.visible:
|
||
_problems.append("[winshell] 树窗未开(V5b 前置中断)〔窗口化批 V5〕")
|
||
else:
|
||
var hotspot: Rect2 = main.ui.modals.camp.scene.get_hotspot_rect(&"forge")
|
||
var c := hotspot.get_center()
|
||
_push_click(c)
|
||
_push_release(c)
|
||
await _frames()
|
||
var vis_n2 := 0
|
||
for w in five:
|
||
if w.visible:
|
||
vis_n2 += 1
|
||
if vis_n2 != 1 or not tree_view.visible:
|
||
_problems.append("[winshell] 窗开态点穿场景(visible=%d 树窗=%s——遮罩未挡)〔窗口化批 V5〕" % [vis_n2, str(tree_view.visible)])
|
||
# 清窗经真实钮(真实 UI 中生涯入口只在无窗态可达——遮罩挡场景点选;信号直发的轮转残留
|
||
# 随栈 close 恢复语义逐层清账,V6/V8 从干净单窗态起步)
|
||
await _close_all_windows(views)
|
||
# V6 ESC 先关窗(生涯窗开态 Esc→关生涯窗、浮窗不唤起)
|
||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||
await _frames()
|
||
if not career.visible:
|
||
_problems.append("[winshell] 生涯窗未开(V6 前置中断)〔窗口化批 V6〕")
|
||
else:
|
||
_push_esc()
|
||
await _frames()
|
||
if career.visible:
|
||
_problems.append("[winshell] 生涯窗开态 Esc 未关窗(ESC 优先序失守)〔窗口化批 V6〕")
|
||
if main.ui.modals.camp.esc_overlay.visible:
|
||
_problems.append("[winshell] 关窗同拍唤起浮窗(应无窗才进浮窗)〔窗口化批 V6〕")
|
||
# V7 内容超高窗内滚动(树窗+真实投影喂加长行册)
|
||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||
await _frames()
|
||
if not tree_view.visible:
|
||
_problems.append("[winshell] 树窗未开(V7 前置中断)〔窗口化批 V7〕")
|
||
else:
|
||
var data: Dictionary = main._camp_data()
|
||
var rows: Array = data.get("tree", []) as Array
|
||
for i in 30:
|
||
rows.append({"node_id": "probe_row_%d" % i, "display_name": "探针加长行 %d" % i,
|
||
"vein": "spine", "kind": "stat", "level": 0, "cap": 1, "price": 0, "can_buy": false})
|
||
data["tree"] = rows
|
||
(tree_view as TreeView).refresh_camp(data)
|
||
await _frames(3) # queue_free 摘行与 min size 重算帧末延迟
|
||
var panel7: PanelContainer = _window_panel(tree_view)
|
||
var rect7: Rect2 = panel7.get_global_rect()
|
||
if absf(rect7.position.x - 280.0) > 0.01 or absf(rect7.position.y - 80.0) > 0.01 \
|
||
or absf(rect7.size.x - 720.0) > 0.01 or absf(rect7.size.y - 640.0) > 0.01:
|
||
_problems.append("[winshell] 加长行册后窗板涨形(实测 %s——窗不随内容涨失守)〔窗口化批 V7〕" % str(rect7))
|
||
var holder: VBoxContainer = (tree_view as TreeView)._tree_holder
|
||
var scroll := holder.get_parent() as ScrollContainer
|
||
if not (holder.size.y > scroll.size.y):
|
||
_problems.append("[winshell] 内容实高 %f 未超滚动视口 %f(超高实据不足)〔窗口化批 V7〕" % [holder.size.y, scroll.size.y])
|
||
# 清窗经真实钮(同 V6 前置——干净单窗态起步)
|
||
await _close_all_windows(views)
|
||
# V8 生涯同壳(窗板同几何+返回钮在位+内容列滚动面在)
|
||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||
await _frames()
|
||
if not career.visible:
|
||
_problems.append("[winshell] 生涯窗未开(V8 前置中断)〔窗口化批 V8〕")
|
||
else:
|
||
var panel8: PanelContainer = _window_panel(career)
|
||
if panel8 == null:
|
||
_problems.append("[winshell] 生涯窗板缺失〔窗口化批 V8〕")
|
||
else:
|
||
var rect8: Rect2 = panel8.get_global_rect()
|
||
if absf(rect8.position.x - 280.0) > 0.01 or absf(rect8.position.y - 80.0) > 0.01 \
|
||
or absf(rect8.size.x - 720.0) > 0.01 or absf(rect8.size.y - 640.0) > 0.01:
|
||
_problems.append("[winshell] 生涯窗板几何偏移(实测 %s)〔窗口化批 V8〕" % str(rect8))
|
||
if ProbeUiUtil.find_button(career, "返回", "prefix") == null:
|
||
_problems.append("[winshell] 生涯「返回」钮缺失〔窗口化批 V8〕")
|
||
var scrolls := career.find_children("*", "ScrollContainer", true, false)
|
||
var scroll_ok := false
|
||
for s in scrolls:
|
||
if (s as ScrollContainer).size_flags_vertical == Control.SIZE_EXPAND_FILL:
|
||
scroll_ok = true
|
||
if not scroll_ok:
|
||
_problems.append("[winshell] 生涯内容列滚动面缺失(ScrollContainer 纵向 EXPAND_FILL)〔窗口化批 V8〕")
|
||
# 段尾收闭:生涯窗经真实「返回」钮关闭(on_camp_closed 关闭名单不含生涯窗——防残留泄段)
|
||
var career_back: Button = ProbeUiUtil.find_button(career, "返回", "prefix")
|
||
if career_back != null:
|
||
career_back.pressed.emit()
|
||
await _frames()
|
||
if career.visible:
|
||
_problems.append("[winshell] 生涯「返回」钮未隐窗〔窗口化批 V4〕")
|
||
main._on_camp_closed() # 复归菜单语境(后续选档段前提不受扰)
|
||
await _frames()
|
||
|
||
|
||
# 清窗经真实钮(逐窗巡检 visible→钮 pressed——栈 close 恢复账随层清空)
|
||
func _close_all_windows(views: Dictionary) -> void:
|
||
for k: StringName in [&"forge", &"chest", &"dummy", &"board"]:
|
||
var w: Control = views[k]
|
||
if not w.visible:
|
||
continue
|
||
var back: Button = ProbeUiUtil.find_button(w, "返回营地")
|
||
if back == null:
|
||
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(k))
|
||
continue
|
||
back.pressed.emit()
|
||
await _frames()
|
||
|
||
|
||
# 窗板定位(视图子树固定链:首子节点=遮罩 ColorRect → CenterContainer → 窗板 PanelContainer)
|
||
func _window_panel(view: Control) -> PanelContainer:
|
||
if view.get_child_count() == 0:
|
||
return null
|
||
var dim := view.get_child(0)
|
||
if dim.get_child_count() == 0:
|
||
return null
|
||
var center := dim.get_child(0)
|
||
if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer):
|
||
return null
|
||
return center.get_child(0) as PanelContainer
|
||
|
||
|
||
# 真实鼠标合成(press+release 成对——BaseButton pressed 信号默认 release 触发,ui_input_probe :718 先例)
|
||
func _push_click(pos: Vector2) -> void:
|
||
var ev := InputEventMouseButton.new()
|
||
ev.position = pos
|
||
ev.global_position = pos
|
||
ev.button_index = MOUSE_BUTTON_LEFT
|
||
ev.pressed = true
|
||
main.get_viewport().push_input(ev, true)
|
||
|
||
|
||
func _push_release(pos: Vector2) -> void:
|
||
var ev := InputEventMouseButton.new()
|
||
ev.position = pos
|
||
ev.global_position = pos
|
||
ev.button_index = MOUSE_BUTTON_LEFT
|
||
ev.pressed = false
|
||
main.get_viewport().push_input(ev, true)
|
||
|
||
|
||
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 单例动作态)
|