iter53-卡1:卷轴视口批: 关卡画面视口化(level-map.md 布局决策5落地)——视口承载件/锚点跟随/平滑滚动减动效/拖拽滚轮平移与clamp/指引件/探针26断言/golden两Shot冻结
This commit is contained in:
1 parent
fd9c8ad90e
commit
55f4de3ca4
22 files changed
+983
-152
No files matched your search
@@ -0,0 +1,91 @@
|
||||
# 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:
|
||||
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):
|
||||
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:
|
||||
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)
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://probe/level_map_shot_probe.gd" id="1"]
|
||||
|
||||
[node name="LevelMapShotProbe" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -0,0 +1,389 @@
|
||||
# level_map_viewport_probe.gd — 卷轴视口行为探针(卷轴视口批 R1–R8 验收载体)
|
||||
# 判的是 level-map.md 布局决策 5 的视口行为四条:浅图整幅居中无裁切(R1)、停点切换/兑付回图
|
||||
# 当前节点居中锚定——平滑滚动与减动效瞬切(R2/R7)、拖拽/滚轮手动平移与 clamp 包络——
|
||||
# 首末列可达(R3/R4/R5)、拖拽不触发选路提交(R6)、指引件可见性与几何(R8,像素级判据②③)。
|
||||
# 真实入口 = 合成拖拽沿/滚轮沿/点击沿经 push_input 进引擎输入管线打在真实画布 _gui_input 上
|
||||
# (probe_ui_util.push_drag_at/push_wheel_at/push_click_at);状态注入 = 直调核心层写口构造
|
||||
# 单关停点(LevelRunner.start_level_external——defer_entry=入口停点 / resume_state=断点续玩
|
||||
# 重建到任意停点,零 RNG 消耗;test-conventions §5.4 自动化测试直构口径),零 main 冒险流程
|
||||
# 依赖:真实屏 = LevelMapScreen 实例 + 真实单关 runner。
|
||||
# 运行:Godot_console.exe --headless --path . res://probe/level_map_viewport_probe.tscn
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
const ViewportScript := preload("res://presentation/views/level_map_viewport.gd")
|
||||
|
||||
const SEED_L1 := 910211 # 浅图停点种子(L1 单链——画布 846.5×256 全幅入窗)
|
||||
const SEED_L3 := 910212 # 深图停点种子(L3 图——画布超 1280 窗宽,滚动视口生效面)
|
||||
|
||||
var _problems: Array[String] = []
|
||||
var _checks := 0
|
||||
var _runner # 当前停点的单关 runner(open_choices 驱动随记——查证面共用)
|
||||
|
||||
|
||||
func _check(desc: String, ok: bool) -> void:
|
||||
_checks += 1
|
||||
if not ok:
|
||||
_problems.append(desc)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var host := Control.new()
|
||||
add_child(host)
|
||||
host.size = get_viewport().get_visible_rect().size # 显式置底座(父非 Control 锚点不解析)
|
||||
var screen = load("res://presentation/views/level_map_screen.gd").new()
|
||||
screen.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
host.add_child(screen)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame # 布局收敛先行(视口实况窗口尺寸就位后再开停点——锚点数学以实况窗口为准)
|
||||
|
||||
var vp = _find_viewport(screen)
|
||||
if vp == null:
|
||||
_problems.append("卷轴视口承载件未挂入关卡屏(LevelMapViewport 实例缺位)")
|
||||
_finish()
|
||||
return
|
||||
|
||||
await _probe_shallow_no_crop(screen, vp)
|
||||
await _probe_deep_entry_clamp(screen, vp)
|
||||
await _probe_wheel_pan(screen, vp)
|
||||
await _probe_drag_pan(screen, vp)
|
||||
await _probe_clamp_bounds_and_columns(screen, vp)
|
||||
await _probe_drag_no_submit(screen, vp)
|
||||
await _probe_follow_anchor(screen, vp)
|
||||
await _probe_smooth_scroll_reduced_motion(screen, vp)
|
||||
await _probe_guidance_chrome(screen, vp)
|
||||
await _probe_shallow_guidance_hidden(screen, vp)
|
||||
_finish()
|
||||
|
||||
|
||||
## R1 浅图整幅居中无裁切+画布入视口件承载(画布=视口首子件——setup 注入序)
|
||||
func _probe_shallow_no_crop(screen, vp) -> void:
|
||||
_runner = _make_runner_entry(1, SEED_L1)
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var canvas: Control = vp.get_child(0) as Control
|
||||
if canvas == null:
|
||||
_problems.append("R1 画布实例缺位(视口首子件非 Control)")
|
||||
return
|
||||
_check("R1 浅图画布全幅入窗(canvas=%s window=%s)〔卷轴批 V1〕" % [canvas.size, vp.size],
|
||||
canvas.size.x <= vp.size.x + 0.5 and canvas.size.y <= vp.size.y + 0.5)
|
||||
var want: Vector2 = (vp.size - canvas.size) / 2.0
|
||||
_check("R1 居中分支 offset 定位(want=%s got=%s)〔卷轴批 V1〕" % [want, canvas.position],
|
||||
canvas.position.distance_to(want) <= 0.5)
|
||||
_check("R1 画布矩形在窗内无裁切〔卷轴批 V1〕", vp.get_global_rect().encloses(canvas.get_global_rect()))
|
||||
|
||||
|
||||
## R5 前置+深图入口停点:锚点=入口组包围盒中心夹紧(spec :101——多入口并列取包围盒居中)
|
||||
func _probe_deep_entry_clamp(screen, vp) -> void:
|
||||
_runner = _make_runner_entry(3, SEED_L3)
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var center: Vector2 = vp.viewport_center_canvas_pos()
|
||||
var half: float = vp.size.x / 2.0
|
||||
var canvas_len: float = _canvas_len(screen)
|
||||
_check("R5 入口锚定后视口中线在夹紧域内(center.x=%.1f ∈ [%.1f, %.1f])〔卷轴批 V5〕" %
|
||||
[center.x, half, canvas_len - half],
|
||||
center.x >= half - 0.5 and center.x <= canvas_len - half + 0.5)
|
||||
# 入口停点候选 = 入口节点——包围盒中心夹紧即锚点(本档图入口分布跨图,中心落夹紧域)
|
||||
var nodes: Array = _runner.nodes()
|
||||
var pos: Dictionary = screen.layout_probe(nodes)
|
||||
var ids: Array = _runner.next_candidate_ids()
|
||||
var lo := INF
|
||||
var hi := -INF
|
||||
for node in nodes:
|
||||
if ids.has(_runner.node_id_of(node)):
|
||||
lo = minf(lo, pos[node].x)
|
||||
hi = maxf(hi, pos[node].x)
|
||||
var want: float = clampf((lo + hi) / 2.0, half, canvas_len - half)
|
||||
_check("R5 入口组包围盒中心=锚点(bbox=%.1f center=%.1f want=%.1f)〔卷轴批 V5〕" %
|
||||
[(lo + hi) / 2.0, center.x, want], absf(center.x - want) <= 0.5)
|
||||
|
||||
|
||||
## R4 滚轮横滚移动视口(下=向右前进向——代拍板映射;窗内平移不钉边界,与 R5 两极 true 构成翻转)
|
||||
func _probe_wheel_pan(screen, vp) -> void:
|
||||
var before: float = vp.viewport_center_canvas_pos().x
|
||||
var pos: Vector2 = vp.get_global_rect().get_center()
|
||||
ProbeUiUtil.push_wheel_at(self, pos, true)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var after: float = vp.viewport_center_canvas_pos().x
|
||||
_check("R4 滚轮下单档平移 +60(before=%.1f after=%.1f)〔卷轴批 V4〕" % [before, after],
|
||||
absf(after - before - 60.0) <= 0.5)
|
||||
_check("R4 窗内平移不钉边界(is_clamped_at_edge=false)〔卷轴批 V4〕", not vp.is_clamped_at_edge())
|
||||
ProbeUiUtil.push_wheel_at(self, pos, false)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
_check("R4 滚轮上回移 −60〔卷轴批 V4〕",
|
||||
absf(vp.viewport_center_canvas_pos().x - before) <= 0.5)
|
||||
|
||||
|
||||
## R3 拖拽平移移动视口(窗内位移量=拖拽位移,无 clamp 干扰)
|
||||
func _probe_drag_pan(screen, vp) -> void:
|
||||
# 先滚入窗内区(两档 +120——避开左包络边界的 clamp 吸收),再拖拽核位移
|
||||
var pos: Vector2 = vp.get_global_rect().get_center()
|
||||
ProbeUiUtil.push_wheel_at(self, pos, true)
|
||||
ProbeUiUtil.push_wheel_at(self, pos, true)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var before: float = vp.viewport_center_canvas_pos().x
|
||||
ProbeUiUtil.push_drag_at(self, pos, Vector2(-40, 0))
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var after: float = vp.viewport_center_canvas_pos().x
|
||||
_check("R3 拖拽左移 −40 平移量逐位一致(画布随手——视线 +40,before=%.1f after=%.1f)〔卷轴批 V3〕" % [before, after],
|
||||
absf(after - before - 40.0) <= 0.5)
|
||||
# 复位到左包络边界,给后续探针稳定起点
|
||||
ProbeUiUtil.push_drag_at(self, pos, Vector2(2000, 0))
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
|
||||
|
||||
## R5 clamp 边界与首末列可达(拖拽两极)
|
||||
func _probe_clamp_bounds_and_columns(screen, vp) -> void:
|
||||
var nodes: Array = _runner.nodes()
|
||||
var pos: Dictionary = screen.layout_probe(nodes)
|
||||
var first_node := _extreme_node(pos, nodes, true)
|
||||
var last_node := _extreme_node(pos, nodes, false)
|
||||
var vp_rect: Rect2 = vp.get_global_rect()
|
||||
# 右极:拖拽超包络 → 视口钉左缘,首列(深度 0)入屏
|
||||
ProbeUiUtil.push_drag_at(self, vp_rect.get_center(), Vector2(2000, 0))
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var center: float = vp.viewport_center_canvas_pos().x
|
||||
var half: float = vp.size.x / 2.0
|
||||
_check("R5 拖拽超包络收敛夹紧域(center.x=%.1f)〔卷轴批 V5〕" % center,
|
||||
center >= half - 0.5 and center <= _canvas_len(screen) - half + 0.5)
|
||||
_check("R5 右极视口钉边界(is_clamped_at_edge=true)〔卷轴批 V5〕", vp.is_clamped_at_edge())
|
||||
_check("R5 首列(深度 0)中心入屏〔卷轴批 V5〕",
|
||||
vp_rect.has_point(_node_screen_pos(screen, pos, first_node)))
|
||||
# 左极:反向超包络 → 视口钉右缘,末列(含 Boss 紫赤描边列)入屏
|
||||
ProbeUiUtil.push_drag_at(self, vp_rect.get_center(), Vector2(-2000, 0))
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
_check("R5 左极视口钉边界(is_clamped_at_edge=true)〔卷轴批 V5〕", vp.is_clamped_at_edge())
|
||||
_check("R5 末列中心入屏(拖拽两极可达——布局决策 5)〔卷轴批 V5〕",
|
||||
vp_rect.has_point(_node_screen_pos(screen, pos, last_node)))
|
||||
|
||||
|
||||
## R6 拖拽不触发选路提交;阈值另一侧完整点击仍提交(提交语义零改动)
|
||||
func _probe_drag_no_submit(screen, vp) -> void:
|
||||
# 重建到中段可居中停点(候选=停点出边——候选节点居屏中可拖可达)
|
||||
var stops := _centerable_stops(screen, vp)
|
||||
if stops.size() < 3:
|
||||
_problems.append("R6 可居中停点不足(=%d)" % stops.size())
|
||||
return
|
||||
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[0]))
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var nodes: Array = _runner.nodes()
|
||||
var cands: Array = _runner.next_candidate_ids()
|
||||
var target := {}
|
||||
for node in nodes:
|
||||
if cands.has(_runner.node_id_of(node)):
|
||||
target = node
|
||||
break
|
||||
if target.is_empty():
|
||||
_problems.append("R6 候选节点缺位(候选=%s)" % str(cands))
|
||||
return
|
||||
var gpos: Vector2 = _node_screen_pos(screen, screen.layout_probe(nodes), target)
|
||||
var submitted: Array = []
|
||||
var on_route := func(nid: int) -> void: submitted.append(nid)
|
||||
screen.route_chosen.connect(on_route)
|
||||
ProbeUiUtil.push_drag_at(self, gpos, Vector2(120, 24))
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
_check("R6 拖拽沿起止候选上方零提交(submitted=%s)〔卷轴批 V6〕" % str(submitted), submitted.is_empty())
|
||||
_check("R6 拖拽后本屏未关〔卷轴批 V6〕", screen.visible)
|
||||
var gpos2: Vector2 = _node_screen_pos(screen, screen.layout_probe(nodes), target) # 拖拽平移后重算投影(画布随视口移动)
|
||||
ProbeUiUtil.push_click_at(self, gpos2)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
_check("R6 完整点击(未超阈值)仍提交选路——阈值区分另一侧〔卷轴批 V6〕", submitted.size() == 1)
|
||||
screen.route_chosen.disconnect(on_route)
|
||||
|
||||
|
||||
## R2 停点切换后当前节点居中(视口中线投影 x=640±2——视口满屏宽即屏中线)
|
||||
func _probe_follow_anchor(screen, vp) -> void:
|
||||
var stops := _centerable_stops(screen, vp)
|
||||
if stops.size() < 3:
|
||||
_problems.append("R2 可居中停点不足(=%d)" % stops.size())
|
||||
return
|
||||
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[1]))
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().create_timer(0.6).timeout # 平滑滚动终态(0.3s 单段缓动+余量)
|
||||
var sx: float = _node_screen_pos(screen, screen.layout_probe(_runner.nodes()), stops[1]).x
|
||||
var want: float = vp.get_global_rect().get_center().x
|
||||
_check("R2 停点切换后当前节点投影=视口中线(got=%.1f want=%.1f±2)〔卷轴批 V2〕" % [sx, want],
|
||||
absf(sx - want) <= 2.0)
|
||||
|
||||
|
||||
## R7 平滑滚动有 tween 中间帧;减动效开=位置即达(画布坐标域判定)
|
||||
func _probe_smooth_scroll_reduced_motion(screen, vp) -> void:
|
||||
var saved_scale: float = UiKit.motion_scale
|
||||
UiKit.motion_scale = 1.0
|
||||
var stops := _centerable_stops(screen, vp)
|
||||
if stops.size() < 3:
|
||||
_problems.append("R7 可居中停点不足(=%d)" % stops.size())
|
||||
return
|
||||
# 起点:锚在停点 A(画面已停稳)
|
||||
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[0]))
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().create_timer(0.6).timeout
|
||||
var start: float = vp.viewport_center_canvas_pos().x
|
||||
# 停点切换 A→B(屏幕保持可见=兑付回图同路径)——平滑滚动
|
||||
var target_b: float = screen.layout_probe(_runner.nodes())[stops[1]].x
|
||||
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[1]))
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().create_timer(0.05).timeout # 中间帧采样(300ms 单段缓动早段)
|
||||
var mid: float = vp.viewport_center_canvas_pos().x
|
||||
var total := absf(target_b - start)
|
||||
_check("R7 平滑滚动中间帧在途中(start=%.1f mid=%.1f target=%.1f)〔卷轴批 V7〕" % [start, mid, target_b],
|
||||
total > 4.0 and absf(mid - start) > 0.5 and absf(mid - target_b) > 0.5
|
||||
and (mid - start) * (target_b - start) > 0.0) # 同向在途
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_check("R7 平滑滚动终态到位〔卷轴批 V7〕",
|
||||
absf(vp.viewport_center_canvas_pos().x - target_b) <= 2.0)
|
||||
# 减动效置位(UiKit.motion 通道单点——设置面开关写同一值):停点切换 B→C 位置即达
|
||||
UiKit.motion_scale = 0.5
|
||||
var target_c: float = screen.layout_probe(_runner.nodes())[stops[2]].x
|
||||
_runner = _make_runner_at_stop(3, SEED_L3, _runner.node_id_of(stops[2]))
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
_check("R7 减动效开=位置即达(got=%.1f target=%.1f)〔卷轴批 V7〕" %
|
||||
[vp.viewport_center_canvas_pos().x, target_c],
|
||||
absf(vp.viewport_center_canvas_pos().x - target_c) <= 2.0)
|
||||
UiKit.motion_scale = saved_scale
|
||||
|
||||
|
||||
## R8 指引件可见性与几何(深图超窗——像素级判据②③)
|
||||
func _probe_guidance_chrome(screen, vp) -> void:
|
||||
var band_l: Control = vp.get_node_or_null(NodePath("FadeL"))
|
||||
var band_r: Control = vp.get_node_or_null(NodePath("FadeR"))
|
||||
var chip: Control = vp.get_node_or_null(NodePath("PanHint"))
|
||||
if band_l == null or band_r == null or chip == null:
|
||||
_problems.append("R8 指引件缺位(FadeL/FadeR/PanHint)〔卷轴批 V8〕")
|
||||
return
|
||||
_check("R8 深图渐隐带可见〔卷轴批 V8〕", band_l.visible and band_r.visible)
|
||||
_check("R8 渐隐带宽 70px(L=%.1f R=%.1f)〔卷轴批 V8〕" % [band_l.size.x, band_r.size.x],
|
||||
absf(band_l.size.x - 70.0) <= 0.5 and absf(band_r.size.x - 70.0) <= 0.5)
|
||||
_check("R8 平移提示 chip 可见且 240×30(size=%s)〔卷轴批 V8〕" % chip.size,
|
||||
chip.visible and absf(chip.size.x - 240.0) <= 0.5 and absf(chip.size.y - 30.0) <= 0.5)
|
||||
_check("R8 chip 图区底部居中(水平中线对窗中线±2)〔卷轴批 V8〕",
|
||||
absf(chip.get_global_rect().get_center().x - vp.get_global_rect().get_center().x) <= 2.0)
|
||||
|
||||
|
||||
## R8 浅图指引件隐藏(无暗示必要)
|
||||
func _probe_shallow_guidance_hidden(screen, vp) -> void:
|
||||
_runner = _make_runner_entry(1, SEED_L1)
|
||||
screen.open_choices(_cand_nodes(_runner), _runner)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var band_l: Control = vp.get_node_or_null(NodePath("FadeL"))
|
||||
var chip: Control = vp.get_node_or_null(NodePath("PanHint"))
|
||||
_check("R8 浅图画布不超窗指引件隐藏〔卷轴批 V8〕",
|
||||
band_l != null and not band_l.visible and chip != null and not chip.visible)
|
||||
|
||||
|
||||
# ---- 驱动与查证工具(状态注入面——直调核心层写口) ----
|
||||
|
||||
## 入口停点 runner(defer_entry——选路态挂起、候选=入口节点)
|
||||
func _make_runner_entry(level_index: int, seed_val: int):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.new_adventure(seed_val)
|
||||
var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng)
|
||||
if not runner.start_level_external(level_index, Callable(runner, "rest_heal_amount"), true):
|
||||
_problems.append("状态注入失败:start_level_external(%d, %d) 未受理" % [level_index, seed_val])
|
||||
return runner
|
||||
|
||||
|
||||
## 中段停点 runner(断点续玩重建——图由种子+参数档重生成同图,零 RNG 消耗)
|
||||
func _make_runner_at_stop(level_index: int, seed_val: int, node_id: int):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.new_adventure(seed_val)
|
||||
var runner = load("res://core/level_runner.gd").new(get_node("/root/ConfigDB"), rng)
|
||||
if not runner.start_level_external(level_index, Callable(runner, "rest_heal_amount"),
|
||||
false, {"node_id": node_id, "visited_ids": [node_id]}):
|
||||
_problems.append("状态注入失败:断点停点 %d 未受理" % node_id)
|
||||
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
|
||||
|
||||
|
||||
## 深图可居中停点选取(有出边且画布 x 落在夹紧域内的节点——锚定不贴边、当前节点投影可居中;
|
||||
## 按 x 降序去重取第 idx 个——同层同 x 只计一个,层距 122 保证停点间滚动位移充分)
|
||||
func _centerable_stops(screen, vp) -> Array:
|
||||
var nodes: Array = _runner.nodes()
|
||||
var pos: Dictionary = screen.layout_probe(nodes)
|
||||
var half: float = vp.size.x / 2.0
|
||||
var canvas_len: float = _canvas_len(screen)
|
||||
var by_x_desc: Array = nodes.duplicate()
|
||||
by_x_desc.sort_custom(func(a, b) -> bool: return pos[a].x > pos[b].x)
|
||||
var picked: Array = []
|
||||
var seen_x := {}
|
||||
for node in by_x_desc:
|
||||
if _runner.node_out_ids(node).is_empty():
|
||||
continue
|
||||
var x: float = pos[node].x
|
||||
if x < half + 20.0 or x > canvas_len - half - 20.0 or seen_x.has(x):
|
||||
continue
|
||||
seen_x[x] = true
|
||||
picked.append(node)
|
||||
return picked
|
||||
|
||||
|
||||
func _find_viewport(screen) -> Control:
|
||||
return _find_by_script(screen, ViewportScript)
|
||||
|
||||
|
||||
func _find_by_script(root: Node, script: Script) -> Control:
|
||||
if root.get_script() == script:
|
||||
return root as Control
|
||||
for child in root.get_children():
|
||||
var found := _find_by_script(child, script)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
func _canvas_len(screen) -> float:
|
||||
return screen.canvas_probe()["min_size"].x # 画布包络宽(open_choices 拟合值——画布尺寸即它)
|
||||
|
||||
|
||||
func _node_screen_pos(screen, pos: Dictionary, node: Dictionary) -> Vector2:
|
||||
return screen.canvas_probe()["origin"] + pos[node]
|
||||
|
||||
|
||||
func _extreme_node(pos: Dictionary, nodes: Array, first: bool) -> Dictionary:
|
||||
var best := {}
|
||||
var best_x := INF if first else -INF
|
||||
for node in nodes:
|
||||
var x: float = pos[node].x
|
||||
if (first and x < best_x) or (not first and x > best_x):
|
||||
best_x = x
|
||||
best = node
|
||||
return best
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if _checks == 0:
|
||||
_problems.append("零断言执行(断言数下限闸——三硬闸③)")
|
||||
if _problems.is_empty():
|
||||
print("[level_map_viewport_probe] PASS — 卷轴视口行为断言全绿(%d 项)" % _checks)
|
||||
get_tree().quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
printerr("[level_map_viewport_probe] FAIL — " + p)
|
||||
get_tree().quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cprbakccv06re
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://probe/level_map_viewport_probe.gd" id="1"]
|
||||
|
||||
[node name="LevelMapViewportProbe" type="Node"]
|
||||
script = ExtResource("1")
|
||||
@@ -141,6 +141,42 @@ static func push_click_at(main, pos: Vector2) -> void:
|
||||
main.get_viewport().push_input(up, true)
|
||||
|
||||
|
||||
# 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批
|
||||
# R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件
|
||||
# 自身承载,被测面按 relative 逐帧平移)
|
||||
static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) -> void:
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = pos
|
||||
down.global_position = pos
|
||||
down.button_index = MOUSE_BUTTON_LEFT
|
||||
down.pressed = true
|
||||
main.get_viewport().push_input(down, true)
|
||||
for i in range(1, steps + 1):
|
||||
var cur := pos + delta * float(i) / float(steps)
|
||||
var mv := InputEventMouseMotion.new()
|
||||
mv.position = cur
|
||||
mv.global_position = cur
|
||||
mv.relative = delta / float(steps)
|
||||
main.get_viewport().push_input(mv, true)
|
||||
var up := InputEventMouseButton.new()
|
||||
up.position = pos + delta
|
||||
up.global_position = pos + delta
|
||||
up.button_index = MOUSE_BUTTON_LEFT
|
||||
up.pressed = false
|
||||
main.get_viewport().push_input(up, true)
|
||||
|
||||
|
||||
# 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面,
|
||||
# 本工具只发原始滚轮档)
|
||||
static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.position = pos
|
||||
ev.global_position = pos
|
||||
ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true)
|
||||
|
||||
|
||||
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽
|
||||
# user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点);
|
||||
# 驱动后写盘目标=隔离槽文件(持久化断言读 user://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
|
||||
|
||||
@@ -87,6 +87,14 @@ func _ready() -> void:
|
||||
evf.position = fog_pos
|
||||
evf.global_position = fog_pos
|
||||
main.get_viewport().push_input(evf, true)
|
||||
# 完整点击=press+release 成对(卷轴视口批阈值语义:拖距跟踪器由按下起武装,
|
||||
# 单 press 沿会留 armed 态污染后续 motion 判定——判据语义不变:雾下点击零提交)
|
||||
var evf2 := InputEventMouseButton.new()
|
||||
evf2.button_index = MOUSE_BUTTON_LEFT
|
||||
evf2.pressed = false
|
||||
evf2.position = fog_pos
|
||||
evf2.global_position = fog_pos
|
||||
main.get_viewport().push_input(evf2, true)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
ro.route_chosen.disconnect(on_route)
|
||||
@@ -114,6 +122,14 @@ func _ready() -> void:
|
||||
evc.position = gpos
|
||||
evc.global_position = gpos
|
||||
main.get_viewport().push_input(evc, true)
|
||||
# 完整点击=press+release 成对(卷轴视口批阈值语义:提交沿=未超阈值的抬杆——
|
||||
# 判据语义不变:图上真实点击提交选路;单 press 沿不再是提交沿)
|
||||
var evr := InputEventMouseButton.new()
|
||||
evr.button_index = MOUSE_BUTTON_LEFT
|
||||
evr.pressed = false
|
||||
evr.position = gpos
|
||||
evr.global_position = gpos
|
||||
main.get_viewport().push_input(evr, true)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.run.level_screen.visible or main.run_scope.runner.awaiting_choice():
|
||||
|
||||
Reference in new issue
Block a user