# VERTICAL SLICE - NOT FOR PRODUCTION # 整屏截图探针(迭代 9 卡⑰a 2026-09-30——呈现层测试分级 B 档基建:四屏视口截图程序化产出)。 # 必须**窗口化**运行(真渲染设备——headless 无渲染面,get_image 得到空纹理): # Godot_console.exe --path . res://smoke/shot_probe.tscn -- <输出目录> # 流程 = 真实按钮信号路径驱动(ui_click_probe 同款;存档/设置/元进度三面换注隔离档——禁碰真实档, # 坑册「断点写盘随 save_manager 引用走」教训同款覆盖):菜单(启动态)→ 营地(campbtn)→ # 关卡画面(lvbtn→停点出图)→ 战斗(level_screen.press_candidate(0) 真实选路提交)。 # 每屏双帧 + frame_post_draw 同步后抓 get_viewport().get_texture(),PNG 落输出目录; # 最小自检 = 四件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。 extends Node var main var out_dir := "" var _problems: Array = [] var _meta := {} # 控件实测几何(全局 rect——shot_checker 文字框/明度带取样坐标源,禁硬编码像素) func _ready() -> void: 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 同款——真档零污染) ── for p0 in ["user://smoke_shot_probe.json", "user://smoke_shot_probe.json.tmp", "user://smoke_shot_probe_settings.cfg"]: if FileAccess.file_exists(p0): DirAccess.remove_absolute(p0) var iso_c = load("res://core/save_manager.gd").new("user://smoke_shot_probe.json") iso_c.load_from_disk() main.save_manager = iso_c main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c) main.settings_store = load("res://presentation/settings_store.gd").new("user://smoke_shot_probe_settings.cfg") await get_tree().process_frame # ① 菜单(启动态) _meta["menu_sub"] = _find_label_rect(main._start_overlay, "改造批") _meta["menu_help"] = _find_label_rect(main._start_overlay, "玩法:") _meta["menu_title"] = _find_label_rect(main._start_overlay, "Dungeon Sweeper") await _snap("menu") # ② 营地 var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地") if campbtn == null: _problems.append("营地入口缺失(开始层 campbtn)") else: campbtn.pressed.emit() await get_tree().process_frame await _snap("camp") var close_btn: Button = _find_button_by_prefix(main.camp_overlay, "关闭营地") if close_btn != null: close_btn.pressed.emit() await get_tree().process_frame # ③ 关卡画面(开始冒险→停点出图——冒险开始即出图,停点前移批语义) var lvbtn: Button = _find_button_by_prefix(main._start_overlay, "开始冒险") if lvbtn == null: _problems.append("开始冒险按钮缺失(开始层 lvbtn)") else: lvbtn.pressed.emit() for i in 4: await get_tree().process_frame await _snap("level") # GP 常驻徽章断言(迭代 9 卡⑪核验——读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕) var gp_label: Label = _find_label_by_text(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 取样坐标源) _meta["battle_log_hint"] = _find_label_rect(main.stage, "结算明细") _meta["hero_panel"] = _node_rect(_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 真机测量(迭代 9 卡⑦——基线 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) else: _problems.append("关卡画面未出现(lvbtn 路径)") # ── 几何元数据落盘(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", "camp", "level", "battle"]: 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 _find_button_by_prefix(root: Node, prefix: String) -> Button: if root == null: return null if root is Button and String(root.text).contains(prefix): return root for c in root.get_children(): var r := _find_button_by_prefix(c, prefix) if r != null: return r return null # 控件实测几何采集(checker 断言坐标源——禁硬编码像素,分辨率档位自适应) func _find_label_rect(root: Node, text_fragment: String) -> Dictionary: if root == null: return {} if root is Label and String(root.text).contains(text_fragment): return {"x": root.global_position.x, "y": root.global_position.y, "w": root.size.x, "h": root.size.y} for c in root.get_children(): var r := _find_label_rect(c, text_fragment) if not r.is_empty(): return r return {} func _find_label_by_text(root: Node, text_fragment: String) -> Label: if root == null: return null if root is Label and String(root.text).contains(text_fragment): return root for c in root.get_children(): var r := _find_label_by_text(c, text_fragment) if r != null: return r return null 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} func _find_by_script(root: Node, script_name: String) -> Node: if root == null: return null var s: Script = root.get_script() if s != null and String(s.resource_path).contains(script_name): return root for c in root.get_children(): var r := _find_by_script(c, script_name) if r != null: return r return null