Files
gplan-2/game/smoke/shot_probe.gd
T
DKChan 67342c690e feat(迭代28 T4): 渲染面内容断言三件(探针自检+墨色判据+启动态菜单 golden)
①shot_probe.gd 两条自检进 _problems:boot 态顶层可见层=start_overlay(内容层树遍历——屏壳容器不当候选,camp 壳透明实证);菜单标题/副标题 label is_visible_in_tree 且 rect 落屏内;隔离档 refresh_resume 开=菜单截图不依赖真实档断点态。②shot_checker.py 增 F 字在判据:菜单文字框与中位灰度差>60 像素占比带 [2%,55%](极性无关——亮字暗底同判;实测标题 13.07%/副标题 11.47% 定 provisional 阈值;对比度只判反差不判字在的缺口由此补)。③启动态菜单 golden:帧差前置验 0.0000%/max_diff=0(光柱静态贴片零动画);golden_baseline_refresh.sh 扩 --menu 模式(shot_probe 取材→game/tests/golden/menu_boot.png,字体+按钮石纹素材守卫);render_gate.sh 补菜单 golden 对比段(素材缺位 SKIP 同 Q5 口径)。闸=render_gate 全绿(探针问题=0+checker A-D/F 全过+棋盘 golden 0.00%×3+菜单 golden 0.00%)。注:本机 render_gate 需 gamedev venv Python 前置 PATH(闸机脚本调 python(PIL),本机默认 python 无 PIL——用户侧环境自带)
2026-10-15 07:28:38 +08:00

228 lines
12 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
# 整屏截图探针(呈现层测试分级 B 档基建:视口截图程序化产出;七屏——
# 菜单/设置面/生涯页/营地/关卡画面/战斗/状态面板,正主=tools/render_gate.sh)。
# 必须**窗口化**运行(真渲染设备——headless 无渲染面,get_image 得到空纹理):
# Godot_console.exe --path . res://smoke/shot_probe.tscn -- <输出目录>
# 流程 = 真实按钮信号路径驱动(ui_click_probe 同款;存档/设置/元进度三面换注隔离档——禁碰真实档,
# 坑册「断点写盘随 save_manager 引用走」教训同款覆盖):菜单(启动态)→ 设置面(settingsbtn
# 真实信号路径)→ 生涯页(careerbtn 同款)→ 营地(campbtn)→ 关卡画面(lvbtn→停点出图)→
# 战斗(level_screen.press_candidate(0) 真实选路提交)→ 状态面板(地图屏状态角标真实信号路径)。
# 每屏双帧 + frame_post_draw 同步后抓 get_viewport().get_texture(),PNG 落输出目录;
# 最小自检 = 七件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。
# 商店房不纳屏(真实路径口径):真实路径必经 L1 打穿→L2 商店节点
# 选路(商店层在 L2 Boss 前必经位)——探针现行驱动只进首战节点,低代价确定性
# 到达不成立;合成投喂(ui_struct_probe 同款)破坏本探针真实路径口径 → 不纳屏。
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 寻址) ──
# 迭代 28 T4:refresh_resume 开——续玩钮按隔离档(无断点)定基,菜单截图/golden 基线不依赖
# 真实档断点态(boot 关系由冒烟 _smoke_boot_chain V2d 断,此处只保截图确定性)
ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, true)
# 隔离档即时应用(迭代 19 卡① T1——main 装配已按真实设置恢复过窗口态〔本机实测:真实档
# fullscreen=true 时窗口进全屏,视口纹理 2304x1440≠1280 基准档,checker 尺寸/取样全盘失真〕;
# apply 隔离档出厂默认=窗口化 1280x800,恢复本探针设计前提。探针侧环境修正,零运行时改动)
main.settings_store.apply()
await get_tree().process_frame
# ① 菜单(启动态)——meta 锚经 S1 裁后换代(2026-10-08 用户裁:副标题锚改认现行文案;
# 帮助文腿整个退役——载体玩法速览牌 2026-10-06 用户令退役,checker 对应断言段同撤)
# 自检一(迭代 28 T4):boot 态顶层可见全屏层=start_overlay(内容层树遍历——屏壳是容器不当候选:
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
var top: Node = null
var top_rx := -1
var top_ry := -1
for c: Node in [main._start_overlay, main._battle_root, main._inter_overlay,
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
main.status_panel, main.career_overlay, main.confirm_dialog, main.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._start_overlay:
_problems.append("boot 态顶层可见层≠start_overlay(实测 %s——菜单被埋)〔装配生命批 V4〕"
% (top.name if top != null else "null"))
# 自检二(迭代 28 T4):菜单标题/副标题 label 必须 is_visible_in_tree 且 rect 落屏内
#(对比度只判反差不判「字在」——本条+checker 墨色占比共同补内容判据)
var vp := get_viewport().get_visible_rect()
for pair in [["menu_title", "Dungeon Sweeper"], ["menu_sub", "扫雷的因"]]:
var lbl: Label = ProbeUiUtil.find_label(main._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._start_overlay, String(pair[1]))
await _snap("menu")
# ①b 设置面(迭代 19 卡① T1 扩屏——settingsbtn 真实信号路径,开合同款真实钮)
var setbtn: Button = ProbeUiUtil.find_button(main._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.settings_overlay.visible:
_problems.append("设置面未打开(settingsbtn 路径)")
else:
_meta["settings_title"] = ProbeUiUtil.label_rect(main.settings_overlay, "设置")
await _snap("settings")
var set_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
if set_close == null:
_problems.append("设置面返回钮缺失")
else:
set_close.pressed.emit()
await get_tree().process_frame
# ①c 生涯页(迭代 19 卡① T1 扩屏——careerbtn 真实信号路径,ui_struct_probe 同款入口链)
var careerbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "生涯", "prefix")
if careerbtn == null:
_problems.append("生涯入口缺失(开始层 careerbtn)")
else:
careerbtn.pressed.emit()
for i in 3:
await get_tree().process_frame
if not main.career_overlay.visible or main._start_overlay.visible:
_problems.append("生涯页未打开或开始层未隐藏(careerbtn 路径)")
else:
_meta["career_title"] = ProbeUiUtil.label_rect(main.career_overlay, "生 涯")
await _snap("career")
var career_close: Button = ProbeUiUtil.find_button(main.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.career_overlay.visible or not main._start_overlay.visible:
_problems.append("生涯页关闭后页未隐藏或开始层未恢复")
# ② 营地
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 取样坐标源;明细带锚 S1 裁后改认现行文案)
_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)
# ⑤ 状态面板(迭代 19 卡① T1 扩屏——战斗屏状态角标真实信号路径;层在战斗屏之上开合)
var pb: Button = main.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.status_panel.visible:
_problems.append("状态面板未打开(角标路径)")
else:
_meta["status_title"] = ProbeUiUtil.label_rect(main.status_panel, "状态")
await _snap("status")
var status_close: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
if status_close == null:
_problems.append("状态面板继续钮缺失")
else:
status_close.pressed.emit()
await get_tree().process_frame
if main.status_panel.visible:
_problems.append("状态面板关闭后未隐藏(继续钮路径)")
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()
# ── 最小自检:七件存在且非零字节(迭代 19 卡① T1 扩屏后口径) ──
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}