From 65e9016f7fe456d9585280f201c6230f3fc5154a Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 30 Sep 2026 06:11:01 +0800 Subject: [PATCH] =?UTF-8?q?iter9-T7:=20=E5=8D=A1=E2=91=B0a=20B=20=E6=A1=A3?= =?UTF-8?q?=E8=A7=86=E5=8F=A3=E6=88=AA=E5=9B=BE=E5=9F=BA=E5=BB=BA=E8=AF=95?= =?UTF-8?q?=E7=82=B9=EF=BC=88=E6=B5=8B=E8=AF=95=E5=88=86=E7=BA=A7=E6=89=B9?= =?UTF-8?q?=E5=90=8C=E7=AA=97=E4=BB=B6=EF=BC=89=E2=80=94=E2=80=94shot=5Fpr?= =?UTF-8?q?obe=20=E7=AA=97=E5=8F=A3=E5=8C=96=E5=9B=9B=E5=B1=8F=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E6=8E=A2=E9=92=88=EF=BC=88=E8=8F=9C=E5=8D=95/?= =?UTF-8?q?=E8=90=A5=E5=9C=B0/=E5=85=B3=E5=8D=A1/=E6=88=98=E6=96=97?= =?UTF-8?q?=EF=BC=8C=E7=9C=9F=E5=AE=9E=E6=8C=89=E9=92=AE=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=A9=B1=E5=8A=A8+=E5=AD=98=E6=A1=A3/?= =?UTF-8?q?=E5=85=83=E8=BF=9B=E5=BA=A6/=E8=AE=BE=E7=BD=AE=E4=B8=89?= =?UTF-8?q?=E9=9D=A2=E9=9A=94=E7=A6=BB=E6=A1=A3=EF=BC=8C=E5=8F=8C=E5=B8=A7?= =?UTF-8?q?+frame=5Fpost=5Fdraw=20=E5=90=8C=E6=AD=A5=E6=8A=93=E8=A7=86?= =?UTF-8?q?=E5=8F=A3=E7=BA=B9=E7=90=86=EF=BC=89+tools/shot=5Fchecker.py=20?= =?UTF-8?q?B=20=E6=A1=A3=E6=96=AD=E8=A8=80=E9=AA=A8=E6=9E=B6=EF=BC=88?= =?UTF-8?q?=E5=B0=BA=E5=AF=B8=201280=C3=97800/=E7=81=B0=E5=BA=A6=20std?= =?UTF-8?q?=E2=89=A58/=E5=94=AF=E4=B8=80=E8=89=B2=E6=95=B0=E2=89=A564?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E5=AE=9E=E6=B5=8B=E5=9B=9B=E5=B1=8F=E5=85=A8?= =?UTF-8?q?=E8=BF=87=EF=BC=88std=2018.6-64.9/=E8=89=B2=E6=95=B0=20495-1745?= =?UTF-8?q?=EF=BC=89=E8=90=BD=20artwork/qa-screens-20260930=EF=BC=88?= =?UTF-8?q?=E4=B8=8D=E5=85=A5=20git=EF=BC=89=EF=BC=9B=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E9=A1=BB=E7=AA=97=E5=8F=A3=E5=8C=96=E8=BF=90=E8=A1=8C=EF=BC=88?= =?UTF-8?q?headless=20=E6=97=A0=E6=B8=B2=E6=9F=93=E9=9D=A2=EF=BC=89?= =?UTF-8?q?=EF=BC=8CT8=20=E6=95=B4=E5=B1=8F=E6=A0=B7=E5=BC=A0=E9=97=B8?= =?UTF-8?q?=E5=9B=9B=E6=96=AD=E8=A8=80=E5=9C=A8=E6=AD=A4=E9=AA=A8=E6=9E=B6?= =?UTF-8?q?=E4=B8=8A=E6=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/smoke/shot_probe.gd | 100 +++++++++++++++++++++++++++++++++++++ game/smoke/shot_probe.tscn | 6 +++ tools/shot_checker.py | 59 ++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 game/smoke/shot_probe.gd create mode 100644 game/smoke/shot_probe.tscn create mode 100644 tools/shot_checker.py diff --git a/game/smoke/shot_probe.gd b/game/smoke/shot_probe.gd new file mode 100644 index 00000000..42b80f3f --- /dev/null +++ b/game/smoke/shot_probe.gd @@ -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 diff --git a/game/smoke/shot_probe.tscn b/game/smoke/shot_probe.tscn new file mode 100644 index 00000000..bec4d4e3 --- /dev/null +++ b/game/smoke/shot_probe.tscn @@ -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") diff --git a/tools/shot_checker.py b/tools/shot_checker.py new file mode 100644 index 00000000..f49d35c4 --- /dev/null +++ b/tools/shot_checker.py @@ -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())