221 lines
12 KiB
GDScript
221 lines
12 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 整屏截图探针(呈现层测试分级 B 档基建:视口截图程序化产出;七屏——
|
||
# 菜单/设置面/生涯页/营地/关卡画面/战斗/状态面板,现行事实源=tools/render_gate.sh)。
|
||
# 必须**窗口化**运行(真渲染设备——headless 无渲染面,get_image 得到空纹理):
|
||
# Godot_console.exe --path . res://probe/shot_probe.tscn -- <输出目录>
|
||
# 流程 = 真实按钮信号路径驱动(ui_click_probe 同款;存档/设置/元进度三面换注隔离档——禁碰真实档,
|
||
# 坑册「断点写盘随 save_manager 引用走」教训同款覆盖):菜单(启动态)→ 设置面(settingsbtn
|
||
# 真实信号路径)→ 生涯页(函数直调中间态——卡② mn10)→ 营地(进档开层新路径——mn9)→ 关卡画面(函数直调中间态→停点出图)→
|
||
# 战斗(level_screen.press_candidate(0) 真实选路提交)→ 状态面板(地图屏状态角标真实信号路径)。
|
||
# 每屏双帧 + frame_post_draw 同步后抓 get_viewport().get_texture(),PNG 落输出目录;
|
||
# 最小自检 = 七件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。
|
||
# 商店房不纳屏(真实路径口径):真实路径必经 L1 打穿→L2 商店节点
|
||
# 选路(商店层在 L2 Boss 前必经位)——探针现行驱动只进首战节点,低代价确定性
|
||
# 到达不成立;合成投喂(ui_struct_probe 同款)破坏本探针真实路径口径 → 不纳屏。
|
||
extends Node
|
||
|
||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||
|
||
var main: Node
|
||
var out_dir := ""
|
||
var _problems: Array = []
|
||
var _meta := {} # 控件实测几何(全局 rect——shot_checker 文字框/明度带取样坐标源,禁硬编码像素)
|
||
|
||
|
||
func _ready() -> void:
|
||
# 运行前提=窗口化真渲染面:headless 下 frame_post_draw 永不触发+视口纹理恒空——无此守卫时 headless 调用方必挂死
|
||
if DisplayServer.get_name() == "headless":
|
||
print("[shot_probe] FAIL — 本探针必须窗口化运行(headless 无渲染面,截图必空);headless 自动闸禁跑本探针(现行事实源=tools/probe_gate.sh)")
|
||
get_tree().quit(1)
|
||
return
|
||
var args := OS.get_cmdline_user_args()
|
||
out_dir = args[0] if args.size() >= 1 and args[0] != "" else "user://qa-screens"
|
||
DirAccess.make_dir_recursive_absolute(out_dir)
|
||
await get_tree().process_frame
|
||
main = load("res://presentation/main.tscn").instantiate()
|
||
add_child(main)
|
||
await get_tree().process_frame
|
||
# ── 存档/元进度/设置三面隔离(ui_click_probe 同款——真档零污染;历史语义 = contains 寻址) ──
|
||
# refresh_resume 开——续玩钮按隔离档(无断点)定基,菜单截图/golden 基线不依赖
|
||
# 真实档断点态(boot 关系由冒烟 _smoke_boot_chain V2d 断,此处只保截图确定性)
|
||
ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, true)
|
||
# 隔离档即时应用(main 装配已按真实设置恢复过窗口态〔本机实测:真实档
|
||
# fullscreen=true 时窗口进全屏,视口纹理 2304x1440≠1280 基准档,checker 尺寸/取样全盘失真〕;
|
||
# apply 隔离档出厂默认=窗口化 1280x800,恢复本探针设计前提。探针侧环境修正,零运行时改动)
|
||
main.settings_store.apply()
|
||
await get_tree().process_frame
|
||
# ① 菜单(启动态)——无帮助文腿(玩法速览牌不在画面,checker 无对应断言段);
|
||
# 无副标题件,菜单文字锚只剩标题
|
||
# 自检一:boot 态顶层可见全屏层=start_overlay(内容层树遍历——屏壳是容器不当候选:
|
||
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
|
||
var top: Node = null
|
||
var top_rx := -1
|
||
var top_ry := -1
|
||
for c: Node in [main._start_overlay, main._battle_root, main._inter_overlay,
|
||
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
|
||
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
|
||
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]:
|
||
if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible:
|
||
continue
|
||
var rx: int = c.get_parent().get_index()
|
||
var ry: int = c.get_index()
|
||
if top == null or rx > top_rx or (rx == top_rx and ry > top_ry):
|
||
top = c
|
||
top_rx = rx
|
||
top_ry = ry
|
||
if top != main._start_overlay:
|
||
_problems.append("boot 态顶层可见层≠start_overlay(实测 %s——菜单被埋)〔装配生命批 V4〕"
|
||
% (top.name if top != null else "null"))
|
||
# 自检二:菜单标题 label 必须 is_visible_in_tree 且 rect 落屏内
|
||
#(对比度只判反差不判「字在」——本条+checker 墨色占比共同补内容判据)
|
||
var vp := get_viewport().get_visible_rect()
|
||
for pair in [["menu_title", "Dungeon Sweeper"]]:
|
||
var lbl: Label = ProbeUiUtil.find_label(main._start_overlay, String(pair[1]))
|
||
if lbl == null:
|
||
_problems.append("菜单文字缺失: %s(find_label 零命中)〔装配生命批 V4〕" % String(pair[1]))
|
||
continue
|
||
if not lbl.is_visible_in_tree():
|
||
_problems.append("菜单文字不可见: %s(is_visible_in_tree=false——层被埋或隐藏)〔装配生命批 V4〕" % String(pair[1]))
|
||
var gr: Rect2 = lbl.get_global_rect()
|
||
if not vp.encloses(gr):
|
||
_problems.append("菜单文字 rect 越出视口: %s(%s ⊄ %s)〔装配生命批 V4〕" % [String(pair[1]), str(gr), str(vp)])
|
||
_meta[String(pair[0])] = ProbeUiUtil.label_rect(main._start_overlay, String(pair[1]))
|
||
await _snap("menu")
|
||
# ①b 设置面(settingsbtn 真实信号路径,开合同款真实钮)
|
||
var setbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
|
||
if setbtn == null:
|
||
_problems.append("设置入口缺失(开始层 settingsbtn)")
|
||
else:
|
||
setbtn.pressed.emit()
|
||
for i in 3:
|
||
await get_tree().process_frame
|
||
if not main.settings_overlay.visible:
|
||
_problems.append("设置面未打开(settingsbtn 路径)")
|
||
else:
|
||
_meta["settings_title"] = ProbeUiUtil.label_rect(main.settings_overlay, "设置")
|
||
await _snap("settings")
|
||
var set_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
|
||
if set_close == null:
|
||
_problems.append("设置面返回钮缺失")
|
||
else:
|
||
set_close.pressed.emit()
|
||
await get_tree().process_frame
|
||
# ①c 生涯页(函数直调中间态——卡② mn10:careerbtn 已撤,卡④ 笔记本改挂前无 UI 路径)
|
||
if true:
|
||
main._open_career()
|
||
for i in 3:
|
||
await get_tree().process_frame
|
||
if not main.career_overlay.visible or main._start_overlay.visible:
|
||
_problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)")
|
||
else:
|
||
_meta["career_title"] = ProbeUiUtil.label_rect(main.career_overlay, "生 涯")
|
||
await _snap("career")
|
||
var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix")
|
||
if career_close == null:
|
||
_problems.append("生涯页返回钮缺失")
|
||
else:
|
||
career_close.pressed.emit()
|
||
for i in 3:
|
||
await get_tree().process_frame
|
||
if main.career_overlay.visible or not main._start_overlay.visible:
|
||
_problems.append("生涯页关闭后页未隐藏或开始层未恢复")
|
||
# ② 营地(进档开层新路径——卡② mn9;槽模板注入隔离命名空间)
|
||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"):
|
||
_problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)")
|
||
else:
|
||
await get_tree().process_frame
|
||
await _snap("camp")
|
||
var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains")
|
||
if close_btn != null:
|
||
close_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态)
|
||
await get_tree().process_frame
|
||
# ③ 关卡画面(开始冒险→停点出图——入口停点语义;函数直调中间态 mn10)
|
||
main._start_adventure_from_menu()
|
||
for i in 4:
|
||
await get_tree().process_frame
|
||
await _snap("level")
|
||
# GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕)
|
||
var gp_label: Label = ProbeUiUtil.find_label(main.level_screen, "◈ ")
|
||
if gp_label == null or not gp_label.is_visible_in_tree():
|
||
_problems.append("GP 徽章不可见(关卡画面顶栏)")
|
||
elif main.level_screen.get("_runner") != null:
|
||
var expect := "◈ %d" % int(main.level_screen.get("_runner").gp)
|
||
if gp_label.text != expect:
|
||
_problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect])
|
||
# ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款)
|
||
if main.level_screen != null and main.level_screen.visible:
|
||
main.level_screen.press_candidate(0)
|
||
for i in 10:
|
||
await get_tree().process_frame
|
||
await _snap("battle")
|
||
# 战斗屏文字框/明度带锚点(全局 rect——checker 取样坐标源;明细带锚 S1 裁后改认现行文案)
|
||
_meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.stage, "上轮摘要")
|
||
_meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view"))
|
||
_meta["stage_view"] = _node_rect(main.stage)
|
||
_meta["board_zone"] = _node_rect(main.board_zone)
|
||
# draw calls 真机测量(基线 270→目标 <200;窗口化渲染帧读数,
|
||
# 取值时机=战斗截图同帧后一帧——呈现终态满负荷)
|
||
await get_tree().process_frame
|
||
var dc := int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME))
|
||
_meta["battle_draw_calls"] = dc
|
||
print("[shot_probe] battle draw_calls=%d(基线 270/目标 <200)" % dc)
|
||
# ⑤ 状态面板(战斗屏状态角标真实信号路径;层在战斗屏之上开合)
|
||
var pb: Button = main.board_zone.status_badge
|
||
if pb == null or not pb.visible or pb.disabled:
|
||
_problems.append("状态面板入口角标缺失/置灰(战斗屏 board_zone)")
|
||
else:
|
||
pb.pressed.emit()
|
||
for i in 3:
|
||
await get_tree().process_frame
|
||
if not main.status_panel.visible:
|
||
_problems.append("状态面板未打开(角标路径)")
|
||
else:
|
||
_meta["status_title"] = ProbeUiUtil.label_rect(main.status_panel, "状态")
|
||
await _snap("status")
|
||
var status_close: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||
if status_close == null:
|
||
_problems.append("状态面板继续钮缺失")
|
||
else:
|
||
status_close.pressed.emit()
|
||
await get_tree().process_frame
|
||
if main.status_panel.visible:
|
||
_problems.append("状态面板关闭后未隐藏(继续钮路径)")
|
||
else:
|
||
_problems.append("关卡画面未出现(函数直调中间态路径)")
|
||
# ── 几何元数据落盘(shot_checker 取样坐标源) ──
|
||
var mf := FileAccess.open(out_dir.path_join("shot_meta.json"), FileAccess.WRITE)
|
||
if mf != null:
|
||
mf.store_string(JSON.stringify(_meta))
|
||
mf.close()
|
||
# ── 最小自检:七件存在且非零字节 ──
|
||
for s in ["menu", "settings", "career", "camp", "level", "battle", "status"]:
|
||
var p := out_dir.path_join("shot_%s.png" % s)
|
||
if not FileAccess.file_exists(p):
|
||
_problems.append("截图缺失: %s" % p)
|
||
else:
|
||
var fa := FileAccess.open(p, FileAccess.READ)
|
||
if fa == null or fa.get_length() < 1000:
|
||
_problems.append("截图疑似空文件: %s" % p)
|
||
print("[shot_probe] 完成 out=%s 问题=%d %s" % [out_dir, _problems.size(), str(_problems)])
|
||
get_tree().quit(1 if _problems.size() > 0 else 0)
|
||
|
||
|
||
func _snap(key: String) -> void:
|
||
await RenderingServer.frame_post_draw
|
||
await RenderingServer.frame_post_draw
|
||
var img: Image = get_viewport().get_texture().get_image()
|
||
if img == null:
|
||
_problems.append("视口纹理为空: %s" % key)
|
||
return
|
||
img.save_png(out_dir.path_join("shot_%s.png" % key))
|
||
print("[shot_probe] saved %s (%dx%d)" % [key, img.get_width(), img.get_height()])
|
||
|
||
|
||
func _node_rect(n: Node) -> Dictionary:
|
||
if n == null or not (n is CanvasItem):
|
||
return {}
|
||
return {"x": n.global_position.x, "y": n.global_position.y,
|
||
"w": n.size.x, "h": n.size.y}
|