Files
gplan-2/game/sim/journey_player.gd

588 lines
26 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 单点通道)。
# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口);
# 棋盘格=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 # 刷新确认链一次性
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 = "节点层「继续」(方法-视图口)"
main.ui.modals.run.node_overlay.press_continue()
continue
if bool(st["三选一"]):
_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 = "地图候选命中(方法-地图命中口)"
main.ui.modals.run.level_screen.press_candidate(0)
func _drive_curio() -> void:
# 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面)
var co: Control = main.ui.modals.run.curio_overlay
_last_action = "三选一停点分发(方法-视图口)"
if co._continue_btn.visible:
co.press_continue()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
co.press_card(0)
co.press_confirm()
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty():
co.press_card(0)
co.press_confirm()
else:
co.press_continue()
func _drive_battle_once() -> void:
# 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口)
var settlement = main.run_scope.settlement
var diag: Dictionary = settlement.diagnostic_snapshot()
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)
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)
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])