# 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 商店节点 # 选路(商店按摆放约束恒在终点前一位,每局必经恰一店)——探针现行驱动只进首战节点,低代价确定性 # 到达不成立;合成投喂(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.ui.screens.start_overlay, main.ui.screens.battle_root, main.ui.screens.inter_overlay, main.ui.modals.legend_overlay, main.ui.modals.node_overlay, main.ui.modals.level_screen, main.ui.modals.curio_overlay, main.ui.modals.end_overlay, main.ui.modals.shop_overlay, main.ui.modals.camp_overlay, main.ui.modals.settings_overlay, main.ui.modals.status_panel, main.ui.modals.career_overlay, main.ui.modals.confirm_dialog, main.ui.modals.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.ui.screens.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.ui.screens.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.ui.screens.start_overlay, String(pair[1])) await _snap("menu") # ①b 设置面(settingsbtn 真实信号路径,开合同款真实钮) var setbtn: Button = ProbeUiUtil.find_button(main.ui.screens.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.ui.modals.settings_overlay.visible: _problems.append("设置面未打开(settingsbtn 路径)") else: _meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.settings_overlay, "设置") await _snap("settings") var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.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.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)") else: _meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.career_overlay, "生 涯") await _snap("career") var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.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.ui.modals.career_overlay.visible or not main.ui.screens.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.ui.modals.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.ui.modals.level_screen, "◈ ") if gp_label == null or not gp_label.is_visible_in_tree(): _problems.append("GP 徽章不可见(关卡画面顶栏)") elif main.ui.modals.level_screen.get("_runner") != null: var expect := "◈ %d" % int(main.ui.modals.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.ui.modals.level_screen != null and main.ui.modals.level_screen.visible: main.ui.modals.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.ui.battle.stage, "上轮摘要") _meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view")) _meta["stage_view"] = _node_rect(main.ui.battle.stage) _meta["board_zone"] = _node_rect(main.ui.battle.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.ui.battle.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.ui.modals.status_panel.visible: _problems.append("状态面板未打开(角标路径)") else: _meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.status_panel, "状态") await _snap("status") var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.status_panel, "继续", "prefix") if status_close == null: _problems.append("状态面板继续钮缺失") else: status_close.pressed.emit() await get_tree().process_frame if main.ui.modals.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}