# VERTICAL SLICE - NOT FOR PRODUCTION # 整屏截图探针(呈现层测试分级 B 档基建:视口截图程序化产出;七屏+九态—— # 菜单/设置面/生涯页/营地/关卡画面/战斗/状态面板+营地五窗开态/图例/战末结算/三选一/调试浮窗, # 现行事实源=tools/render_gate.sh;九态=golden 补锚批 2026-11-08)。 # 必须**窗口化**运行(真渲染设备——headless 无渲染面,get_image 得到空纹理): # Godot_console.exe --path . res://probe/shot_probe.tscn -- <输出目录> # 流程 = 真实按钮信号路径驱动(ui_click_probe 同款;存档/设置/元进度三面换注隔离档——禁碰真实档, # 坑册「断点写盘随 save_manager 引用走」教训同款覆盖):菜单(启动态)→ 设置面(settingsbtn # 真实信号路径)→ 生涯页(函数直调中间态——卡② mn10)→ 营地(进档开层新路径——mn9)→ 关卡画面(函数直调中间态→停点出图)→ # 战斗(level_screen candidate_rect 命中矩形真点选路提交)→ 状态面板(地图屏状态角标真实信号路径)。 # 每屏双帧 + 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 无对应断言段); # 无副标题件,菜单文字锚只剩标题 # 判据出处: design/engineering/code-conventions.md:54 模态覆盖层层级规则——boot 态顶层=start_overlay、菜单文字在屏(菜单组) # 自检一: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.run.legend_overlay, main.ui.modals.run.node_overlay, main.ui.modals.run.level_screen, main.ui.modals.run.curio_overlay, main.ui.modals.run.end_overlay, main.ui.modals.menu.shop_overlay, main.ui.modals.camp.scene, main.ui.modals.menu.settings_overlay, main.ui.modals.run.status_panel, main.ui.modals.menu.career_overlay, main.ui.modals.menu.confirm_dialog, main.ui.modals.menu.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 真实信号路径,开合同款真实钮) # 判据出处: design/gdd/systems/表现与支撑/设置与本地化.md:95 最小设置面已兑现:音量/减动效/全屏/退出,载体=独立设置存储+设置界面视图(设置组) var setbtn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix") if setbtn == null: _problems.append("设置入口缺失(开始层 settingsbtn)") else: ProbeUiUtil.push_click(main, setbtn) for i in 3: await get_tree().process_frame if not main.ui.modals.menu.settings_overlay.visible: _problems.append("设置面未打开(settingsbtn 路径)") else: _meta["settings_title"] = ProbeUiUtil.label_rect(main.ui.modals.menu.settings_overlay, "设置") await _snap("settings") var set_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix") if set_close == null: _problems.append("设置面返回钮缺失") else: ProbeUiUtil.push_click(main, set_close) await get_tree().process_frame # ①c 生涯页(函数直调中间态——卡② mn10:careerbtn 已撤,卡④ 笔记本改挂前无 UI 路径) # 判据出处: design/ux/camp.md:45 生涯页与四子界面同壳同形态(生涯组) if true: main._open_career() for i in 3: await get_tree().process_frame if not main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: _problems.append("生涯页未打开或开始层未隐藏(函数直调中间态路径)") else: _meta["career_title"] = ProbeUiUtil.label_rect(main.ui.modals.menu.career_overlay, "生 涯") await _snap("career") var career_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, "返回", "prefix") if career_close == null: _problems.append("生涯页返回钮缺失") else: ProbeUiUtil.push_click(main, career_close) for i in 3: await get_tree().process_frame if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible: _problems.append("生涯页关闭后页未隐藏或开始层未恢复") # ② 营地场景图(进档开层新路径——卡② mn9;截图=可点击场景图形态——卡④ 契约 9 V1 验收载体) # 判据出处: design/ux/camp.md:16 营地=可点击场景图(七物件命中区表——场景图形态组) if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_shot_probe_camp"): _problems.append("营地入口缺失(选档层新档钮——新路径驱动中断)〔句柄子容器批 V1〕") else: await get_tree().process_frame await _snap("camp") # ②b 营地五窗打开态(真点热点开窗→截→「返回」前缀真点关——screen_flow_probe 窗壳链同款) # 判据出处: design/ux/camp.md:45 五窗统一窗壳形态·互斥单开(营地窗态组) for k: StringName in [&"forge", &"chest", &"dummy", &"board", &"notebook"]: ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(k).get_center()) for i in 3: await get_tree().process_frame var wv: Control = main.ui.modals.camp.forge if k == &"forge" else ( main.ui.modals.camp.treasure if k == &"chest" else ( main.ui.modals.camp.tree if k == &"dummy" else ( main.ui.modals.camp.codex if k == &"board" else main.ui.modals.menu.career_overlay))) # 截图键=基线语义名(dummy→tree 天赋树/board→codex 图鉴——热点键≠呈现名,映射单点在此) var wkey: String = {"forge": "forge", "chest": "chest", "dummy": "tree", "board": "codex", "notebook": "notebook"}[String(k)] if not wv.visible: _problems.append("营地窗未打开: %s(真点热点路径)" % String(k)) else: await _snap("camp_%s" % wkey) var back: Button = ProbeUiUtil.find_button(wv, "返回", "prefix") if back == null: _problems.append("营地窗返回钮缺失: %s" % String(k)) else: ProbeUiUtil.push_click(main, back) for i in 3: await get_tree().process_frame ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"campfire").get_center()) # 收闭改接篝火动线(营地根态无关闭钮——卡④ T5) await get_tree().process_frame ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 二次确认离场(关层收闭——选档层随收) # 判据出处: design/ux/camp.md:22 篝火=离开营地回主菜单、必须二次确认(篝心动线组) await get_tree().process_frame # ③ 关卡画面(开始冒险→停点出图——入口停点语义;函数直调中间态 mn10) # 判据出处: design/ux/level-map.md:11 入口停点候选数=入口个数(关卡画面出图组) main._start_adventure_from_menu() for i in 4: await get_tree().process_frame await _snap("level") # GP 常驻徽章断言(读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕) # 判据出处: design/ux/level-map.md:77 GP 常驻徽章=GP 图标+数字(冒险钱包读数;写路径回执后重查) var gp_label: Label = ProbeUiUtil.find_label(main.ui.modals.run.level_screen, "◈ ") if gp_label == null or not gp_label.is_visible_in_tree(): _problems.append("GP 徽章不可见(关卡画面顶栏)") elif main.ui.modals.run.level_screen.get("_runner") != null: var expect := "◈ %d" % int(main.ui.modals.run.level_screen.get("_runner").gp) if gp_label.text != expect: _problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect]) # ④ 战斗(节点一——candidate_rect 命中矩形真点选路提交,ui_click_probe 同款) # 判据出处: design/ux/battle.md:122 B 区构建:站桩/明细带/轮次指示(战斗出图与几何锚组) if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible: # 接触带呼吸相位冻结(board_shot_probe 同款:motion_scale<1.0 ⇒ 施静态带色)—— # golden 基线逐像素比对禁动画相位抖动;必须在进战 rebuild 前置位(rebuild/带色施用按当时相位取样) UiKit.motion_scale = 0.5 ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) for i in 10: await get_tree().process_frame # 全板淡入+涟漪完结锚定(board_shot_probe 同款 1.2s=2×余量——基线禁采动画中途帧) await get_tree().create_timer(1.2).timeout 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;窗口化渲染帧读数, # 判据出处: design/gdd/systems/表现与支撑/HUD框架.md:264 同屏 draw calls < 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) # ⑤ 状态面板(战斗屏状态角标真实信号路径;层在战斗屏之上开合) # 判据出处: design/gdd/systems/表现与支撑/HUD框架.md:277 模态打开遮挡或输入锁定、关闭逐层恢复(状态面板组) 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: ProbeUiUtil.push_click(main, pb) for i in 3: await get_tree().process_frame if not main.ui.modals.run.status_panel.visible: _problems.append("状态面板未打开(角标路径)") else: _meta["status_title"] = ProbeUiUtil.label_rect(main.ui.modals.run.status_panel, "状态") await _snap("status") var status_close: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix") if status_close == null: _problems.append("状态面板继续钮缺失") else: ProbeUiUtil.push_click(main, status_close) await get_tree().process_frame if main.ui.modals.run.status_panel.visible: _problems.append("状态面板关闭后未隐藏(继续钮路径)") # ⑤b 图例浮窗(战斗屏 AP 条真点开→截→close() 关——ui_click_probe 真点先例) # 判据出处: design/gdd/systems/表现与支撑/HUD框架.md:88 图例面板=模态族(图例组) var lgb: Button = main.ui.battle.board_zone.ap_bar._btn_legend if lgb == null or not lgb.visible: _problems.append("图例钮缺失/不可见(战斗屏 AP 条)") else: ProbeUiUtil.push_click(main, lgb) for i in 3: await get_tree().process_frame if not main.ui.modals.run.legend_overlay.visible: _problems.append("图例层未打开(图例钮路径)") else: await _snap("legend") main.ui.modals.run.legend_overlay.close() for i in 3: await get_tree().process_frame # ⑤c 战末结算浮层(直调 open_victory 喂真实形态摘要——ui_struct_probe 合成先例) # 判据出处: design/gdd/systems/表现与支撑/HUD框架.md:276 终局:胜利金蜡封(战末结算组) main.ui.modals.run.end_overlay.open_victory("胜利", "继续", Rect2(200, 200, 400, 300), {"gp_summary": "GP +3", "ks_summary": "击杀 2", "bonus_summary": "无伤奖励", "gp_gain": 3, "gp_total": 3}) for i in 3: await get_tree().process_frame if not main.ui.modals.run.end_overlay.visible: _problems.append("战末结算浮层未打开(open_victory 直调)") else: await _snap("battle_end") main.ui.modals.run.end_overlay.close() for i in 3: await get_tree().process_frame # ⑤d 三选一浮层(直调 open_pick 喂真实形态卡——ui_struct_probe 合成先例) # 判据出处: design/gdd/systems/冒险循环/奇物.md:166 三选一呈现(三选一组) main.ui.modals.run.curio_overlay.open_pick("三选一", [ {"id": "probe_heal", "text": "回血\n恢复 3 点"}, {"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"}, {"id": "probe_pierce", "text": "穿甲\n无视护甲"}, ]) for i in 3: await get_tree().process_frame if not main.ui.modals.run.curio_overlay.visible: _problems.append("三选一浮层未打开(open_pick 直调)") else: await _snap("curio_pick") main.ui.modals.run.curio_overlay.visible = false for i in 3: await get_tree().process_frame # ⑤e 调试浮窗(函数直调开/关——ui_struct_probe/ui_tune_probe 先例;对局中可开) # 判据出处: design/engineering/test-conventions.md:65 人工体验=DebugOverlay/DS_DEBUG 调试面(调试浮窗组) main._show_debug_overlay() for i in 3: await get_tree().process_frame if not main.ui.modals.menu.debug_overlay.visible: _problems.append("调试浮窗未打开(_show_debug_overlay 直调)") else: await _snap("debug") main._close_debug_overlay() for i in 3: await get_tree().process_frame 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", "camp_forge", "camp_chest", "camp_tree", "camp_codex", "camp_notebook", "legend", "battle_end", "curio_pick", "debug"]: 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: # 判据出处: design/engineering/test-conventions.md:84 golden 测试项扩项——截图采集清单扩项、实跑截图冻结(截图采集组) 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}