# 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 -- 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)