614 lines
28 KiB
GDScript
614 lines
28 KiB
GDScript
# journey_player.gd — 全动线机器自玩驱动(C·发现环节去人化)
|
||
# 是什么:headless 场景驱动件——实例化 main、隔离存档,按旅程段驱动全动线(开机→设置→存档三操作
|
||
# 新档/删档/进档→营地功能窗逐窗开合→Esc 回退→大门开冒险→关卡推进→战斗→各停点→终局收束→回菜单),
|
||
# 每屏到达记账+段间异常上报,收尾落 JSONP 报告并入探针覆盖登记(真输入事件同源记账)。
|
||
# 判据(契约 §5):每个屏到达过+段间零 fatal。
|
||
# 异常上报三类(其余静默):①诊断收集器 fatal 级(settlement battle_result=fatal_error/fatal 屏)
|
||
# ②段间超时(单段 >30s 未达预期停点;战斗段另设状态无变化挂死阈值)③屏状态与预期不符。
|
||
# RID 泄漏等已知日志噪音静默。
|
||
# 归因字段:每条上报机器自填——段名/期望屏/实测屏/上一动作/通道标签(真实输入-按钮/
|
||
# 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。
|
||
# 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹
|
||
# 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。
|
||
# 驱动纪律:按钮/场景物件/地图候选全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道;
|
||
# 地图候选=LevelMapScreen.candidate_rect 命中矩形中心 push_click_at——方法口已清零)。
|
||
# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)=余下唯一方法口。
|
||
# 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。
|
||
# 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn
|
||
extends Node
|
||
|
||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")
|
||
const ProbeCoverageRecorder := preload("res://probe/probe_coverage_recorder.gd")
|
||
const AutoPlayer := preload("res://sim/auto_player.gd")
|
||
|
||
const SEGMENT_TIMEOUT_MS := 30000 # 段间超时判据(契约 §5——单段 30s 未达预期停点)
|
||
const BATTLE_STALL_FRAMES := 90 # 战斗段状态无变化挂死阈值(演播期豁免同探针口径)
|
||
const ADVENTURE_BUDGET_FRAMES := 60000 # 冒险段帧预算(全停点循环兜底)
|
||
const MISFIRE_PATH := "res://tests/.gates/coverage/journey_misfire_ledger.json"
|
||
const REPORT_PATH := "res://tests/.gates/coverage/journey_report.js"
|
||
|
||
var main: Node
|
||
var _recorder: RefCounted = null
|
||
var _problems: Array = [] # 异常上报(三类;指纹二次起静默不入此列)
|
||
var _silent_count := 0 # 误报静默笔数(回执可见)
|
||
var _screens_seen: Dictionary = {} # 屏到达记账(判据面)
|
||
var _screen_trace: Array = [] # 屏到达变迁轨迹(诊断面——新屏首现时点+上一动作)
|
||
var _misfire: Dictionary = {} # 指纹 → 次数(跨跑落盘)
|
||
var _last_state := {} # 战斗段状态哈希(挂死检测)
|
||
var _stall := 0
|
||
var _last_action := "(无)" # 归因初判:上一动作描述
|
||
var _map_status_done := false # 地图屏状态角标一次性
|
||
var _legend_done := false # 图例开合一次性(每战斗面一次)
|
||
var _status_done := false # 战斗屏状态面板一次性
|
||
var _refresh_done := false # 刷新确认链一次性
|
||
var _stop_done := false # 停手行为一次性(每战至多一次——挖掘期确定性触发)
|
||
var _stop_settlement: Variant = null # 停手基线战局(换战局重置一次性)
|
||
var _stop_round := -1 # 停手轮次基线(轮换重置已花计数)
|
||
var _digs_this_round := 0 # 本轮已提交挖掘数(停手条件——AP 已花 ≥1)
|
||
|
||
|
||
func _ready() -> void:
|
||
await get_tree().process_frame
|
||
main = load("res://presentation/main.tscn").instantiate()
|
||
add_child(main)
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
_load_misfire()
|
||
_recorder = ProbeCoverageRecorder.new()
|
||
_recorder.attach(main, "journey")
|
||
ProbeUiUtil.isolate_save(main, "journey_player", false, false, true)
|
||
main.save_manager.slots.attach_slot_template("user://journey_player_slot_%d.json")
|
||
for i in range(1, 4): # 夹具清场——陈旧槽文件会翻有档态、新档钮缺席(open_camp_via_savesel 同款坑注)
|
||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||
var sp: String = main.save_manager.slots.slot_path(i) + suf
|
||
if FileAccess.file_exists(sp):
|
||
DirAccess.remove_absolute(sp)
|
||
main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成(探针先例——夹具注)
|
||
main.ui.modals.menu.debug_overlay.seed_edit.text = "42"
|
||
|
||
await _phase_expect("开机落菜单", ["菜单"])
|
||
await _phase_menu_settings()
|
||
await _phase_new_slot(1)
|
||
await _phase_camp_windows()
|
||
await _phase_camp_esc_return()
|
||
await _phase_new_and_delete(2)
|
||
await _phase_load_slot(1)
|
||
await _phase_gate_start()
|
||
await _phase_adventure()
|
||
await _phase_end_back_menu()
|
||
|
||
_recorder.dump_js()
|
||
_dump_report()
|
||
_save_misfire()
|
||
var fatal := _fatal_count()
|
||
if _problems.is_empty() and fatal == 0 and _screens_seen.has("菜单") and _screens_seen.has("终局"):
|
||
print("[journey] PASS — 全动线走查完成(屏到达 %d 类,异常上报 0,fatal 0,误报静默 %d)" % [
|
||
_screens_seen.size(), _silent_count])
|
||
get_tree().quit(0)
|
||
return
|
||
print("[journey] FAIL — 异常 %d(静默 %d)/ fatal %d / 屏到达 %d 类" % [
|
||
_problems.size(), _silent_count, fatal, _screens_seen.size()])
|
||
for p in _problems:
|
||
print("[journey] - " + JSON.stringify(p, " "))
|
||
get_tree().quit(1)
|
||
|
||
|
||
# ---- 屏状态读取与记账 ----
|
||
|
||
func _screen_state() -> Dictionary:
|
||
var st := {
|
||
"菜单": main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible,
|
||
"选档": main.ui.modals.menu.save_select_overlay.visible,
|
||
"设置": main.ui.modals.menu.settings_overlay.visible,
|
||
"生涯": main.ui.modals.menu.career_overlay.visible,
|
||
"确认弹窗": main.ui.modals.menu.confirm_dialog.is_open(),
|
||
"营地": main.ui.modals.camp.scene.visible,
|
||
"锻造窗": main.ui.modals.camp.forge.visible,
|
||
"树窗": main.ui.modals.camp.tree.visible,
|
||
"秘宝窗": main.ui.modals.camp.treasure.visible,
|
||
"图鉴窗": main.ui.modals.camp.codex.visible,
|
||
"Esc浮窗": main.ui.modals.camp.esc_overlay.visible,
|
||
"冒险": main.run_scope != null and main.run_scope.is_adventure,
|
||
"关卡屏": main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible,
|
||
"三选一": main.ui.modals.run.curio_overlay.visible,
|
||
"商店": main.ui.modals.menu.shop_overlay.visible,
|
||
"大奖励": main.ui.screens.inter_overlay != null and main.ui.screens.inter_overlay.visible,
|
||
"节点": main.ui.modals.run.node_overlay.visible,
|
||
"终局": main.ui.modals.run.end_overlay != null and main.ui.modals.run.end_overlay.is_open(),
|
||
}
|
||
var in_battle: bool = bool(st["冒险"]) and not bool(st["关卡屏"]) and not bool(st["终局"]) \
|
||
and main.run_scope != null and main.run_scope.settlement != null \
|
||
and String(main.run_scope.settlement.battle_result) == ""
|
||
st["战斗"] = in_battle
|
||
return st
|
||
|
||
|
||
func _record_screens() -> void:
|
||
var st := _screen_state()
|
||
for k in st:
|
||
if bool(st[k]) and not _screens_seen.has(k):
|
||
_screens_seen[k] = true
|
||
_screen_trace.append({"t": Time.get_datetime_string_from_system(), "screen": k, "last_action": _last_action})
|
||
_check_exclusion(st)
|
||
|
||
|
||
var _excl_reported: Dictionary = {} # 屏互斥违约对(本跑一次性上报——跨跑由误报指纹静默)
|
||
|
||
# 屏级互斥(探针同款判据对):合法叠加仅「终局盖冒险」;违约=③屏状态与预期不符
|
||
func _check_exclusion(st: Dictionary) -> void:
|
||
var pairs := [["菜单", "营地"], ["菜单", "冒险"], ["营地", "冒险"], ["终局", "营地"], ["终局", "菜单"], ["菜单", "战斗"]]
|
||
for pair in pairs:
|
||
if bool(st.get(pair[0], false)) and bool(st.get(pair[1], false)):
|
||
var fk: String = str(pair[0]) + "×" + str(pair[1])
|
||
if not _excl_reported.has(fk):
|
||
_excl_reported[fk] = true
|
||
_report("screen_mismatch", "屏级互斥", "%s 与 %s 不应同场" % [pair[0], pair[1]])
|
||
|
||
|
||
func _active_screens() -> Array:
|
||
var on: Array = []
|
||
var st := _screen_state()
|
||
for k in st:
|
||
if bool(st[k]):
|
||
on.append(k)
|
||
return on
|
||
|
||
|
||
func _fatal_count() -> int:
|
||
var n := 0
|
||
if main.run_scope != null and main.run_scope.settlement != null \
|
||
and String(main.run_scope.settlement.battle_result) == "fatal_error":
|
||
n += 1
|
||
return n
|
||
|
||
|
||
# ---- 异常上报(三类+归因+误报收敛) ----
|
||
|
||
func _report(kind: String, seg: String, expect: String) -> void:
|
||
var finger_src := "%s|%s|%s|%s" % [kind, seg, expect, ",".join(_active_screens())]
|
||
var finger := str(finger_src.hash())
|
||
_misfire[finger] = int(_misfire.get(finger, 0)) + 1
|
||
if int(_misfire[finger]) >= 2:
|
||
_silent_count += 1 # 同一误报源第 2 次自动登记静默(契约 §5 收敛口径)
|
||
return
|
||
_problems.append({
|
||
"kind": kind, "segment": seg, "expect": expect,
|
||
"actual_screens": _active_screens(), "last_action": _last_action,
|
||
"fatal": _fatal_count(), "at": Time.get_datetime_string_from_system(),
|
||
})
|
||
|
||
|
||
func _load_misfire() -> void:
|
||
if FileAccess.file_exists(MISFIRE_PATH):
|
||
var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(MISFIRE_PATH))
|
||
if parsed is Dictionary:
|
||
_misfire = parsed
|
||
|
||
|
||
func _save_misfire() -> void:
|
||
var f := FileAccess.open(MISFIRE_PATH, FileAccess.WRITE)
|
||
if f != null:
|
||
f.store_string(JSON.stringify(_misfire, " "))
|
||
f.close()
|
||
|
||
|
||
# ---- 段间等待(超时判据承载) ----
|
||
|
||
func _frames(n: int = 2) -> void:
|
||
for _i in n:
|
||
await get_tree().process_frame
|
||
_record_screens()
|
||
|
||
|
||
# 期望屏集合在超时内逐帧到达;到达即记账返回。超时=异常②超时+③屏状态不符双记(同源指纹静默收敛)。
|
||
func _phase_expect(seg: String, expect_keys: Array) -> void:
|
||
_last_action = "等待段「%s」落点(期望 %s)" % [seg, ",".join(expect_keys)]
|
||
var t0 := Time.get_ticks_msec()
|
||
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
|
||
await get_tree().process_frame
|
||
_record_screens()
|
||
var st := _screen_state()
|
||
var all_on := true
|
||
for k in expect_keys:
|
||
all_on = all_on and bool(st.get(k, false))
|
||
if all_on:
|
||
await _frames(2) # 开层排版帧——当帧可见时容器未排,紧随的控件真点击会落空(探针件同款坑)
|
||
return
|
||
_report("timeout", seg, "屏 %s 应在场" % ",".join(expect_keys))
|
||
_report("screen_mismatch", seg, "屏 %s 应在场" % ",".join(expect_keys))
|
||
_save_misfire()
|
||
|
||
|
||
# ---- 旅程段 ----
|
||
|
||
func _phase_menu_settings() -> void:
|
||
# 设置开合(开始层真实入口——功能界面全覆盖义务)
|
||
var btn: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "设置", "prefix")
|
||
if btn == null:
|
||
_report("screen_mismatch", "设置入口", "开始层「设置」钮在场")
|
||
return
|
||
_last_action = "真点「设置」钮"
|
||
ProbeUiUtil.push_click(main, btn)
|
||
await _phase_expect("设置开", ["设置"])
|
||
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix")
|
||
if back != null:
|
||
_last_action = "真点设置「返回」钮"
|
||
ProbeUiUtil.push_click(main, back)
|
||
await _phase_expect("设置收", ["菜单"])
|
||
|
||
|
||
func _phase_new_slot(slot: int) -> void:
|
||
# 开机→存档·新档(选档层真实入口:进入存档→槽位新档→落营地)
|
||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||
if esb == null:
|
||
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
|
||
return
|
||
_last_action = "真点「进入存档」钮"
|
||
ProbeUiUtil.push_click(main, esb)
|
||
await _phase_expect("选档层开", ["选档"])
|
||
var nb: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(slot)
|
||
if nb == null:
|
||
_report("screen_mismatch", "新档槽%d" % slot, "槽%d 新档钮在场" % slot)
|
||
return
|
||
_last_action = "真点槽%d「新档」钮" % slot
|
||
ProbeUiUtil.push_click(main, nb)
|
||
await _phase_expect("新档落营地", ["营地"])
|
||
|
||
|
||
func _phase_load_slot(slot: int) -> void:
|
||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||
if esb == null:
|
||
_report("screen_mismatch", "选档入口", "开始层「进入存档」钮在场")
|
||
return
|
||
_last_action = "真点「进入存档」钮(进档链)"
|
||
ProbeUiUtil.push_click(main, esb)
|
||
await _phase_expect("选档层开", ["选档"])
|
||
var lb: Button = main.ui.modals.menu.save_select_overlay._load_btns.get(slot)
|
||
if lb == null:
|
||
_report("screen_mismatch", "进档槽%d" % slot, "槽%d 进档钮在场" % slot)
|
||
return
|
||
_last_action = "真点槽%d「进档」钮" % slot
|
||
ProbeUiUtil.push_click(main, lb)
|
||
await _phase_expect("进档落营地", ["营地"])
|
||
|
||
|
||
func _phase_new_and_delete(slot: int) -> void:
|
||
# 新档→篝火回菜单→选档删档(二次确认双路径真点击)——存档三操作收口
|
||
await _phase_new_slot(slot)
|
||
var camp: Control = main.ui.modals.camp.scene
|
||
_last_action = "真点篝火热点(回退动线)"
|
||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"campfire").get_center())
|
||
await _phase_expect("篝火确认弹窗", ["确认弹窗"])
|
||
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||
if ok != null:
|
||
_last_action = "真点篝火确认「确认」钮"
|
||
ProbeUiUtil.push_click(main, ok)
|
||
await _phase_expect("回菜单", ["菜单"])
|
||
# 删档:进选档→槽位删档一击开窗→真点取消→再删→真点确认(文件三态走查归探针既有套件——C 只验到达)
|
||
var esb: Button = ProbeUiUtil.find_button(main.ui.screens.start_overlay, "进入存档", "prefix")
|
||
if esb != null:
|
||
_last_action = "真点「进入存档」钮(删档链)"
|
||
ProbeUiUtil.push_click(main, esb)
|
||
await _phase_expect("选档层开", ["选档"])
|
||
var db: Button = main.ui.modals.menu.save_select_overlay._delete_btns.get(slot)
|
||
if db != null:
|
||
_last_action = "真点槽%d「删档」钮一击" % slot
|
||
ProbeUiUtil.push_click(main, db)
|
||
await _phase_expect("删档确认窗", ["确认弹窗"])
|
||
var cc: Button = main.ui.modals.menu.confirm_dialog._cancel_btn
|
||
if cc != null:
|
||
_last_action = "真点删档「取消」钮"
|
||
ProbeUiUtil.push_click(main, cc)
|
||
await _frames(2)
|
||
_last_action = "真点槽%d「删档」钮二击" % slot
|
||
ProbeUiUtil.push_click(main, db)
|
||
await _phase_expect("删档确认窗", ["确认弹窗"])
|
||
var ok2: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||
if ok2 != null:
|
||
_last_action = "真点删档「确认」钮"
|
||
ProbeUiUtil.push_click(main, ok2)
|
||
await _frames(3)
|
||
var back: Button = ProbeUiUtil.find_button(main.ui.modals.menu.save_select_overlay, "返回", "prefix")
|
||
if back != null:
|
||
_last_action = "真点选档「返回」钮"
|
||
ProbeUiUtil.push_click(main, back)
|
||
await _phase_expect("选档收", ["菜单"])
|
||
|
||
|
||
func _phase_camp_windows() -> void:
|
||
# 营地功能窗逐窗真点开合(锻造/秘宝/树/图鉴/生涯——campfire/gate 归动线段)
|
||
var views := {
|
||
&"forge": ["锻造窗", main.ui.modals.camp.forge],
|
||
&"chest": ["秘宝窗", main.ui.modals.camp.treasure],
|
||
&"dummy": ["树窗", main.ui.modals.camp.tree],
|
||
&"board": ["图鉴窗", main.ui.modals.camp.codex],
|
||
&"notebook": ["生涯", main.ui.modals.menu.career_overlay],
|
||
}
|
||
for key: StringName in [&"forge", &"chest", &"dummy", &"board", &"notebook"]:
|
||
var camp: Control = main.ui.modals.camp.scene
|
||
_last_action = "真点营地物件热点 %s" % String(key)
|
||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(key).get_center())
|
||
await _phase_expect("%s窗开" % String(key), [views[key][0]])
|
||
var back: Button = ProbeUiUtil.find_button(views[key][1], "返回营地")
|
||
if back == null:
|
||
back = ProbeUiUtil.find_button(views[key][1], "返回", "prefix")
|
||
if back != null:
|
||
_last_action = "真点 %s 窗返回钮" % String(key)
|
||
ProbeUiUtil.push_click(main, back)
|
||
await _frames(3)
|
||
|
||
|
||
func _phase_camp_esc_return() -> void:
|
||
# 营地 Esc 浮窗→「返回主菜单」真点(回退动线+浮窗功能面到达)
|
||
_push_esc()
|
||
await _phase_expect("Esc浮窗开", ["Esc浮窗"])
|
||
var rb: Button = main.ui.modals.camp.esc_overlay._return_btn
|
||
if rb != null:
|
||
_last_action = "真点浮窗「返回主菜单」钮"
|
||
ProbeUiUtil.push_click(main, rb)
|
||
await _phase_expect("浮窗回菜单", ["菜单"])
|
||
|
||
|
||
func _phase_gate_start() -> void:
|
||
# 大门确认→开冒险(动线收口——确认后落点=冒险,非回菜单)
|
||
var camp: Control = main.ui.modals.camp.scene
|
||
_last_action = "真点大门热点"
|
||
ProbeUiUtil.push_click_at(main, camp.get_hotspot_rect(&"gate").get_center())
|
||
await _phase_expect("大门确认弹窗", ["确认弹窗"])
|
||
var ok: Button = main.ui.modals.menu.confirm_dialog._confirm_btn
|
||
if ok != null:
|
||
_last_action = "真点大门「确认」钮"
|
||
ProbeUiUtil.push_click(main, ok)
|
||
await _phase_expect("冒险发起", ["冒险"])
|
||
# 已知缺陷现场(发现环义务已尽):大门开局后开始层滞留盖屏=「开局被开始层盖住」
|
||
# (营地交互修复批承接中、合回前 master 在案)——开始层可见直入 locks_input 谓词,战斗全程锁死。
|
||
# C 上报后以夹具面隐藏开始层继续走查(通道=夹具修整,非真输入;缺陷本体不因此掩盖)。
|
||
await _frames(2)
|
||
if main.ui.screens.start_overlay != null and main.ui.screens.start_overlay.visible:
|
||
_report("screen_mismatch", "大门开局", "开始层滞留盖屏(已知缺陷——营地交互修复批承接中)")
|
||
_last_action = "夹具修整:隐藏滞留开始层以解锁战斗走查(缺陷已上报在案)"
|
||
main.ui.screens.start_overlay.visible = false
|
||
await _frames(2)
|
||
|
||
|
||
func _phase_adventure() -> void:
|
||
# 关卡推进段:入口选路→战斗→各停点分发→真终局收口(帧预算兜底)
|
||
var budget := ADVENTURE_BUDGET_FRAMES
|
||
while budget > 0:
|
||
budget -= 1
|
||
await get_tree().process_frame
|
||
_record_screens()
|
||
var st := _screen_state()
|
||
if bool(st["节点"]):
|
||
_last_action = "真点节点层「继续」钮"
|
||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||
continue
|
||
if bool(st["三选一"]):
|
||
await _drive_curio()
|
||
continue
|
||
if bool(st["商店"]):
|
||
var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店")
|
||
if lv != null:
|
||
_last_action = "真点「离开商店」钮"
|
||
ProbeUiUtil.push_click(main, lv)
|
||
continue
|
||
if bool(st["大奖励"]):
|
||
var nb: Button = ProbeUiUtil.find_button(main.ui.screens.inter_overlay, "进入下一层", "prefix")
|
||
if nb != null:
|
||
_last_action = "真点大奖励「进入下一层」钮"
|
||
ProbeUiUtil.push_click(main, nb)
|
||
await _frames(2)
|
||
continue
|
||
if bool(st["关卡屏"]):
|
||
_drive_map_once()
|
||
continue
|
||
if bool(st["战斗"]):
|
||
await _drive_battle_once()
|
||
continue
|
||
var endo: Control = main.ui.modals.run.end_overlay
|
||
if endo != null and endo.is_open():
|
||
var armed: bool = main.run_scope.settlement_stop_timer.is_armed()
|
||
var running: bool = main.run_scope.is_adventure and main.run_scope.adventure != null \
|
||
and String(main.run_scope.adventure.state) == "running"
|
||
if armed or running:
|
||
_last_action = "真点结算面「继续/下一节点」钮"
|
||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn)
|
||
await _frames(2)
|
||
continue
|
||
break # 真终局(失败/放弃/打穿)——收口段交棒
|
||
if budget <= 0:
|
||
_last_action = "冒险段超时(诊断:%s)" % JSON.stringify({
|
||
"pacer_pending": main.ui.screens.row_pacer.pending_rows().size(),
|
||
"locked": main.ui.flow.locks_input(),
|
||
"battle_result": String(main.run_scope.settlement.battle_result) if main.run_scope.settlement != null else "?",
|
||
"level": int(main.run_scope.runner.level_row.get("level_index", -1)) if main.run_scope.runner != null else -1,
|
||
"round": int(main.run_scope.settlement.round_index) if main.run_scope.settlement != null else -1,
|
||
"ap": int(main.run_scope.settlement.action_economy.ap) if main.run_scope.settlement != null else -1,
|
||
})
|
||
_report("timeout", "冒险段", "帧预算耗尽未达真终局")
|
||
_save_misfire()
|
||
|
||
|
||
func _drive_map_once() -> void:
|
||
# 地图屏:状态角标真点开合一次+候选命中(方法-地图命中口——自绘布局路径,探针族同款入口)
|
||
if not _map_status_done and main.ui.modals.run.level_screen.status_badge != null \
|
||
and not main.ui.modals.run.level_screen.status_badge.disabled:
|
||
_map_status_done = true
|
||
var badge: Button = main.ui.modals.run.level_screen.status_badge
|
||
_last_action = "真点地图屏状态角标"
|
||
ProbeUiUtil.push_click(main, badge)
|
||
await _frames(3) # 面板开层排版帧——同帧点「继续」会落空、输入锁滞留全战役(实证)
|
||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||
ProbeUiUtil.push_click(main, resumed)
|
||
_last_action = "地图候选命中(真实输入-命中矩形)"
|
||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center())
|
||
|
||
|
||
func _drive_curio() -> void:
|
||
# 三选一停点:待选态真点首卡+确认(gui_input 点选——确认钮现形当帧容器重排在途,排版帧后再点);
|
||
# 兑付/已消费态真点继续
|
||
var co: Control = main.ui.modals.run.curio_overlay
|
||
_last_action = "三选一停点分发(真输入)"
|
||
if co._continue_btn.visible:
|
||
ProbeUiUtil.push_click(main, co._continue_btn)
|
||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||
ProbeUiUtil.push_click_at(main, (co._bcards[0] as Control).get_global_rect().get_center())
|
||
await _frames(2)
|
||
ProbeUiUtil.push_click(main, co._confirm_btn)
|
||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
|
||
ProbeUiUtil.push_click_at(main, (co._bcards[0] as Control).get_global_rect().get_center())
|
||
await _frames(2)
|
||
ProbeUiUtil.push_click(main, co._confirm_btn)
|
||
else:
|
||
ProbeUiUtil.push_click(main, co._continue_btn)
|
||
|
||
|
||
func _drive_battle_once() -> void:
|
||
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
|
||
var settlement = main.run_scope.settlement
|
||
var diag: Dictionary = settlement.diagnostic_snapshot()
|
||
if settlement != _stop_settlement: # 换战局——停手一次性与轮内计数重置
|
||
_stop_settlement = settlement
|
||
_stop_done = false
|
||
_stop_round = -1
|
||
_digs_this_round = 0
|
||
var round_now := int(diag.round)
|
||
if round_now != _stop_round:
|
||
_stop_round = round_now
|
||
_digs_this_round = 0
|
||
var st := {
|
||
"round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap,
|
||
"revealed": diag.minesweeper_revealed,
|
||
}
|
||
var pacer_idle: bool = main.ui.screens.row_pacer.pending_rows().is_empty()
|
||
if JSON.stringify(st) == JSON.stringify(_last_state) and pacer_idle \
|
||
and not main.ui.flow.locks_input():
|
||
_stall += 1
|
||
if _stall >= BATTLE_STALL_FRAMES:
|
||
_report("timeout", "战斗段", "状态无变化达 %d 帧" % BATTLE_STALL_FRAMES)
|
||
_stall = 0
|
||
_save_misfire()
|
||
_last_state = {} # 重置基线防同帧连报
|
||
else:
|
||
_stall = 0
|
||
_last_state = st
|
||
if not pacer_idle or main.ui.flow.locks_input():
|
||
return
|
||
var ap_bar: Control = main.ui.battle.board_zone.ap_bar
|
||
# 功能钮真点(一次性——图例开合/状态面板开合/刷新确认链)
|
||
if not _legend_done:
|
||
_legend_done = true
|
||
_last_action = "真点图例钮"
|
||
ProbeUiUtil.push_click(main, ap_bar._btn_legend)
|
||
await _frames(2)
|
||
var lclose: Button = ProbeUiUtil.find_button(main.ui.modals.run.legend_overlay, "关闭")
|
||
if lclose != null and main.ui.modals.run.legend_overlay.is_open():
|
||
ProbeUiUtil.push_click(main, lclose)
|
||
await _frames(2)
|
||
return
|
||
if not _status_done and not main.ui.battle.board_zone.status_badge.disabled:
|
||
_status_done = true
|
||
_last_action = "真点战斗屏状态角标"
|
||
ProbeUiUtil.push_click(main, main.ui.battle.board_zone.status_badge)
|
||
await _frames(3) # 面板开层排版帧(同地图屏实证坑)
|
||
var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix")
|
||
if resumed != null and main.ui.modals.run.status_panel.visible:
|
||
ProbeUiUtil.push_click(main, resumed)
|
||
await _frames(2)
|
||
return
|
||
if not _refresh_done and ap_bar.is_refresh_ready():
|
||
_refresh_done = true
|
||
_last_action = "真点「弃图刷新」钮+确认链"
|
||
ProbeUiUtil.push_click(main, ap_bar._btn_refresh)
|
||
await _frames(2)
|
||
var cbtn: Button = ProbeUiUtil.find_button(ap_bar, "确认弃图")
|
||
if cbtn != null:
|
||
ProbeUiUtil.push_click(main, cbtn)
|
||
await _frames(2)
|
||
return
|
||
# 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入)
|
||
var minesweeper = settlement.minesweeper
|
||
var plan: Dictionary = AutoPlayer.choose_dig(minesweeper)
|
||
# 停手行为(每战至多一次——挖掘期确定性触发,无随机占比;触发时点微调在代拍板白名单):
|
||
# 相位 Active+停手钮使能+AP 已花 ≥1+盘面有挖可走 → 真点停手钮收轮(真输入行为之一)
|
||
if not _stop_done and String(diag.action_economy.phase) == "Active" \
|
||
and not ap_bar._btn_stop.disabled and _digs_this_round >= 1 and not plan.is_empty():
|
||
_stop_done = true
|
||
_last_action = "真点「停手」钮(挖掘期定型化触发)"
|
||
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
|
||
await _frames(2)
|
||
return
|
||
if plan.is_empty():
|
||
plan = AutoPlayer.gamble_pick(minesweeper)
|
||
if plan.is_empty():
|
||
_last_action = "真点「停手」钮(无计划)"
|
||
ProbeUiUtil.push_click(main, ap_bar._btn_stop)
|
||
await _frames(2)
|
||
return
|
||
_last_action = "棋盘挖格 %s(方法-棋盘口)" % str(plan.dig_cell)
|
||
if plan.hero_cell != minesweeper.hero_pos():
|
||
main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||
main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
|
||
_digs_this_round += 1
|
||
await _frames(1)
|
||
|
||
|
||
func _phase_end_back_menu() -> void:
|
||
# 终局收束:真点「返回」→ 回菜单;结算框/中途面已在冒险段分发
|
||
var t0 := Time.get_ticks_msec()
|
||
while Time.get_ticks_msec() - t0 < SEGMENT_TIMEOUT_MS:
|
||
await get_tree().process_frame
|
||
_record_screens()
|
||
var endo: Control = main.ui.modals.run.end_overlay
|
||
if endo != null and endo.is_open() and not main.run_scope.settlement_stop_timer.is_armed():
|
||
break
|
||
if main.ui.modals.run.end_overlay == null or not main.ui.modals.run.end_overlay.is_open():
|
||
_report("timeout", "终局收束", "真终局面未在超时内到达")
|
||
_save_misfire()
|
||
return
|
||
var btn: Button = main.ui.modals.run.end_overlay._btn
|
||
if btn != null:
|
||
_last_action = "真点终局「返回」钮"
|
||
ProbeUiUtil.push_click(main, btn)
|
||
await _phase_expect("终局回菜单", ["菜单"])
|
||
|
||
|
||
func _push_esc() -> void:
|
||
var ev := InputEventKey.new()
|
||
ev.keycode = KEY_ESCAPE
|
||
ev.physical_keycode = KEY_ESCAPE
|
||
ev.pressed = true
|
||
main.get_viewport().push_input(ev, true) # 键盘真输入(通道=真实输入-键盘)
|
||
|
||
|
||
# ---- 报告落盘(JSONP) ----
|
||
|
||
func _dump_report() -> void:
|
||
var dir := DirAccess.open("res://")
|
||
if dir != null:
|
||
dir.make_dir_recursive("tests/.gates/coverage")
|
||
var payload := {
|
||
"meta": {
|
||
"generated_at": Time.get_datetime_string_from_system(),
|
||
"seed": OS.get_environment("PROBE_SEED"),
|
||
"screens_seen_total": _screens_seen.size(),
|
||
"anomalies_reported": _problems.size(),
|
||
"anomalies_silent": _silent_count,
|
||
"fatal_count": _fatal_count(),
|
||
"anomaly_classes": "fatal/timeout/screen_mismatch(其余静默;误报指纹二次起静默)",
|
||
},
|
||
"screens_seen": _screens_seen.keys(),
|
||
"screen_trace": _screen_trace,
|
||
"anomalies": _problems,
|
||
}
|
||
var f := FileAccess.open(REPORT_PATH, FileAccess.WRITE)
|
||
if f != null:
|
||
var js := "/* 全动线自玩报告——journey_player.gd 随跑覆写;误报核销归用户复核轮。 */\n"
|
||
js += "window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {};\n"
|
||
js += "window.__QA_COVERAGE__.journey = %s;\n" % JSON.stringify(payload, " ")
|
||
f.store_string(js)
|
||
f.close()
|
||
print("[journey] 报告已落盘 %s(屏到达 %d 类 / 上报 %d / 静默 %d)" % [
|
||
REPORT_PATH, _screens_seen.size(), _problems.size(), _silent_count])
|