Files
gplan-2/game/smoke/shot_probe.gd
T

123 lines
6.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
const ProbeUiUtil := preload("res://smoke/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 调用方必挂死(2026-10-02 挂死灾害批护栏)
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 寻址) ──
ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, false)
await get_tree().process_frame
# ① 菜单(启动态)
_meta["menu_sub"] = ProbeUiUtil.label_rect(main._start_overlay, "改造批")
_meta["menu_help"] = ProbeUiUtil.label_rect(main._start_overlay, "玩法:")
_meta["menu_title"] = ProbeUiUtil.label_rect(main._start_overlay, "Dungeon Sweeper")
await _snap("menu")
# ② 营地
var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "contains")
if campbtn == null:
_problems.append("营地入口缺失(开始层 campbtn)")
else:
campbtn.pressed.emit()
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
# ③ 关卡画面(开始冒险→停点出图——冒险开始即出图,停点前移批语义)
var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains")
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 = 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 取样坐标源)
_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 真机测量(迭代 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 _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}