批1(QA harness·探针存量emit真输入化+覆盖登记写入): 两探针52处emit站点转真输入管线(push_input press+release成对)51处,保留1处置灰态穿透emit标〔接线断言〕; 新增probe_coverage_recorder.gd真输入覆盖登记件(旗标在位期间命中入账,锚链归一化键=具名/脚本锚+类#钮面文本+序位,@临时名不入键,JSONP落tests/.gates/coverage/每探针一件); probe_ui_util补coverage_hook单点+push_motion/push_wheel(滚轮press+release成对——只发press钉死鼠标焦点实证fork/返回营地两钮点击全失效)/window_panel/scroll_button_into_view(折叠区行钮rect越窗底栏实证误中返回营地钮,真输入滚轮滚入可视区再点); V5b遮挡试探订正=前窗经真实返回钮清账再真点开窗(emit直发穿遮罩属口径违例); 落点判定补钉2处(esc段子界面开窗落点/树窗返回钮落点); test-conventions §2新增UI驱动断言真输入管线条文(滚轮成对+折叠区滚入+落点判定+接线断言豁免口径); .gates/.gitignore收coverage/。读数: screen_flow_probe孤立直跑PASS(六步动线+屏级互斥全绿); ui_click_probe孤立直跑exit=0问题=0(budget=15210,三笔树购买请求全通,PROBE_SEED=7); 登记落盘ui_click=27控件/38命中/临时名跳过0,screen_flow=19控件/25命中。全量档闸(fd9c8ad9基线): 两新增红均判为主区推进漂移非本批引入——流水线闸红=待办清单QA条目证据句(master 1730a8fb已修),单元settlement_wiring_snapshot红待并master后窄跑复核; 本步凭据=两探针直跑绿+偏差8条已回写qa-harness讨论文档§10
This commit is contained in:
1 parent
fd9c8ad90e
commit
2cbd2ca9a7
7 files changed
+390
-79
No files matched your search
@@ -20,6 +20,8 @@
|
||||
|
||||
凡模态覆盖层,探针必须断言:打开后自身 visible 且遮挡物已隐藏(模态被遮挡 = 功能存在但不可达)。
|
||||
|
||||
**UI 驱动断言须走真输入管线(2026-11-06 立)**:探针里「点按钮/点场景物件」类驱动断言,一律经引擎输入管线承载(合成 InputEvent 走 `push_input`,press+release 成对;合成滚轮同样必须 press+release 成对——只发 press 会把鼠标焦点钉死在按下点控件上,后续点击全部失效);信号直发(`pressed.emit()` 等)绕过命中测试,保不了「玩家点击可达」,只许用于真输入到不了的面并当场标〔接线断言〕(现役例:置灰态穿透钉处理器兜底)。折叠区控件先以真输入滚轮滚入可视区再点。每个动线断言钉「落点」判定(点了之后到哪个屏/哪层可见),防驱动中断后的空转假绿。真输入命中控件时自动登记控件覆盖(探针共用面登记件落 `.gates/` 观测产物),覆盖面生长不靠人记。
|
||||
|
||||
## 3. 缺陷修复必须沉淀回归断言
|
||||
|
||||
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(例:封印动作耗尽 AP → 必须进入第 2 轮;
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# probe_coverage_recorder.gd — 真输入覆盖登记件(探针共用面)
|
||||
# 是什么:探针/C 驱动件把合成 InputEvent 经 Viewport.push_input 送进引擎输入管线时,登记
|
||||
# 「真输入实际命中了哪些交互控件」,探针收尾落 JSONP 式 .js 到 tests/.gates/coverage/
|
||||
# (.js 供 file:// 页面 <script src> 直载——fetch 本地 json 会被 CORS 拦)。
|
||||
# 成员面(两源):① 场景树全部 BaseButton——pressed 沿自动接线(重扫懒触发,见下);
|
||||
# ② 自绘命中类——现役接 camp_scene_view.object_pressed(信号带物件 id,key 追加 #物件id 后缀),
|
||||
# 其余自绘命中面走 note_hit() 显式口。
|
||||
# 真输入旗标:push 前置位、帧末清位——只有旗标在位期间的命中入账;信号直发(〔接线断言〕保留站)
|
||||
# 与产品码自发射永不入账。命中=控件真的收到事件:hitbox 错位/遮挡时零命中,缺口自然暴露——
|
||||
# 这是「新控件落地→覆盖登记自动出现它」的数据源。
|
||||
# key 规则(契约「场景文件路径+节点路径」的实况落地偏差,详见 qa-harness 讨论文档回写节):
|
||||
# 本仓库 UI 大头由代码构建(main_ui_build/ui_kit 不设节点名),运行时节点路径全为 @类@序
|
||||
# 临时名——契约原样落地产出空登记。故键取「锚链归一化」形态:自主根到控件只收集带稳定身份的
|
||||
# 锚(具名节点用其名、带脚本节点用脚本文件名;@ 临时名段整体不入键),控件本体记 类#钮面文本,
|
||||
# 同键控件按接线遍历序追加 #序位。键在确定构建序下可复现;UI 构建序变更会平移同键序位——
|
||||
# 表现为键漂移而非静默丢失,属测量面可接受代价。
|
||||
# 用法:探针 _ready 里 `_recorder = ProbeCoverageRecorder.new()` → `attach(main, "<探针名>")`;
|
||||
# 收尾 `dump_js()`。探针共用面不入 S 系扫描面(code-conventions §4.13 分档)。
|
||||
extends RefCounted
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
|
||||
var _main: Node
|
||||
var _probe_name: String
|
||||
var _pending := false # 真输入在途旗标
|
||||
var _camp: Control = null # 营地场景根(自绘命中单点)
|
||||
var _hits: Dictionary = {} # 稳定 key(String) → 命中次数(int)
|
||||
var _node_keys: Dictionary = {} # 接线时分配的节点键(instance_id → key)
|
||||
var _key_seen: Dictionary = {} # 基键出现次数(同键控件按遍历序追加序位)
|
||||
var _wired: Dictionary = {} # 已连 pressed 的按钮实例 id → true(防重连)
|
||||
var _skipped_temp := 0 # 临时名命中笔数(不入 key,只计数)
|
||||
|
||||
|
||||
# ---- 装载与接线 ----
|
||||
|
||||
func attach(main: Node, probe_name: String) -> void:
|
||||
_main = main
|
||||
_probe_name = probe_name
|
||||
ProbeUiUtil.coverage_hook = self # 共用件真输入口统一过旗标(push_click/push_click_at/push_motion)
|
||||
_wire_buttons()
|
||||
_wire_camp_scene()
|
||||
|
||||
|
||||
# 懒重扫:每次真输入 push 前接线一遍新入树的按钮(开层重建的钮行在下次点击前接上);
|
||||
# 已释放按钮的连接随节点释放失效,登记表按 instance_id 清扫。
|
||||
func mark_real_input() -> void:
|
||||
for id in _wired.keys():
|
||||
if instance_from_id(int(id)) == null:
|
||||
_wired.erase(id)
|
||||
_node_keys.erase(id)
|
||||
_wire_buttons()
|
||||
if not _pending: # 同帧多次 push 只挂一次帧末清位旗(重复 connect 会报引擎错)
|
||||
_pending = true
|
||||
_main.get_tree().process_frame.connect(_clear_pending, CONNECT_ONE_SHOT)
|
||||
|
||||
|
||||
func _clear_pending() -> void:
|
||||
_pending = false
|
||||
|
||||
|
||||
func _wire_buttons() -> void:
|
||||
_wire_into(_main)
|
||||
|
||||
|
||||
func _wire_into(node: Node) -> void:
|
||||
if node is BaseButton:
|
||||
var id := node.get_instance_id()
|
||||
if not _wired.has(id):
|
||||
_wired[id] = true
|
||||
_node_keys[id] = _assign_key(node)
|
||||
(node as BaseButton).pressed.connect(_on_button_pressed.bind(id))
|
||||
for child in node.get_children():
|
||||
_wire_into(child)
|
||||
|
||||
|
||||
func _wire_camp_scene() -> void:
|
||||
_camp = _find_by_script(_main, "camp_scene_view")
|
||||
if _camp != null:
|
||||
_camp.object_pressed.connect(_on_camp_object)
|
||||
|
||||
|
||||
func _find_by_script(root: Node, script_name: String) -> Control:
|
||||
var s: Script = root.get_script()
|
||||
if s != null and String(s.resource_path).contains(script_name) and root is Control:
|
||||
return root as Control
|
||||
for child in root.get_children():
|
||||
var found := _find_by_script(child, script_name)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
|
||||
|
||||
# ---- 键分配(锚链归一化,接线时一次算好存表)----
|
||||
|
||||
func _assign_key(n: Node) -> String:
|
||||
var chain := _anchor_chain(n)
|
||||
var leaf := ""
|
||||
if n is BaseButton:
|
||||
leaf = "%s#%s" % [n.get_class(), String((n as BaseButton).text)]
|
||||
else:
|
||||
leaf = n.get_class()
|
||||
var base := "%s:%s/%s" % [String(_main.scene_file_path), "/".join(chain), leaf]
|
||||
# 同键控件按接线遍历序加序位(#1 省略)——重复键可分账,序位随构建序漂移属已知代价
|
||||
_key_seen[base] = int(_key_seen.get(base, 0)) + 1
|
||||
if int(_key_seen[base]) > 1:
|
||||
base += "#%d" % int(_key_seen[base])
|
||||
return base
|
||||
|
||||
|
||||
# 自主根到 n 的稳定锚链:具名节点取其名、带脚本节点取脚本文件名;@ 临时名段不入链。
|
||||
func _anchor_chain(n: Node) -> Array[String]:
|
||||
var chain: Array[String] = []
|
||||
var cur: Node = n
|
||||
while cur != null and cur != _main:
|
||||
var nm := String(cur.name)
|
||||
if not nm.begins_with("@"):
|
||||
chain.push_front(nm)
|
||||
elif cur.get_script() != null:
|
||||
var sp := String((cur.get_script() as Script).resource_path)
|
||||
chain.push_front(sp.get_file().trim_suffix(".gd"))
|
||||
cur = cur.get_parent()
|
||||
if chain.is_empty():
|
||||
chain.push_front("(root)")
|
||||
return chain
|
||||
|
||||
|
||||
# ---- 显式登记口(自绘命中类无信号可连时的单点)----
|
||||
|
||||
func note_hit(n: Node, suffix: String = "") -> void:
|
||||
if not _pending or n == null:
|
||||
return
|
||||
var id := n.get_instance_id()
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
if key == "":
|
||||
key = _assign_key(n)
|
||||
_node_keys[id] = key
|
||||
_bump(key + suffix)
|
||||
|
||||
|
||||
# ---- 命中处理 ----
|
||||
|
||||
func _on_button_pressed(id: int) -> void:
|
||||
if not _pending:
|
||||
return
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
if key == "":
|
||||
_skipped_temp += 1
|
||||
return
|
||||
_bump(key)
|
||||
|
||||
|
||||
func _on_camp_object(key: StringName) -> void:
|
||||
if not _pending or _camp == null:
|
||||
return
|
||||
var id := _camp.get_instance_id()
|
||||
var base := String(_node_keys.get(id, ""))
|
||||
if base == "":
|
||||
base = _assign_key(_camp)
|
||||
_node_keys[id] = base
|
||||
_bump(base + "#" + String(key))
|
||||
|
||||
|
||||
func _bump(key: String) -> void:
|
||||
_hits[key] = int(_hits.get(key, 0)) + 1
|
||||
|
||||
|
||||
# ---- 落盘(JSONP 式 .js——file:// 页面 <script src> 直载)----
|
||||
|
||||
func dump_js() -> void:
|
||||
var dir := DirAccess.open("res://")
|
||||
if dir == null:
|
||||
push_error("[coverage] res:// 打不开,登记未落盘")
|
||||
return
|
||||
var mk := dir.make_dir_recursive("tests/.gates/coverage")
|
||||
if mk != OK and not dir.dir_exists("tests/.gates/coverage"):
|
||||
push_error("[coverage] coverage 目录建不了(err=%d),登记未落盘" % mk)
|
||||
return
|
||||
var path := "res://tests/.gates/coverage/probe_hits_%s.js" % _probe_name
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("[coverage] 登记文件打不开:%s" % path)
|
||||
return
|
||||
var total := 0
|
||||
for k in _hits:
|
||||
total += int(_hits[k])
|
||||
var payload := {
|
||||
"meta": {
|
||||
"probe": _probe_name,
|
||||
"seed": OS.get_environment("PROBE_SEED"),
|
||||
"scene": String(_main.scene_file_path),
|
||||
"generated_at": Time.get_datetime_string_from_system(),
|
||||
"driver": "InputEvent via Viewport.push_input(真输入管线)",
|
||||
"key_shape": "锚链归一化(具名/脚本锚+类#文本+序位;@ 临时名不入键)",
|
||||
"hits_total": total,
|
||||
"skipped_temp_names": _skipped_temp,
|
||||
},
|
||||
"hits": _hits,
|
||||
}
|
||||
var js := "/* 真输入覆盖登记——探针每次运行自动覆写,人工修订无效(豁免面走人工清单,不落本文件)。 */\n"
|
||||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||||
js += "window.__QA_COVERAGE__.probe_real_input = window.__QA_COVERAGE__.probe_real_input || {};\n"
|
||||
js += "window.__QA_COVERAGE__.probe_real_input[%s] = %s;\n" % [
|
||||
JSON.stringify(_probe_name), JSON.stringify(payload, " ")]
|
||||
f.store_string(js)
|
||||
f.close()
|
||||
print("[coverage] 真输入命中登记已落盘 %s(控件 %d 处 / 命中 %d 次 / 临时名跳过 %d)" % [
|
||||
path, _hits.size(), total, _skipped_temp])
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1uhtlw3kmhxw
|
||||
@@ -120,13 +120,20 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
|
||||
# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)----
|
||||
# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对
|
||||
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。
|
||||
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 先例式)。
|
||||
# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。
|
||||
# coverage_hook = 真输入覆盖登记件(probe_coverage_recorder.gd)挂点:本件三个 push 口是探针
|
||||
# 真输入的单点通道,push 前通知登记件置旗标——命中记账只认旗标在位期间(信号直发不入账)。
|
||||
static var coverage_hook: Object = null
|
||||
|
||||
|
||||
static func push_click(main, ctrl: Control) -> void:
|
||||
push_click_at(main, ctrl.get_global_rect().get_center())
|
||||
|
||||
|
||||
static func push_click_at(main, pos: Vector2) -> void:
|
||||
if coverage_hook != null:
|
||||
coverage_hook.mark_real_input()
|
||||
var down := InputEventMouseButton.new()
|
||||
down.position = pos
|
||||
down.global_position = pos
|
||||
@@ -141,6 +148,71 @@ static func push_click_at(main, pos: Vector2) -> void:
|
||||
main.get_viewport().push_input(up, true)
|
||||
|
||||
|
||||
# 悬停真输入(tooltip/悬停态断言的承载——motion 进管线触发 mouse_entered/exited 沿)
|
||||
static func push_motion(main, pos: Vector2) -> void:
|
||||
if coverage_hook != null:
|
||||
coverage_hook.mark_real_input()
|
||||
var mv := InputEventMouseMotion.new()
|
||||
mv.position = pos
|
||||
mv.global_position = pos
|
||||
main.get_viewport().push_input(mv, true)
|
||||
|
||||
|
||||
# 滚轮真输入(dir=+1 向下翻 / -1 向上翻——折叠区行滚入视野的唯一真输入通道)。
|
||||
# wheel 合成必须 press+release 成对:硬件滚轮虽无 release 沿,但引擎 GUI 会把按键事件的
|
||||
# press 记为鼠标焦点且等 release 才释放——只发 press 会把焦点钉死在按下点控件上,
|
||||
# 后续左键点击全被路由给焦点控件(实证:滚轮后 fork/返回营地两钮点击全部失效)。
|
||||
static func push_wheel(main, pos: Vector2, dir: int) -> void:
|
||||
if coverage_hook != null:
|
||||
coverage_hook.mark_real_input()
|
||||
var idx := MOUSE_BUTTON_WHEEL_DOWN if dir > 0 else MOUSE_BUTTON_WHEEL_UP
|
||||
var w := InputEventMouseButton.new()
|
||||
w.position = pos
|
||||
w.global_position = pos
|
||||
w.button_index = idx
|
||||
w.pressed = true
|
||||
main.get_viewport().push_input(w, true)
|
||||
var w2 := InputEventMouseButton.new()
|
||||
w2.position = pos
|
||||
w2.global_position = pos
|
||||
w2.button_index = idx
|
||||
w2.pressed = false
|
||||
main.get_viewport().push_input(w2, true)
|
||||
|
||||
|
||||
# 窗板定位(窗壳视图子树固定链:遮罩 ColorRect → CenterContainer → 窗板 PanelContainer——
|
||||
# screen_flow_probe._window_panel 同式,探针族共用单点)
|
||||
static func window_panel(view: Control) -> PanelContainer:
|
||||
if view == null or view.get_child_count() == 0:
|
||||
return null
|
||||
var dim := view.get_child(0)
|
||||
if dim.get_child_count() == 0:
|
||||
return null
|
||||
var center := dim.get_child(0)
|
||||
if center.get_child_count() == 0 or not (center.get_child(0) is PanelContainer):
|
||||
return null
|
||||
return center.get_child(0) as PanelContainer
|
||||
|
||||
|
||||
# 折叠区行滚入窗板可视区(真输入滚轮驱动;滚动不重建行,rect 随滚更新)。
|
||||
# 判据:钮 rect 整体落在窗板内(上缘留 8px、下缘让开底栏 64px——返回营地钮带高约 56px)。
|
||||
# 保险丝 60 次滚叶防异常内容死循环;窗板缺失或钮句柄失效即返回(调用方前置断言兜底)。
|
||||
static func scroll_button_into_view(main, view: Control, btn: Control) -> void:
|
||||
var panel := window_panel(view)
|
||||
if panel == null or btn == null:
|
||||
return
|
||||
var guard := 60
|
||||
while guard > 0:
|
||||
var r := btn.get_global_rect()
|
||||
var pr := panel.get_global_rect()
|
||||
if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 64.0:
|
||||
return
|
||||
var down := r.end.y > pr.end.y - 64.0
|
||||
push_wheel(main, pr.get_center(), 1 if down else -1)
|
||||
guard -= 1
|
||||
await main.get_tree().process_frame
|
||||
|
||||
|
||||
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽
|
||||
# user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点);
|
||||
# 驱动后写盘目标=隔离槽文件(持久化断言读 user://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
extends Node
|
||||
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||||
|
||||
var main: Node
|
||||
var _problems: Array[String] = []
|
||||
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -20,6 +22,10 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
|
||||
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
|
||||
_recorder = ProbeCoverageRecorder.new()
|
||||
_recorder.attach(main, "screen_flow")
|
||||
|
||||
# 隔离存档(全 worktree 共享同一份 user://save.json——探针必须换注,禁碰真实档)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow", false, false, false)
|
||||
await get_tree().process_frame
|
||||
@@ -35,6 +41,7 @@ func _ready() -> void:
|
||||
await _step_abandon_to_end()
|
||||
await _step_end_back_to_menu()
|
||||
|
||||
_recorder.dump_js()
|
||||
if _problems.is_empty():
|
||||
print("[screen_flow_probe] PASS — 六步主流程+回退路径全绿(屏级互斥:任意时刻 ≤1 屏可见)")
|
||||
get_tree().quit(0)
|
||||
@@ -122,11 +129,11 @@ func _step_camp_back_to_menu() -> void:
|
||||
if main.ui.modals.camp.scene == null:
|
||||
_problems.append("③营地→菜单:营地场景根句柄缺失〔句柄子容器批 V1〕")
|
||||
return
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"campfire").get_center()) # 真输入(营地场景 _gui_input 命中——大门段同式)
|
||||
await _frames()
|
||||
if not main.ui.modals.menu.confirm_dialog.visible:
|
||||
_problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕")
|
||||
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」离场(confirmed 先于关层——卡④ T4)
|
||||
await _frames()
|
||||
_expect_exclusive("③营地→菜单(回退)", "菜单")
|
||||
|
||||
@@ -150,7 +157,7 @@ func _step_save_select_open_back() -> void:
|
||||
if back_btn == null:
|
||||
_problems.append("[savesel] sl10 返回钮缺失(页壳顶条)")
|
||||
else:
|
||||
back_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, back_btn) # 真点「返回」→ 关选档层
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)")
|
||||
@@ -228,7 +235,7 @@ func _step_end_back_to_menu() -> void:
|
||||
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.visible:
|
||||
_problems.append("⑦终局→菜单:前置不在终局")
|
||||
return
|
||||
main.ui.modals.run.end_overlay.continue_pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「继续」(_btn=continue_pressed 同源句柄)
|
||||
await _frames(3)
|
||||
_expect_exclusive("⑦终局→菜单", "菜单")
|
||||
if main.run_scope != null and main.run_scope.is_adventure:
|
||||
@@ -254,8 +261,10 @@ func _step_camp_esc_and_hud() -> void:
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 子界面开态(真输入命中)
|
||||
await _frames()
|
||||
if not main.ui.modals.camp.tree.visible:
|
||||
_problems.append("[esc] 子界面未随场景点选打开(后续关窗断言的前置落点)")
|
||||
_push_esc()
|
||||
await _frames()
|
||||
if main.ui.modals.camp.tree.visible:
|
||||
@@ -285,7 +294,7 @@ func _step_camp_esc_and_hud() -> void:
|
||||
if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕〔ESC批 B3〕")
|
||||
# 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显)
|
||||
main.ui.modals.camp.esc_overlay.return_requested.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.camp.esc_overlay._return_btn) # 真点「返回主菜单」
|
||||
await _frames()
|
||||
if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕〔ESC批 B3〕")
|
||||
@@ -323,11 +332,11 @@ func _step_camp_window_shell() -> void:
|
||||
if prev_back == null:
|
||||
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(opened.back()))
|
||||
else:
|
||||
prev_back.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, prev_back) # 真点「返回营地」关前窗
|
||||
await _frames()
|
||||
if prev.visible or not main.ui.modals.camp.scene.visible:
|
||||
_problems.append("[winshell] %s 关闭钮按下未隐窗或场景根失守〔窗口化批 V4〕" % String(opened.back()))
|
||||
main.ui.modals.camp.scene.object_pressed.emit(k)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(k).get_center()) # 真输入开窗(场景 _gui_input 命中)
|
||||
await _frames()
|
||||
opened.append(k)
|
||||
var view: Control = views[k]
|
||||
@@ -362,7 +371,10 @@ func _step_camp_window_shell() -> void:
|
||||
if vis_n != 1:
|
||||
_problems.append("[winshell] %s 开态五窗 visible 计数=%d 应=1(互斥单开失守)〔窗口化批 V5〕" % [String(k), vis_n])
|
||||
# 树窗点选遮挡(V5b——树窗开态向锻炉命中区推真实鼠标点:被遮罩挡=零新窗零响应)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||||
# 真输入化订正:前窗(图鉴)先经真实关闭钮清账再开树窗——emit 直发可穿透遮罩开窗,
|
||||
# 真输入下「无窗态场景命中」是唯一合法开窗路径(遮罩语义由本段正面验证)。
|
||||
await _close_all_windows(views)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
|
||||
await _frames()
|
||||
var tree_view: Control = views[&"dummy"]
|
||||
if not tree_view.visible:
|
||||
@@ -370,8 +382,7 @@ func _step_camp_window_shell() -> void:
|
||||
else:
|
||||
var hotspot: Rect2 = main.ui.modals.camp.scene.get_hotspot_rect(&"forge")
|
||||
var c := hotspot.get_center()
|
||||
_push_click(c)
|
||||
_push_release(c)
|
||||
ProbeUiUtil.push_click_at(main, c) # 遮挡试探真点击——遮罩生效则零命中零响应
|
||||
await _frames()
|
||||
var vis_n2 := 0
|
||||
for w in five:
|
||||
@@ -383,7 +394,7 @@ func _step_camp_window_shell() -> void:
|
||||
# 随栈 close 恢复语义逐层清账,V6/V8 从干净单窗态起步)
|
||||
await _close_all_windows(views)
|
||||
# V6 ESC 先关窗(生涯窗开态 Esc→关生涯窗、浮窗不唤起)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
|
||||
await _frames()
|
||||
if not career.visible:
|
||||
_problems.append("[winshell] 生涯窗未开(V6 前置中断)〔窗口化批 V6〕")
|
||||
@@ -395,7 +406,7 @@ func _step_camp_window_shell() -> void:
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("[winshell] 关窗同拍唤起浮窗(应无窗才进浮窗)〔窗口化批 V6〕")
|
||||
# V7 内容超高窗内滚动(树窗+真实投影喂加长行册)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
|
||||
await _frames()
|
||||
if not tree_view.visible:
|
||||
_problems.append("[winshell] 树窗未开(V7 前置中断)〔窗口化批 V7〕")
|
||||
@@ -420,7 +431,7 @@ func _step_camp_window_shell() -> void:
|
||||
# 清窗经真实钮(同 V6 前置——干净单窗态起步)
|
||||
await _close_all_windows(views)
|
||||
# V8 生涯同壳(窗板同几何+返回钮在位+内容列滚动面在)
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"notebook")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center())
|
||||
await _frames()
|
||||
if not career.visible:
|
||||
_problems.append("[winshell] 生涯窗未开(V8 前置中断)〔窗口化批 V8〕")
|
||||
@@ -445,7 +456,7 @@ func _step_camp_window_shell() -> void:
|
||||
# 段尾收闭:生涯窗经真实「返回」钮关闭(on_camp_closed 关闭名单不含生涯窗——防残留泄段)
|
||||
var career_back: Button = ProbeUiUtil.find_button(career, "返回", "prefix")
|
||||
if career_back != null:
|
||||
career_back.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, career_back) # 真点「返回」收闭生涯窗
|
||||
await _frames()
|
||||
if career.visible:
|
||||
_problems.append("[winshell] 生涯「返回」钮未隐窗〔窗口化批 V4〕")
|
||||
@@ -463,7 +474,7 @@ func _close_all_windows(views: Dictionary) -> void:
|
||||
if back == null:
|
||||
_problems.append("[winshell] %s 底栏「返回营地」钮缺失〔窗口化批 V4〕" % String(k))
|
||||
continue
|
||||
back.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, back) # 真点「返回营地」逐窗清账
|
||||
await _frames()
|
||||
|
||||
|
||||
@@ -480,25 +491,7 @@ func _window_panel(view: Control) -> PanelContainer:
|
||||
return center.get_child(0) as PanelContainer
|
||||
|
||||
|
||||
# 真实鼠标合成(press+release 成对——BaseButton pressed 信号默认 release 触发,ui_input_probe :718 先例)
|
||||
func _push_click(pos: Vector2) -> void:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.position = pos
|
||||
ev.global_position = pos
|
||||
ev.button_index = MOUSE_BUTTON_LEFT
|
||||
ev.pressed = true
|
||||
main.get_viewport().push_input(ev, true)
|
||||
|
||||
|
||||
func _push_release(pos: Vector2) -> void:
|
||||
var ev := InputEventMouseButton.new()
|
||||
ev.position = pos
|
||||
ev.global_position = pos
|
||||
ev.button_index = MOUSE_BUTTON_LEFT
|
||||
ev.pressed = false
|
||||
main.get_viewport().push_input(ev, true)
|
||||
|
||||
|
||||
# 键盘真输入合成(鼠标点击统一走 ProbeUiUtil.push_click_at——覆盖登记单点通道)
|
||||
func _push_esc(echo := false) -> void:
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ESCAPE
|
||||
|
||||
@@ -11,8 +11,10 @@ extends Node
|
||||
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd") # O4:自动玩家策略单一实现
|
||||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||||
|
||||
var main: Node
|
||||
var _recorder: RefCounted = null # 真输入覆盖登记件(probe_coverage_recorder.gd——attach 后全探针真输入自动记账)
|
||||
var _last_state := {}
|
||||
var _stall_frames := 0
|
||||
var _closed_via_abandon := false # T17——预算耗尽经放弃路径收口(断点段返回断言豁免位)
|
||||
@@ -48,6 +50,9 @@ func _ready() -> void:
|
||||
main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
# 真输入覆盖登记(命中控件自动记账,收尾落 tests/.gates/coverage/——覆盖面生长不靠人记)
|
||||
_recorder = ProbeCoverageRecorder.new()
|
||||
_recorder.attach(main, "ui_click")
|
||||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||||
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
|
||||
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
|
||||
@@ -67,7 +72,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.scene.visible:
|
||||
_problems.append("营地场景根未打开(进档开层新路径)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 场景点选真实路由(木桩→天赋树子界面——卡④ T5)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 场景点选真输入(木桩→天赋树子界面——卡④ T5;_gui_input 命中)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.tree._tree_buy_btns.is_empty():
|
||||
_problems.append("树淬炼购买钮缺失(四柜台拆解后树=独立子界面)〔句柄子容器批 V1〕")
|
||||
@@ -85,7 +90,8 @@ func _ready() -> void:
|
||||
else:
|
||||
var bal_before: int = iso_c.meta_state.keystone_balance()
|
||||
var price: int = int(main.db.crossrun.meta_tree.row_by_id("sp_hp").get("price", 0))
|
||||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, buy_btn) # 折叠区行先滚入窗板可视区(真输入滚轮)
|
||||
ProbeUiUtil.push_click(main, buy_btn) # 真点购买 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
|
||||
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕〔营地图批 V4〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
|
||||
@@ -99,7 +105,8 @@ func _ready() -> void:
|
||||
if String(card.node_id) == "att_socket1":
|
||||
socket_btn = card.btn
|
||||
if socket_btn != null:
|
||||
socket_btn.pressed.emit()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, socket_btn)
|
||||
ProbeUiUtil.push_click(main, socket_btn) # 真点解锁(点亮同调位前置)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
@@ -110,7 +117,8 @@ func _ready() -> void:
|
||||
if fork_btn == null:
|
||||
_problems.append("Fork 解锁钮缺失(点击路径)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
fork_btn.pressed.emit()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, fork_btn) # fork 行在折叠区(rect 越窗底栏——实证误中「返回营地」钮)
|
||||
ProbeUiUtil.push_click(main, fork_btn) # 真点解锁(展开 a/b 双态钮)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
@@ -121,7 +129,8 @@ func _ready() -> void:
|
||||
if side_b == null:
|
||||
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
side_b.pressed.emit() # 合成切态点击 → _on_camp_fork_toggle → meta.tree_toggle_fork
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, side_b)
|
||||
ProbeUiUtil.push_click(main, side_b) # 真点切态 → _on_camp_fork_toggle → meta.tree_toggle_fork
|
||||
await get_tree().process_frame
|
||||
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_fk.load_from_disk()
|
||||
@@ -130,9 +139,15 @@ func _ready() -> void:
|
||||
# ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移):
|
||||
# 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图
|
||||
# _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。
|
||||
main.ui.modals.camp.tree.closed.emit() # 树界面返回(关层归 presenter 单口)
|
||||
var tree_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")
|
||||
if tree_back == null:
|
||||
_problems.append("树界面「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, tree_back) # 真点返回(关层归 presenter 单口——closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"chest") # 宝箱→秘宝匣(灰字四源位宿主)
|
||||
if main.ui.modals.camp.tree.visible:
|
||||
_problems.append("树窗「返回营地」真点击后未隐窗(真输入化补钉——落点判定)〔句柄子容器批 V1〕")
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center()) # 宝箱→秘宝匣(灰字四源位宿主)
|
||||
await get_tree().process_frame
|
||||
var ws_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp.treasure._workshop_holder, ws_labels)
|
||||
@@ -169,9 +184,13 @@ func _ready() -> void:
|
||||
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)〔句柄子容器批 V1〕")
|
||||
if not refresh_inked:
|
||||
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)〔句柄子容器批 V1〕")
|
||||
main.ui.modals.camp.treasure.closed.emit()
|
||||
var treasure_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地")
|
||||
if treasure_back == null:
|
||||
_problems.append("秘宝匣「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, treasure_back) # 真点返回(closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.scene.object_pressed.emit(&"board") # 布告板→图鉴占位(锁文宿主)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"board").get_center()) # 布告板→图鉴占位(锁文宿主)
|
||||
await get_tree().process_frame
|
||||
var codex_labels: Array = []
|
||||
_collect_labels_by_text(main.ui.modals.camp.codex, codex_labels)
|
||||
@@ -181,7 +200,11 @@ func _ready() -> void:
|
||||
for l in atlas_locks:
|
||||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||||
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕〔营地图批 V6〕")
|
||||
main.ui.modals.camp.codex.closed.emit()
|
||||
var codex_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.codex, "返回营地")
|
||||
if codex_back == null:
|
||||
_problems.append("图鉴「返回营地」钮缺失(真输入化驱动中断)〔句柄子容器批 V1〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, codex_back) # 真点返回(closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
|
||||
await get_tree().process_frame
|
||||
@@ -213,7 +236,7 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
|
||||
else:
|
||||
var samples: Array = []
|
||||
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
|
||||
ProbeUiUtil.push_click(main, new_btn) # 真点「新档」→ _on_save_select_new → create_slot+open_camp
|
||||
for fi in 3:
|
||||
await get_tree().process_frame
|
||||
samples.append([main.ui.modals.menu.save_select_overlay.visible, main.ui.modals.camp.scene.visible, main.ui.screens.start_overlay.visible])
|
||||
@@ -239,7 +262,7 @@ func _ready() -> void:
|
||||
if load_btn == null:
|
||||
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
|
||||
else:
|
||||
load_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, load_btn) # 真点「进档」→ 激活槽2 开营地
|
||||
await get_tree().process_frame
|
||||
if main.save_manager.slots.active_slot() != 2:
|
||||
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
|
||||
@@ -254,7 +277,7 @@ func _ready() -> void:
|
||||
if del_btn == null:
|
||||
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
|
||||
else:
|
||||
del_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, del_btn) # 真点「删档」一击只开窗
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.menu.confirm_dialog.is_open():
|
||||
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
|
||||
@@ -265,7 +288,7 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] sl7 确认窗未经模态栈开(直开旁路——wr2 单口失守)")
|
||||
elif not main.ui.flow.locks_input():
|
||||
_problems.append("[savesel] sl7 确认窗开时输入锁未生效(锁域失守——wr2)")
|
||||
main.ui.modals.menu.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(cancelled 钮同源)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
|
||||
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
|
||||
@@ -273,9 +296,9 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
|
||||
if main.ui.flow.stack._occluded.has(main.ui.modals.menu.confirm_dialog):
|
||||
_problems.append("[savesel] sl7 取消后确认窗遮挡账未清(关侧未走模态栈——wr2)")
|
||||
del_btn.pressed.emit() # 再开窗走确认路径
|
||||
ProbeUiUtil.push_click(main, del_btn) # 真点再开窗走确认路径
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.menu.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」(confirmed 钮同源)
|
||||
await get_tree().process_frame
|
||||
if FileAccess.file_exists(csel_tpl % 2) or main.ui.modals.menu.confirm_dialog.is_open():
|
||||
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
|
||||
@@ -343,7 +366,7 @@ func _ready() -> void:
|
||||
if settings_close == null:
|
||||
_problems.append("设置面返回按钮缺失")
|
||||
else:
|
||||
settings_close.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, settings_close) # 真点「返回」收设置面
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.settings_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("设置面关闭后未恢复开始层")
|
||||
@@ -377,7 +400,7 @@ func _ready() -> void:
|
||||
if career_close == null:
|
||||
_problems.append("生涯页返回按钮缺失")
|
||||
else:
|
||||
career_close.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, career_close) # 真点「返回」收生涯页
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.menu.career_overlay.visible or not main.ui.screens.start_overlay.visible:
|
||||
_problems.append("生涯页关闭后未恢复开始层")
|
||||
@@ -463,7 +486,11 @@ func _ready() -> void:
|
||||
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
|
||||
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
|
||||
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
|
||||
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
|
||||
var shop_leave: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if shop_leave == null:
|
||||
_problems.append("商店「离开商店」钮缺失(真输入化驱动中断)")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, shop_leave) # 真点离店(leave_pressed 同源钮)放行选路
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.ui.modals.run.level_screen.visible:
|
||||
@@ -476,12 +503,13 @@ func _ready() -> void:
|
||||
if mpb == null or not mpb.visible or mpb.disabled:
|
||||
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
|
||||
else:
|
||||
mpb.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, mpb) # 真点地图屏角标开状态面板
|
||||
if not main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
|
||||
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
|
||||
await get_tree().process_frame # 面板当帧开——容器排版帧后钮 rect 才有效(真点击依赖排版)
|
||||
var rbtn2: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||||
if rbtn2 != null:
|
||||
rbtn2.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, rbtn2) # 真点「继续」关面板
|
||||
if main.ui.modals.run.status_panel.visible or not main.ui.modals.run.level_screen.visible:
|
||||
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
|
||||
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
|
||||
@@ -519,7 +547,8 @@ func _ready() -> void:
|
||||
if next_btn == null:
|
||||
_problems.append("大奖励屏确认按钮缺失")
|
||||
else:
|
||||
next_btn.pressed.emit()
|
||||
await get_tree().process_frame # 停点层当帧开——排版帧后真点击
|
||||
ProbeUiUtil.push_click(main, next_btn) # 真点「进入下一层」推进 L2(落点判定见下)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.runner == null or int(main.run_scope.runner.level_row.get("level_index", -1)) != 2:
|
||||
@@ -540,7 +569,7 @@ func _ready() -> void:
|
||||
if main.ui.modals.run.end_overlay.verdict_text() == "":
|
||||
_problems.append("胜利结算框判定行缺失")
|
||||
_settlement_confirms += 1
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「下一节点」(结算框已过帧排版)
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
if main.run_scope.settlement_stop_timer.is_armed() or not main.ui.modals.run.level_screen.visible:
|
||||
@@ -555,7 +584,7 @@ func _ready() -> void:
|
||||
# (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。
|
||||
var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running"
|
||||
if main.run_scope.is_adventure and _adv_running:
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
|
||||
continue
|
||||
if not main.ui.flow.locks_input():
|
||||
_problems.append("终局 overlay 打开时输入未锁定")
|
||||
@@ -612,7 +641,7 @@ func _ready() -> void:
|
||||
if main.ui.modals.run.end_overlay.is_open():
|
||||
# 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。
|
||||
if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running":
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 中途结算面真点「继续」推进
|
||||
else:
|
||||
break
|
||||
elif main.ui.modals.run.curio_overlay.visible:
|
||||
@@ -626,18 +655,20 @@ func _ready() -> void:
|
||||
elif main.ui.modals.run.node_overlay.visible:
|
||||
main.ui.modals.run.node_overlay.press_continue()
|
||||
elif main.ui.modals.menu.shop_overlay.visible:
|
||||
main.ui.modals.menu.shop_overlay.leave_pressed.emit()
|
||||
var shop_leave2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||||
if shop_leave2 != null:
|
||||
ProbeUiUtil.push_click(main, shop_leave2) # 真点离店(收口段同主循环分派)
|
||||
elif main.ui.screens.inter_overlay.visible:
|
||||
var next_btn2: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||||
if next_btn2 != null:
|
||||
next_btn2.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, next_btn2) # 真点「进入下一层」(收口段)
|
||||
elif main.ui.modals.run.level_screen.visible:
|
||||
var abandon_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.level_screen, "放弃", "prefix")
|
||||
if abandon_btn != null:
|
||||
_closed_via_abandon = true
|
||||
abandon_btn.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, abandon_btn) # 真点「放弃」一击
|
||||
await get_tree().process_frame
|
||||
abandon_btn.pressed.emit() # 二击确认(3s 窗内)
|
||||
ProbeUiUtil.push_click(main, abandon_btn) # 真点二击确认(3s 窗内)
|
||||
for i in 10:
|
||||
await get_tree().process_frame
|
||||
break
|
||||
@@ -665,7 +696,7 @@ func _ready() -> void:
|
||||
if main.ui.modals.run.end_overlay._btn == null:
|
||||
_problems.append("断点段:终局面返回按钮缺失")
|
||||
else:
|
||||
main.ui.modals.run.end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「返回」(_btn=继续/返回共用句柄)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.screens.start_overlay.visible:
|
||||
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
|
||||
@@ -693,7 +724,7 @@ func _ready() -> void:
|
||||
_problems.append("大门无断点:未确认即进冒险〔营地图批 V3〕")
|
||||
if main.ui.modals.menu.confirm_dialog._secondary_btn.visible:
|
||||
_problems.append("大门无断点:次钮应隐藏(两出口形态)〔营地图批 V3〕")
|
||||
main.ui.modals.menu.confirm_dialog.cancelled.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 真点「取消」(大门取消出口)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.scene.visible or main.run_scope.is_adventure or main._camp_data()["balance"] != bal_gate:
|
||||
_problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕")
|
||||
@@ -795,6 +826,7 @@ func _ready() -> void:
|
||||
exit_code = 1
|
||||
if not _problems.is_empty():
|
||||
exit_code = 1
|
||||
_recorder.dump_js() # 收尾落盘(大门段/开场行段点击在循环退出后——落盘必须在 quit 前)
|
||||
get_tree().quit(exit_code)
|
||||
|
||||
|
||||
@@ -821,7 +853,7 @@ func _run_modal_probes_once() -> void:
|
||||
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
|
||||
if not _tested_legend:
|
||||
_tested_legend = true
|
||||
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮开层
|
||||
if not main.ui.modals.run.legend_overlay.is_open():
|
||||
_problems.append("图例打开失败")
|
||||
if main.ui.modals.run.end_overlay.is_open() or main.ui.screens.start_overlay.visible:
|
||||
@@ -851,7 +883,7 @@ func _run_modal_probes_once() -> void:
|
||||
_problems.append("战斗屏状态角标缺失/未点亮/被置灰(P1 丁入口失守 visible=%s disabled=%s)"
|
||||
% [str(pbtn != null and pbtn.visible), str(pbtn != null and pbtn.disabled)])
|
||||
else:
|
||||
pbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _on_status_badge_pressed
|
||||
ProbeUiUtil.push_click(main, pbtn) # 真点状态角标 → 组合根 _on_status_badge_pressed
|
||||
if not main.ui.modals.run.status_panel.visible:
|
||||
_problems.append("状态面板未随角标打开(组合根接线失守)")
|
||||
if not main.ui.flow.locks_input():
|
||||
@@ -903,30 +935,32 @@ func _run_modal_probes_once() -> void:
|
||||
if resume_btn == null:
|
||||
_problems.append("状态面板关闭钮缺失(「继续」)")
|
||||
else:
|
||||
resume_btn.pressed.emit()
|
||||
await get_tree().process_frame # 面板当帧开——排版帧后真点击
|
||||
ProbeUiUtil.push_click(main, resume_btn) # 真点「继续」关面板恢复
|
||||
if main.ui.modals.run.status_panel.visible or main.ui.flow.locks_input():
|
||||
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
|
||||
% [str(main.ui.modals.run.status_panel.visible), str(main.ui.flow.locks_input())])
|
||||
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
|
||||
main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit()
|
||||
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住
|
||||
#(置灰态穿透判定保留 emit 直发——真输入到不了置灰钮,处理器级防线只能以〔接线断言〕钉住)
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_legend) # 真点图例钮复开
|
||||
if not main.ui.battle.board_zone.status_badge.disabled:
|
||||
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
|
||||
main.ui.battle.board_zone.status_badge.pressed.emit()
|
||||
main.ui.battle.board_zone.status_badge.pressed.emit() # 〔接线断言〕置灰态 emit 穿透——钉处理器兜底(真输入被 disabled 拦,测不到此层)
|
||||
if main.ui.modals.run.status_panel.visible:
|
||||
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
|
||||
main.ui.modals.run.legend_overlay.close()
|
||||
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
|
||||
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发(真输入 motion 进管线触发 entered/exited 沿)
|
||||
if not _tested_tooltip:
|
||||
var row = main.ui.battle.stage.hero_standee.slot_row # 状态 tooltip 载体(B 区站桩)
|
||||
for child in row.get_children():
|
||||
if child is PanelContainer and child.visible and child.has_meta("eff"):
|
||||
_tested_tooltip = true
|
||||
child.mouse_entered.emit()
|
||||
ProbeUiUtil.push_motion(main, (child as PanelContainer).get_global_rect().get_center())
|
||||
if not row._tip.visible:
|
||||
_problems.append("状态槽 tooltip 未显示")
|
||||
elif not row._tip_label.text.contains("来源") or not row._tip_label.text.contains("剩余"):
|
||||
_problems.append("状态槽 tooltip 缺来源/剩余(%s)" % row._tip_label.text.replace("\n", "|"))
|
||||
child.mouse_exited.emit()
|
||||
ProbeUiUtil.push_motion(main, Vector2(-40, -40)) # 移出一切控件(视口外沿触发 exited)
|
||||
if row._tip.visible:
|
||||
_problems.append("状态槽 tooltip 未随移出隐藏")
|
||||
break
|
||||
@@ -1027,8 +1061,8 @@ func _random_unknown_adjacent(minesweeper) -> Vector2i:
|
||||
|
||||
|
||||
func _click_stop() -> void:
|
||||
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||||
main.ui.battle.board_zone.ap_bar._btn_stop.pressed.emit()
|
||||
# 停手走真实点击路径(AP 条「停手」钮 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_stop)
|
||||
|
||||
|
||||
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。
|
||||
# receipt.json 是每跑批覆写的当次回执、ci-status.json/ci-history.jsonl 是已退役的 CI 回执判定与流水账(判定只在远端,本机残留)、
|
||||
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志——均为中间产物,不入库。
|
||||
# ci.lock/ci.rerun 是跑批件锁与补跑标记、*.log 是跑批当场日志、coverage/ 是探针真输入覆盖登记
|
||||
#(JSONP 式 .js,每次探针运行自动覆写——观测产物非事实源,不入库)——均为中间产物,不入库。
|
||||
receipt.json
|
||||
ci-status.json
|
||||
ci-history.jsonl
|
||||
ci.lock
|
||||
ci.rerun
|
||||
*.log
|
||||
coverage/
|
||||
Reference in new issue
Block a user