标识符面:flag_*→seal_ 族(含 Godot 信号 flag_judged→seal_judged 与连接/回调同名、 FACTS 两键 flags_count→seals_count/dug_under_flag→dug_under_seal 全链、 .tres 键名与 schema .gd 同批同步);显式豁免零改动=size_flags_*(Godot 布局属性)+_fatal_flag。 中文面:金币→GP(金币袋→GP 袋)/插旗→封印/雷格→危险格/惩罚格→危险格/旗→封印·封印印记族, game/ 内 *.gd/.tres/.md/.json 四类禁用词与 flag 双双零命中(V1/V4)。 守卫字面量不动(被检查的禁字不是使用):smoke_all V21 零「雷/旗」检查面、 ui_font_asset_test 字形集攻/雷/旗/封 保持原断言逻辑。 代拍板:板面单字槽 glyph「旗」→「封」(禁字纪律+单字位宽,消费面四处同名随动)。 素材换代(2026-10-18 用户裁案甲):assets/art/board/flag_seal.png→seal_mark.png(素材不入 git, 盘上改名+导入重刷),代码两处 res:// 字符串+COL_FLAG/CURSOR_FLAG 常量同步, 闸件四处随批(run_all_gates.sh/render_gate.sh/golden_baseline_refresh.sh/path_check.py)。 anchor.json 键名手改,重锚与改前改后 diff 随本卡收口(同裁案甲)。 基线读数(改前,主区 ed2dfaf6):冒烟 1151 检查 0 失败/单元 34 套全绿/探针 --click 存量红仅 screen_flow_probe(user:// 断点污染在册已知项)。 改后同批读数(本 worktree):冒烟 1151 检查 0 失败/探针 --click 同一存量红、其余全绿。
126 lines
5.2 KiB
GDScript
126 lines
5.2 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 演出渲染证据探针(真实渲染设备抓战斗演出瞬间帧)。
|
||
# 必须**窗口化**运行(真渲染设备——headless 无渲染面):Godot_console.exe --path . res://smoke/fx_shot_probe.tscn -- <输出目录>
|
||
# 流程 = shot_probe 同款隔离面(存档/设置/元进度换注隔离档,禁碰真实档)+ 真实按钮路径开冒险进首战;
|
||
# 自动玩家挖格驱动行结算,在 行到达/弹道飞行/结算排播 三时刻抓帧——演出链「逻辑在(fx_probe PASS)×
|
||
# 渲染在(本探针 PNG)」双证的第二半边。PNG 落输出目录(artwork/qa-fx-*/,素材纪律不入 git)。
|
||
extends Node
|
||
|
||
const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||
|
||
# ── 演出采样窗口口径(拍板点:headless/窗口化帧率不定,帧时刻以墙钟近似锚定;改窗口 = 改抓帧
|
||
# 时刻观测面,非断言语义。窗口值与原字面量逐一同值——T6 收口批仅提具名。) ──
|
||
const T_DIG_FLIGHT := 0.12 # 挖格行到达/弹道在飞窗口(settle_batch 回执后停留)
|
||
const T_SETTLE := 0.35 # 结算排播窗口(行到达后二连抓)
|
||
const T_ROUND_TICK := 0.25 # 停手轮末(敌方攻击弹道/受击闪白 0.2-1.6s 流过)连抓 6 帧的采样步距
|
||
const T_FINAL := 0.6 # 收尾帧前静置
|
||
|
||
var main: Node
|
||
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-fx"
|
||
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
|
||
ProbeUiUtil.isolate_save(main, "fx_shot_probe", false, true, false)
|
||
# 隔离档即时应用(迭代 19 卡① T1——真实档 fullscreen 态会污染抓帧尺寸〔shot_probe 同款修正〕)
|
||
main.settings_store.apply()
|
||
for i in 8:
|
||
await get_tree().process_frame # UI 装配完成等足帧(canvas_item 空引用窗口)
|
||
var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains")
|
||
if lvbtn == null:
|
||
_fail("开始冒险按钮缺失")
|
||
return
|
||
lvbtn.pressed.emit()
|
||
# 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询)
|
||
for i in 60:
|
||
await get_tree().process_frame
|
||
if main.level_screen != null and main.level_screen.visible:
|
||
break
|
||
if main.level_screen == null or not main.level_screen.visible:
|
||
_fail("关卡画面未出现(lvbtn 路径)")
|
||
return
|
||
for i in 5:
|
||
await get_tree().process_frame
|
||
main.level_screen.press_candidate(0)
|
||
for i in 20:
|
||
await get_tree().process_frame
|
||
var st = main.run_scope.settlement
|
||
if st == null:
|
||
_fail("首战未开(settlement 空)")
|
||
return
|
||
var rules = main.run_scope.settlement.minesweeper
|
||
var board = rules.current_board()
|
||
# 自有驱动(只挖/走——测试驱动可用板面数据挑安全格;新手策略档的封印拒绝会耗尽预算):挖到 3 格停
|
||
var dug := 0
|
||
var guard := 40
|
||
while dug < 3 and guard > 0:
|
||
guard -= 1
|
||
var target = null
|
||
for n in board.neighbors8(rules.hero_pos()):
|
||
if not rules.is_revealed(n) and not board.is_mine(n):
|
||
target = n
|
||
break
|
||
if target != null:
|
||
var out: Array = rules.settle_batch([{"action": "dig", "cell": target}])
|
||
if out.is_empty() or not bool(out[0].get("accepted", false)):
|
||
continue
|
||
dug += 1
|
||
await get_tree().create_timer(T_DIG_FLIGHT).timeout
|
||
await _snap("fx_dig%d" % dug) # 行到达/弹道在飞窗口
|
||
await get_tree().create_timer(T_SETTLE).timeout
|
||
await _snap("fx_settle%d" % dug) # 结算排播窗口
|
||
continue
|
||
# 无相邻可挖 → 走向有未开安全格邻居的已揭开格(挖掘期移动免费)
|
||
var dest = null
|
||
for c in rules.revealed_cells():
|
||
if c == rules.hero_pos() or rules.compute_move_path(c).is_empty():
|
||
continue
|
||
for n in board.neighbors8(c):
|
||
if not rules.is_revealed(n) and not board.is_mine(n):
|
||
dest = c
|
||
break
|
||
if dest != null:
|
||
break
|
||
if dest == null:
|
||
break
|
||
rules.settle_batch([{"action": "move", "cell": dest}])
|
||
# 停手 → 轮末结算(敌方攻击弹道/受击闪白/结算排播在 0.2-1.6s 窗口流过)——连抓 6 帧
|
||
rules.settle_batch([{"action": "stop"}])
|
||
for i in 6:
|
||
await get_tree().create_timer(T_ROUND_TICK).timeout
|
||
await _snap("fx_round%d" % (i + 1))
|
||
if dug == 0:
|
||
_fail("无安全格可挖(首板全雷不可信)")
|
||
return
|
||
await get_tree().create_timer(T_FINAL).timeout
|
||
await _snap("fx_final")
|
||
if _problems.is_empty():
|
||
print("[fx_shot_probe] PASS — 演出帧 %d 张落 %s(挖格 %d)" % [dug * 2 + 1, out_dir, dug])
|
||
get_tree().quit(0)
|
||
else:
|
||
for p in _problems:
|
||
push_error("[fx_shot_probe] FAIL — " + p)
|
||
get_tree().quit(1)
|
||
|
||
|
||
func _snap(tag: String) -> void:
|
||
await RenderingServer.frame_post_draw
|
||
var img: Image = get_viewport().get_texture().get_image()
|
||
if img == null or img.is_empty():
|
||
_problems.append("抓帧空纹理: " + tag)
|
||
return
|
||
img.save_png("%s/%s.png" % [out_dir, tag])
|
||
|
||
|
||
func _fail(msg: String) -> void:
|
||
_problems.append(msg)
|
||
push_error("[fx_shot_probe] FAIL — " + msg)
|
||
get_tree().quit(1)
|