diff --git a/game/smoke/shot_probe.gd b/game/smoke/shot_probe.gd index b6c73070..eed87371 100644 --- a/game/smoke/shot_probe.gd +++ b/game/smoke/shot_probe.gd @@ -36,7 +36,9 @@ func _ready() -> void: add_child(main) await get_tree().process_frame # ── 存档/元进度/设置三面隔离(ui_click_probe 同款——真档零污染;历史语义 = contains 寻址) ── - ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, false) + # 迭代 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,恢复本探针设计前提。探针侧环境修正,零运行时改动) @@ -44,8 +46,40 @@ func _ready() -> void: await get_tree().process_frame # ① 菜单(启动态)——meta 锚经 S1 裁后换代(2026-10-08 用户裁:副标题锚改认现行文案; # 帮助文腿整个退役——载体玩法速览牌 2026-10-06 用户令退役,checker 对应断言段同撤) - _meta["menu_sub"] = ProbeUiUtil.label_rect(main._start_overlay, "扫雷的因") - _meta["menu_title"] = ProbeUiUtil.label_rect(main._start_overlay, "Dungeon Sweeper") + # 自检一(迭代 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") diff --git a/game/tests/golden/menu_boot.png b/game/tests/golden/menu_boot.png new file mode 100644 index 00000000..90b95cd9 Binary files /dev/null and b/game/tests/golden/menu_boot.png differ diff --git a/tools/golden_baseline_refresh.sh b/tools/golden_baseline_refresh.sh index 0d61f280..17d1b189 100644 --- a/tools/golden_baseline_refresh.sh +++ b/tools/golden_baseline_refresh.sh @@ -1,20 +1,54 @@ #!/usr/bin/env bash -# 棋盘 golden 基线换代辅助(迭代 19 卡① T2 2026-10-08——呈现类改动批专用)。 +# golden 基线换代辅助(迭代 19 卡① T2 2026-10-08——呈现类改动批专用;迭代 28 T4 扩菜单全屏基线路径)。 # 权限口径(Q6):呈现类改动批内 agent 自行换代+提交说明**强制写明触发原因**;数值/逻辑批禁碰。 -# 前置条件(Q5/S3):素材必须齐盘(字体/棋盘瓦片/英雄/旗纹理——本脚本逐项守卫, -# 缺位即拒跑退出 1=基线禁锚字形回退态;缺位时先拷主区素材+导入边车再跑)。 -# 流程:窗口化 board_shot_probe 抓 L1-L3 整屏帧+board_meta.json → golden_board_crop.py -# 按 meta 取样坐标裁棋盘区(禁硬编码像素)→ 覆盖 game/tests/golden/ 三张基线。 -# 过程截图落 mktemp 临时区即弃(Q2),只有裁剪产物入库。 -# 用法: tools/golden_baseline_refresh.sh [game目录] 触发原因写进当批提交说明(强制)。 +# 前置条件(Q5/S3):素材必须齐盘(本脚本逐项守卫,缺位即拒跑退出 1=基线禁锚回退态; +# 缺位时先拷主区素材+导入边车再跑)。 +# 模式: +# --menu(迭代 28 T4 新增):shot_probe 启动态菜单整屏帧 → game/tests/golden/menu_boot.png。 +# 全屏基线可行性前置已验证:菜单连续帧差 0.0000%/max_diff=0(2026-10-15 帧差诊断——光柱为 +# 程序生成静态贴片、零动画着色器,全屏基线不抖动);采集确定性=探针隔离档(无断点+出厂 +# 设置 1280×800),与 render_gate 的 shot_menu 对比段同源同帧。 +# 缺省(棋盘,原口径):board_shot_probe 抓 L1-L3 整屏帧+board_meta.json → golden_board_crop.py +# 按 meta 取样坐标裁棋盘区(禁硬编码像素)→ 覆盖 game/tests/golden/ 三张基线。 +# 过程截图落 mktemp 临时区即弃(Q2),只有基线产物入库。 +# 用法: tools/golden_baseline_refresh.sh [game目录] [--menu] 触发原因写进当批提交说明(强制)。 set -u G="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" REPO="$(cd "$(dirname "$0")/.." && pwd)" -GAME="${1:-$REPO/game}" +GAME="" +MODE="board" +for a in "$@"; do + case "$a" in + --menu) MODE="menu" ;; + *) if [ -z "$GAME" ]; then GAME="$a"; fi ;; + esac +done +GAME="${GAME:-$REPO/game}" GAME_WIN="$(cygpath -m "$GAME")" TMP="$(mktemp -d /tmp/goldenrefresh_XXXXXX)" trap 'rm -rf "$TMP"' EXIT +if [ "$MODE" = "menu" ]; then + for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ + assets/art/ui/button_stone.png; do + if [ ! -f "$GAME/$a" ]; then + echo "[golden_refresh] 拒跑 — 菜单素材缺位: $a(S3 停机点口径:基线禁锚回退态,先补素材+导入边车)" + exit 1 + fi + done + timeout 240 "$G" --path "$GAME_WIN" res://smoke/shot_probe.tscn -- "$TMP/shots" >"$TMP/run.log" 2>&1 + st=$? + grep -E "\[shot_probe\]" "$TMP/run.log" | tail -3 + if [ "$st" -ne 0 ] || [ ! -s "$TMP/shots/shot_menu.png" ]; then + echo "[golden_refresh] FAIL — shot_probe 退出码=$st(菜单截图缺失/探针自检红,见上)" + exit 1 + fi + mkdir -p "$REPO/game/tests/golden" + cp "$TMP/shots/shot_menu.png" "$REPO/game/tests/golden/menu_boot.png" || exit 1 + echo "[golden_refresh] 完成 — 菜单全屏基线已代入 game/tests/golden/menu_boot.png(提交说明须写明触发原因)" + exit 0 +fi + for a in assets/fonts/ui_font_fusion_pixel_zh_hans.ttf \ assets/art/board/flag_seal.png \ assets/art/character/character_hero_idle.png \ diff --git a/tools/render_gate.sh b/tools/render_gate.sh index b65f3379..06a3d47d 100644 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -135,6 +135,23 @@ else fi fi +echo "======== 菜单 golden 逐像素(迭代 28 T4——启动态全屏基线)========" +if [ ! -s "$REPO/game/tests/golden/menu_boot.png" ]; then + echo "[render_gate] FAIL — game/tests/golden/menu_boot.png 基线缺失(菜单 golden 未建——tools/golden_baseline_refresh.sh --menu)"; _rc=1 +elif [ ! -s "$SHOT/shot_menu.png" ]; then + echo "[render_gate] SKIP — shot_menu.png 缺失(shot_probe 段已红),菜单 golden 不判" +elif [ ! -f "$GAME/assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" ] || [ ! -f "$GAME/assets/art/ui/button_stone.png" ]; then + echo "[render_gate] SKIP — 菜单素材缺位(字体/按钮石纹——Q5 口径:基线禁锚回退态,运行时 SKIP)" +else + MENUG="$TMP/menugolden" + mkdir -p "$MENUG" + cp "$SHOT/shot_menu.png" "$MENUG/menu_boot.png" + python tools/golden_compare.py "$REPO/game/tests/golden" "$MENUG" menu_boot.png | sed 's/^/[render_gate] /' + if [ "${PIPESTATUS[0]}" -ne 0 ]; then + echo "[render_gate] FAIL — 菜单 golden 超容差(S2 停机点口径:核对热区——按钮换墨/布局变更属预期面=显式重制基线+呈报;超预期面=L3)"; _rc=1 + fi +fi + echo "======== 残留检查(批末闸) ========" left="$(orphans | wc -l | tr -d ' ')" total="$(tasklist 2>/dev/null | grep -ci '^Godot' || true)" diff --git a/tools/shot_checker.py b/tools/shot_checker.py index 042d7666..f50577e9 100644 --- a/tools/shot_checker.py +++ b/tools/shot_checker.py @@ -13,6 +13,9 @@ r"""shot_checker.py — 整屏样张闸机检(迭代 9 卡⑤/T8 2026-09-30; C 明度三档带: 战斗屏 信息面(C 区面板均值) > 世界中(棋盘区均值) > 世界底(舞台无光柱区均值) (§2 明度分层——层间灰度可分); D 文字禁压纹理: 各文字框背景主簇纯度 ≥0.5(±12 灰度带占比——文字下垫平滑容器非 busy 纹理); + F 文字在判据: 菜单文字框内与中位灰度差 >60 的像素占比在 [2%, 55%] 带内(极性无关——亮字暗底/ + 暗字亮底同判;对比度只判反差不判「字在」,被埋菜单坐标处有反差即过线的缺口由本判据补, + provisional 阈值由 2026-10-15 实测定); E 焦点预算〔首焦唯一〕: 语义面无法机检 → 编入段闸实机目检清单(T15 人闸判定)。 退出码: 0 = 全过 / 1 = 有失败。 """ @@ -77,6 +80,32 @@ def check_text_box(img: Image.Image, box: tuple, name: str, failures: list) -> N failures.append("%s: 背景纹理幅度 %.1f > 18(文字压 busy 纹理)" % (name, mid_std)) +def check_text_presence(img: Image.Image, box: tuple, name: str, failures: list, + lo: float = 0.02, hi: float = 0.55, dev: int = 60) -> None: + """文字在判据(迭代 28 T4——对比度只判反差不判「字在」)。 + + 框内与中位灰度差 >dev 的像素占比须落在 [lo, hi] 带内:极性无关(亮字暗底/暗字亮底同判), + 文字簇量级带(实测菜单标题 13.1%/副标题 11.5%——2026-10-15 窗口化实测定 provisional)。 + 出带=疑似空框/纯色块/文字被移出框(结构性移位)。 + """ + if not box: + failures.append("%s: meta 几何缺失" % name) + return + px = sorted(box_pixels(img, box)) + n = len(px) + if n == 0: + failures.append("%s: 空框(零像素)" % name) + return + med = px[n // 2] + ratio = sum(1 for v in px if abs(v - med) > dev) / n + ok = lo <= ratio <= hi + print("[shot_checker] %s — 文字像素占比=%.2f%%(带 [%.0f%%, %.0f%%] 极性无关)%s" % ( + name, ratio * 100, lo * 100, hi * 100, "I-OK" if ok else "I-FAIL")) + if not ok: + failures.append("%s: 文字像素占比 %.2f%% 出带 [%.0f%%, %.0f%%](字在判据——疑似空框/纯色/文字移位)" % ( + name, ratio * 100, lo * 100, hi * 100)) + + def main() -> int: if len(sys.argv) < 2: print("用法: python tools/shot_checker.py <截图目录> [屏清单...]") @@ -132,6 +161,8 @@ def main() -> int: if "shot_menu.png" in imgs: m = imgs["shot_menu.png"] check_text_box(m, box_of(meta, "menu_sub"), "菜单·副标题", failures) + check_text_presence(m, box_of(meta, "menu_title"), "菜单·标题", failures) + check_text_presence(m, box_of(meta, "menu_sub"), "菜单·副标题", failures) # 菜单·帮助文断言腿退役(迭代 19 卡① S1 裁 2026-10-08——载体玩法速览牌 2026-10-06 用户令 # 退役「界面禁玩法文字墙」,锚/取样键/断言段三处同撤,不重锚) print("[shot_checker] 焦点预算〔首焦唯一〕= 目检项(T15 段闸人闸清单判定——语义面不机检)")