iter9-T7: 卡⑰a B 档视口截图基建试点(测试分级批同窗件)——shot_probe 窗口化四屏截图探针(菜单/营地/关卡/战斗,真实按钮信号路径驱动+存档/元进度/设置三面隔离档,双帧+frame_post_draw 同步抓视口纹理)+tools/shot_checker.py B 档断言骨架(尺寸 1280×800/灰度 std≥8/唯一色数≥64);实测四屏全过(std 18.6-64.9/色数 495-1745)落 artwork/qa-screens-20260930(不入 git);探针须窗口化运行(headless 无渲染面),T8 整屏样张闸四断言在此骨架上扩

This commit is contained in:
DKChan committed 2026-09-30 06:11:01 +08:00
1 parent 1ecad0b580
commit 65e9016f7f
3 files changed
+165

No files matched your search

+100
View File
@@ -0,0 +1,100 @@
# 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 = []
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
# ① 菜单(启动态)
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")
# ④ 战斗(节点一——真实选路提交路径 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")
else:
_problems.append("关卡画面未出现(lvbtn 路径)")
# ── 最小自检:四件存在且非零字节 ──
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
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://smoke/shot_probe.gd" id="1"]
[node name="ShotProbe" type="Node"]
script = ExtResource("1")
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
r"""shot_checker.py — 整屏截图 B 档断言骨架(迭代 9 卡⑰a 2026-09-30)。
用法: python tools/shot_checker.py <截图目录> [shot_menu.png shot_camp.png shot_level.png shot_battle.png]
断言(v1 骨架——像素级可判层):
① 四屏 PNG 存在且尺寸 = 1280×800(工程逻辑分辨率档);
② 每屏灰度标准差 ≥ 8(非空白/非纯色——渲染真实发生的最低证据);
③ 每屏唯一色数 ≥ 64(内容丰富度下限——占位纯色屏会落在个位数)。
T8 整屏样张闸四断言(全局文字 ≥4.5:1/明度三档带/焦点预算/文字禁压纹理)在本骨架上扩。
退出码: 0 = 全过 / 1 = 有失败。
"""
import sys
from pathlib import Path
from PIL import Image
EXPECTED = (1280, 800)
def main() -> int:
if len(sys.argv) < 2:
print("用法: python tools/shot_checker.py <截图目录> [屏清单...]")
return 1
d = Path(sys.argv[1])
shots = sys.argv[2:] or ["shot_menu.png", "shot_camp.png", "shot_level.png", "shot_battle.png"]
failures = []
for name in shots:
p = d / name
if not p.exists():
failures.append("%s: 文件缺失" % name)
continue
img = Image.open(p).convert("RGB")
if img.size != EXPECTED:
failures.append("%s: 尺寸 %s != %s" % (name, img.size, EXPECTED))
continue
gray = img.convert("L")
hist = gray.histogram()
total = sum(hist)
mean = sum(i * c for i, c in enumerate(hist)) / total
var = sum(c * (i - mean) ** 2 for i, c in enumerate(hist)) / total
std = var ** 0.5
colors = len({p2 for p2 in img.getdata()})
if std < 8.0:
failures.append("%s: 灰度 std %.1f < 8(疑似空白/纯色)" % (name, std))
if colors < 64:
failures.append("%s: 唯一色数 %d < 64(疑似占位屏)" % (name, colors))
print("[shot_checker] %s — %s std=%.1f colors=%d %s" % (
name, "x".join(map(str, img.size)), std, colors, "OK" if std >= 8 and colors >= 64 else "FAIL"))
if failures:
print("[shot_checker] FAIL——")
for f in failures:
print(" - %s" % f)
return 1
print("[shot_checker] PASS(%d 屏骨架断言全过)" % len(shots))
return 0
if __name__ == "__main__":
sys.exit(main())