# VERTICAL SLICE - NOT FOR PRODUCTION # 演出渲染证据探针(真实渲染设备抓战斗演出瞬间帧)。 # 必须**窗口化**运行(真渲染设备——headless 无渲染面):Godot_console.exe --path . res://probe/fx_shot_probe.tscn -- <输出目录> # 流程 = shot_probe 同款隔离面(存档/设置/元进度换注隔离档,禁碰真实档)+ 真实按钮路径开冒险进首战; # 自动玩家挖格驱动行结算,在 行到达/弹道飞行/结算排播 三时刻抓帧——演出链「逻辑在(fx_probe PASS)× # 渲染在(本探针 PNG)」双证的第二半边。PNG 落输出目录(artwork/qa-fx-*/,素材纪律不入 git)。 extends Node const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点 # ── 演出采样窗口口径(headless/窗口化帧率不定,帧时刻以墙钟近似锚定;改窗口 = 改抓帧 # 时刻观测面,非断言语义。窗口值与字面量同值——仅提为具名常量。) ── const T_DIG_FLIGHT := 0.12 # 挖格行到达/弹道在飞窗口(settle_batch 回执后停留) const T_SETTLE := 0.35 # 结算排播窗口(行到达后二连抓) const T_ROUND_TICK := 0.25 # 停手轮末(敌方攻击弹道/受击闪白 0.2-1.6s 流过)连抓 6 帧的采样步距 const T_FINAL := 0.6 # 收尾帧前静置 var main: Node var out_dir := "" var _problems: Array = [] func _ready() -> void: var args := OS.get_cmdline_user_args() out_dir = args[0] if args.size() >= 1 and args[0] != "" else "user://qa-fx" 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 ProbeUiUtil.isolate_save(main, "fx_shot_probe", false, true, true) # 末参=swap_save(卡②签名换代——原 refresh_resume 槽位语义退役) # 隔离档即时应用(真实档 fullscreen 态会污染抓帧尺寸〔shot_probe 同款口径〕) main.settings_store.apply() for i in 8: await get_tree().process_frame # UI 装配完成等足帧(canvas_item 空引用窗口) main._start_adventure_from_menu() # 函数直调中间态(卡② mn10——开始冒险钮已撤) # 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询) for i in 60: await get_tree().process_frame if main.level_screen != null and main.level_screen.visible: break if main.level_screen == null or not main.level_screen.visible: _fail("关卡画面未出现(函数直调中间态路径)") return for i in 5: await get_tree().process_frame main.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame var st = main.run_scope.settlement if st == null: _fail("首战未开(settlement 空)") return var rules = main.run_scope.settlement.minesweeper var board = rules.current_board() # 自有驱动(只挖/走——测试驱动可用板面数据挑安全格;新手策略档的封印拒绝会耗尽预算):挖到 3 格停 var dug := 0 var guard := 40 while dug < 3 and guard > 0: guard -= 1 var target = null for n in board.neighbors8(rules.hero_pos()): if not rules.is_revealed(n) and not board.is_mine(n): target = n break if target != null: var out: Array = rules.settle_batch([{"action": "dig", "cell": target}]) if out.is_empty() or not bool(out[0].get("accepted", false)): continue dug += 1 await get_tree().create_timer(T_DIG_FLIGHT).timeout await _snap("fx_dig%d" % dug) # 行到达/弹道在飞窗口 await get_tree().create_timer(T_SETTLE).timeout await _snap("fx_settle%d" % dug) # 结算排播窗口 continue # 无相邻可挖 → 走向有未开安全格邻居的已揭开格(挖掘期移动免费) var dest = null for c in rules.revealed_cells(): if c == rules.hero_pos() or rules.compute_move_path(c).is_empty(): continue for n in board.neighbors8(c): if not rules.is_revealed(n) and not board.is_mine(n): dest = c break if dest != null: break if dest == null: break rules.settle_batch([{"action": "move", "cell": dest}]) # 停手 → 轮末结算(敌方攻击弹道/受击闪白/结算排播在 0.2-1.6s 窗口流过)——连抓 6 帧 rules.settle_batch([{"action": "stop"}]) for i in 6: await get_tree().create_timer(T_ROUND_TICK).timeout await _snap("fx_round%d" % (i + 1)) if dug == 0: _fail("无安全格可挖(首板全雷不可信)") return await get_tree().create_timer(T_FINAL).timeout await _snap("fx_final") if _problems.is_empty(): print("[fx_shot_probe] PASS — 演出帧 %d 张落 %s(挖格 %d)" % [dug * 2 + 1, out_dir, dug]) get_tree().quit(0) else: for p in _problems: push_error("[fx_shot_probe] FAIL — " + p) get_tree().quit(1) func _snap(tag: String) -> void: await RenderingServer.frame_post_draw var img: Image = get_viewport().get_texture().get_image() if img == null or img.is_empty(): _problems.append("抓帧空纹理: " + tag) return img.save_png("%s/%s.png" % [out_dir, tag]) func _fail(msg: String) -> void: _problems.append(msg) push_error("[fx_shot_probe] FAIL — " + msg) get_tree().quit(1)