95 lines
4.1 KiB
GDScript
95 lines
4.1 KiB
GDScript
# level_map_shot_probe.gd — 关卡画面视口 golden 采集探针(卷轴视口批 golden 扩项)
|
||
# 两 Shot 面(2026-11-05 用户裁:只对拍关卡最开始与关卡 Boss 节点,中间过程不对拍):
|
||
# lmap_entry.png = 入口停点(关卡最开始——锚点=入口组包围盒中心夹紧)
|
||
# lmap_boss.png = 中段停点锚定末列(Boss 紫赤描边列入屏——viewport_center 夹紧右缘)
|
||
# 采集确定性 = 固定种子单关直构(LevelRunner.start_level_external defer_entry / resume_state,
|
||
# 零 RNG 消耗路径)+ 探针隔离档(无断点、出厂设置 1280×800);运行=窗口化(--headless 禁——
|
||
# 渲染闸机器面吃真实渲染帧)。截图经 user args 目录落盘,基线换代走 golden_baseline_refresh.sh --lmap。
|
||
# 运行:Godot_console.exe --path game res://probe/level_map_shot_probe.tscn -- <shots_dir>
|
||
extends Node
|
||
|
||
const SEED_L3 := 910212 # 深图固定种子(与 level_map_viewport_probe 同源——图确定性承载)
|
||
|
||
func _ready() -> void:
|
||
# oracle: design/engineering/test-conventions.md:84 (关卡画面截图取证——载入守卫组)
|
||
var args := OS.get_cmdline_user_args()
|
||
var out_dir := args[0] if args.size() > 0 else "user://"
|
||
await get_tree().process_frame
|
||
var host := Control.new()
|
||
add_child(host)
|
||
host.size = get_viewport().get_visible_rect().size
|
||
var screen = load("res://presentation/views/level_map_screen.gd").new()
|
||
screen.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||
host.add_child(screen)
|
||
for i in range(6):
|
||
await get_tree().process_frame # 布局收敛(视口实况尺寸落定→resize 重锚)
|
||
# Shot①:入口停点(关卡最开始)
|
||
var runner = _make_runner(true, -1)
|
||
screen.open_choices(_cand_nodes(runner), runner)
|
||
await _settle()
|
||
_shot(out_dir, "lmap_entry.png")
|
||
# Shot②:中段停点锚定末列(布局 x 最深的有出边节点——id 不按深度排,锚定后 Boss 列入屏)
|
||
var boss_stop := _deepest_with_outs(screen, runner)
|
||
runner = _make_runner(false, boss_stop)
|
||
screen.open_choices(_cand_nodes(runner), runner)
|
||
await _settle()
|
||
_shot(out_dir, "lmap_boss.png")
|
||
print("[level_map_shot_probe] PASS — 两 Shot 落盘(entry+boss → %s)" % out_dir)
|
||
get_tree().quit(0)
|
||
|
||
|
||
func _settle() -> void:
|
||
for i in range(8):
|
||
await get_tree().process_frame
|
||
await get_tree().create_timer(0.5).timeout # 平滑滚动终态余量(reanchor tween 0.3s)
|
||
|
||
|
||
func _make_runner(entry: bool, node_id: int):
|
||
# oracle: design/engineering/test-conventions.md:84 (判定上报通道——验证函数组)
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.new_adventure(SEED_L3)
|
||
var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng)
|
||
var ok: bool
|
||
if entry:
|
||
ok = runner.start_level_external(3, Callable(runner, "rest_heal_amount"), true)
|
||
else:
|
||
ok = runner.start_level_external(3, Callable(runner, "rest_heal_amount"),
|
||
false, {"node_id": node_id, "visited_ids": [node_id]})
|
||
if not ok:
|
||
push_error("[level_map_shot_probe] 状态注入失败(entry=%s node=%d)" % [str(entry), node_id])
|
||
get_tree().quit(1)
|
||
return runner
|
||
|
||
|
||
func _cand_nodes(runner) -> Array:
|
||
var ids: Array = runner.next_candidate_ids()
|
||
var out: Array = []
|
||
for node in runner.nodes():
|
||
if ids.has(runner.node_id_of(node)):
|
||
out.append(node)
|
||
return out
|
||
|
||
|
||
func _deepest_with_outs(screen, runner) -> int:
|
||
var pos: Dictionary = screen.layout_probe(runner.nodes())
|
||
var best := -1
|
||
var best_x := -INF
|
||
for node in runner.nodes():
|
||
if runner.node_out_ids(node).is_empty():
|
||
continue
|
||
var x: float = pos[node].x
|
||
if x > best_x:
|
||
best_x = x
|
||
best = runner.node_id_of(node)
|
||
return best
|
||
|
||
|
||
func _shot(out_dir: String, fname: String) -> void:
|
||
# oracle: design/engineering/test-conventions.md:84 (判定上报通道——验证函数组)
|
||
DirAccess.make_dir_recursive_absolute(out_dir) # 落盘目录自建(save_png 对缺目录静默失败——刷新链不预建)
|
||
var img := get_viewport().get_texture().get_image()
|
||
var err := img.save_png(out_dir.path_join(fname))
|
||
if err != OK:
|
||
push_error("[level_map_shot_probe] 截图落盘失败 %s(err=%d)" % [fname, err])
|
||
get_tree().quit(1)
|