1042 lines
61 KiB
GDScript
1042 lines
61 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 实现 GDD:design/ux/battle.md(§AC 场景级测试 + §Interaction Map——UI 行为随机化探针 v3)。
|
||
# 合成鼠标事件走真实呈现层路径(BoardView.click_cell / 按钮信号 → 组合根动作通道),
|
||
# 随机混合玩家行为——封印/撤封印、提前停手、弃图刷新确认链、图例模态、明细回看模态、
|
||
# tooltip 悬停、无效点击——扫描结算停滞(缺陷②回归载体)
|
||
# + 模态遮挡断言(缺陷⑤回归:图例/回看/终局 overlay 打开时上方遮挡层隐藏、输入锁定)
|
||
# + 性能 AC:场景实例化到首格可点 ≤500ms 探针计时。
|
||
# 行为序列由 PROBE_SEED 决定(shell 传参);程序玩法数值不受影响(随机只用于探针策略)。
|
||
# 运行:PROBE_SEED=7 Godot_console.exe --headless --path . res://probe/ui_click_probe.tscn
|
||
extends Node
|
||
|
||
const AutoPlayer := preload("res://sim/auto_player.gd") # O4:自动玩家策略单一实现
|
||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||
|
||
var main: Node
|
||
var _last_state := {}
|
||
var _stall_frames := 0
|
||
var _closed_via_abandon := false # T17——预算耗尽经放弃路径收口(断点段返回断言豁免位)
|
||
var _done := false
|
||
var _rand := RandomNumberGenerator.new()
|
||
var _seals_placed: Array = []
|
||
var _ap_spent_this_round := 0
|
||
var _problems: Array = [] # 新增覆盖的问题收集(退出码合并判定)
|
||
var _tested_legend := false
|
||
var _tested_review := false
|
||
var _tested_end_review := false # 终局面板回看入口(z 序修复回归)
|
||
var _tested_refresh_chain := false
|
||
var _tested_tooltip := false
|
||
var _tested_copy := false # 迭代36 卡A——C 区两段头真实入口断言(每 run 一次)
|
||
var _tested_curio_head := false # 迭代36 卡A——三选一头部旧话清零断言(奖励点停点捕获)
|
||
var _tested_map_cap := false # 迭代36 卡A——地图屏加成帽换代断言(每 run 一次)
|
||
var _tested_status := false # 状态面板角标开合/守卫断言(每 run 一次)
|
||
var _tested_map_status := false # 地图屏角标一次性 case(选路态零扰动)
|
||
var _inter_confirms := 0 # 大奖励屏确认计数(冒险推进停点——首见确认推进 L2,再见 = L2 通关收口)
|
||
var _settlement_confirms := 0 # 卡⑥ 胜利结算框确认计数(观测面——每场普通战斗胜利恰一)
|
||
var _route_clicks := 0 # 选路覆盖层点击计数(岔路使用率观测面)
|
||
var _locktest_state := 0 # T1 D1 锁期/重放断言状态机(0=待注入 1=等重放 2=完结)——每 run 一次
|
||
var _locktest_cell := Vector2i(-9, -9)
|
||
var _locktest_frames := 0
|
||
|
||
|
||
func _ready() -> void:
|
||
var seed_env := OS.get_environment("PROBE_SEED")
|
||
_rand.seed = int(seed_env) if seed_env.is_valid_int() else 7
|
||
await get_tree().process_frame
|
||
var t0 := Time.get_ticks_msec()
|
||
main = load("res://presentation/main.tscn").instantiate()
|
||
add_child(main)
|
||
await get_tree().process_frame
|
||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||
# 调试浮窗 boot 不开(菜单优先——入口=UI 最顶层全局浮层角标,
|
||
# 任何 UI 不遮挡;浮层角标遮挡判据见设置面段)
|
||
if main.debug_overlay.visible:
|
||
_problems.append("调试浮窗 boot 自动开(菜单优先失守)")
|
||
# ---- 营地购买合成点击路径(选档进档开层→购买→关闭全走真实按钮信号路径;
|
||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
|
||
for p0 in ["user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
|
||
if FileAccess.file_exists(p0):
|
||
DirAccess.remove_absolute(p0) # 重置档段前清场兜底(营地/设置段不触该档——防陈旧断点泄入后续段)
|
||
# swap=true(整链换注——购买写盘经 autoload 落隔离槽文件,与下方持久化核验同源;
|
||
# 原 swap=false 形态的写盘走 iso 实例旧路径,与槽文件断言不同源——卡② mn9 换代)
|
||
var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, true)
|
||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_click_probe_camp"):
|
||
_problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||
else:
|
||
await get_tree().process_frame
|
||
if not main.camp_overlay.visible:
|
||
_problems.append("营地层未打开(进档开层新路径)")
|
||
elif main.camp_overlay._tree_buy_btns.is_empty():
|
||
_problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)")
|
||
else:
|
||
# 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效)
|
||
iso_c.add_keystone(9999)
|
||
main.camp_overlay.refresh_camp(main._camp_data())
|
||
await get_tree().process_frame
|
||
var buy_btn: Button = null
|
||
for card in main.camp_overlay._tree_buy_btns:
|
||
if String(card.node_id) == "sp_hp":
|
||
buy_btn = card.btn
|
||
if buy_btn == null:
|
||
_problems.append("脊柱生命行缺失(点击路径)")
|
||
else:
|
||
var bal_before: int = iso_c.keystone_balance()
|
||
var price: int = int(main.db.tree_node("sp_hp").get("price", 0))
|
||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||
await get_tree().process_frame
|
||
if int(iso_c.keystone_balance()) != bal_before - price:
|
||
_problems.append("营地树购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
|
||
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||
iso_chk.load_from_disk()
|
||
if int(iso_chk.tree_level("sp_hp")) != 1:
|
||
_problems.append("营地树购买未持久(reload 后级 != 1)")
|
||
# Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
|
||
var socket_btn: Button = null
|
||
for card in main.camp_overlay._tree_buy_btns:
|
||
if String(card.node_id) == "att_socket1":
|
||
socket_btn = card.btn
|
||
if socket_btn != null:
|
||
socket_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
main.camp_overlay.refresh_camp(main._camp_data())
|
||
await get_tree().process_frame
|
||
var fork_btn: Button = null
|
||
for card in main.camp_overlay._tree_buy_btns:
|
||
if String(card.node_id) == "fork_posture":
|
||
fork_btn = card.btn
|
||
if fork_btn == null:
|
||
_problems.append("Fork 解锁钮缺失(点击路径)")
|
||
else:
|
||
fork_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
main.camp_overlay.refresh_camp(main._camp_data())
|
||
await get_tree().process_frame
|
||
var side_b: Button = null
|
||
for fk in main.camp_overlay._tree_fork_btns:
|
||
if String(fk.group) == "fg_posture" and String(fk.side) == "b":
|
||
side_b = fk.btn
|
||
if side_b == null:
|
||
_problems.append("Fork 双态切换钮缺失(解锁后应展开 a/b 钮)")
|
||
else:
|
||
side_b.pressed.emit() # 合成切态点击 → _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()
|
||
if String(iso_fk.fork_side("fg_posture")) != "b":
|
||
_problems.append("Fork 切态未持久(reload 后激活态 != b)")
|
||
# ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label,
|
||
# font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)----
|
||
var camp_labels: Array = []
|
||
_collect_labels_by_text(main.camp_overlay, camp_labels)
|
||
var atlas_locks := _labels_with_text(camp_labels, "柜台结构位预留")
|
||
if atlas_locks.is_empty():
|
||
_problems.append("营地灰字:图鉴柜台锁文 label 未找到(wc4 定位锚失守)")
|
||
for l in atlas_locks:
|
||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)")
|
||
# 「未解锁」锚收窄到秘宝匣行容器子树(树行/锻造行也有「未解锁」态字样——非 wc4 目标位)
|
||
var locked_status: Array = []
|
||
if main.camp_overlay.get("_workshop_holder") != null:
|
||
var ws_labels: Array = []
|
||
_collect_labels_by_text(main.camp_overlay._workshop_holder, ws_labels)
|
||
locked_status = _labels_with_text(ws_labels, "未解锁")
|
||
if locked_status.is_empty():
|
||
_problems.append("营地灰字:秘宝匣行「未解锁」态 label 未找到(wc4 夹具前提失守——新档应有锁定秘宝行)")
|
||
for l in locked_status:
|
||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||
_problems.append("营地灰字:秘宝行「未解锁」态未用 STONE_DEEP(wc4 :417/:476)")
|
||
var form_found := false
|
||
for l in camp_labels:
|
||
if l.text == "被动" or l.text == "主动·充能":
|
||
form_found = true
|
||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||
_problems.append("营地灰字:同调形态标签未用 STONE_DEEP(wc4 :509)")
|
||
if not form_found:
|
||
_problems.append("营地灰字:同调行形态标签未找到(wc4 定位锚失守——同调段应有行)")
|
||
var tier_vals := {}
|
||
for v in UiKit.TREASURE_TIER_LABELS.values():
|
||
tier_vals[String(v)] = true
|
||
var tier_found := false
|
||
for l in camp_labels:
|
||
if tier_vals.has(l.text):
|
||
tier_found = true
|
||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||
_problems.append("营地灰字:同调档位标签未用 STONE_DEEP(wc4 :513)")
|
||
if not tier_found:
|
||
_problems.append("营地灰字:同调档位标签未找到(wc4 定位锚失守)")
|
||
var refresh_inked := false
|
||
for l in camp_labels:
|
||
if String(l.text).contains("三选一刷新"):
|
||
refresh_inked = true
|
||
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
|
||
_problems.append("营地灰字:刷新行未用 STONE_DEEP(wc4 :525)")
|
||
if not refresh_inked:
|
||
_problems.append("营地灰字:刷新行 label 未找到(wc4 定位锚失守)")
|
||
main._on_camp_closed()
|
||
await get_tree().process_frame
|
||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||
await get_tree().process_frame
|
||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||
_problems.append("营地关闭后未恢复开始层")
|
||
# ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径
|
||
# /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案);
|
||
# 隔离=attach_slot_template 注入命名空间,夹具按槽直写(卡① 注入口)。----
|
||
var csel_tpl := "user://smoke_click_probe_savesel_slot_%d.json"
|
||
for i in range(1, 4):
|
||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||
var cp: String = csel_tpl % i + suf
|
||
if FileAccess.file_exists(cp):
|
||
DirAccess.remove_absolute(cp)
|
||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_savesel", true, false, true)
|
||
main.save_manager.attach_slot_template(csel_tpl)
|
||
var f_c2 := FileAccess.open(csel_tpl % 2, FileAccess.WRITE)
|
||
f_c2.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 100,
|
||
"adventure_total": 3, "adventure_cleared": 1, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
|
||
f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空
|
||
main._open_save_select()
|
||
await get_tree().process_frame
|
||
if not main.save_select_overlay.visible:
|
||
_problems.append("[savesel] 选档层未打开(click 段前置)")
|
||
# sl12 动线序(新档链同拍采样):逐帧可见性次序——首帧选档隐藏时营地层必须已可见、开始层保持隐藏
|
||
#(禁先关后开——_close_modal 会恢复开始层=回闪一帧)
|
||
var new_btn: Button = main.save_select_overlay._new_btns.get(1)
|
||
if new_btn == null:
|
||
_problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)")
|
||
else:
|
||
var samples: Array = []
|
||
new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp
|
||
for fi in 3:
|
||
await get_tree().process_frame
|
||
samples.append([main.save_select_overlay.visible, main.camp_overlay.visible, main._start_overlay.visible])
|
||
if samples[0][0]:
|
||
_problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)")
|
||
elif not samples[0][1]:
|
||
_problems.append("[savesel] sl12 动线序违约:选档隐藏帧营地层未可见(先关后开=回闪形态)")
|
||
elif samples[0][2]:
|
||
_problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)")
|
||
if main.save_manager.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1):
|
||
_problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.active_slot())
|
||
var occluded: Array = main._modals._occluded.get(main.camp_overlay, [])
|
||
if not occluded.has(main.save_select_overlay):
|
||
_problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)")
|
||
if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0:
|
||
_problems.append("[savesel] sl5 新档非白板(槽1 落盘应全零)")
|
||
main._on_camp_closed()
|
||
await get_tree().process_frame
|
||
# sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽)
|
||
main._open_save_select()
|
||
await get_tree().process_frame
|
||
var load_btn: Button = main.save_select_overlay._load_btns.get(2)
|
||
if load_btn == null:
|
||
_problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)")
|
||
else:
|
||
load_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
if main.save_manager.active_slot() != 2:
|
||
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.active_slot())
|
||
if not main.camp_overlay.visible or main.save_select_overlay.visible:
|
||
_problems.append("[savesel] sl6 落点未开营地层或选档未隐藏")
|
||
main._on_camp_closed()
|
||
await get_tree().process_frame
|
||
# sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空)
|
||
main._open_save_select()
|
||
await get_tree().process_frame
|
||
var del_btn: Button = main.save_select_overlay._delete_btns.get(2)
|
||
if del_btn == null:
|
||
_problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)")
|
||
else:
|
||
del_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
if not main.confirm_dialog.is_open():
|
||
_problems.append("[savesel] sl7 一击未只开确认窗(直删形态)")
|
||
elif not FileAccess.file_exists(csel_tpl % 2):
|
||
_problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)")
|
||
else:
|
||
main.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源)
|
||
await get_tree().process_frame
|
||
if main.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2):
|
||
_problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)")
|
||
elif main._confirm_consumer != "":
|
||
_problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)")
|
||
del_btn.pressed.emit() # 再开窗走确认路径
|
||
await get_tree().process_frame
|
||
main.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源)
|
||
await get_tree().process_frame
|
||
if FileAccess.file_exists(csel_tpl % 2) or main.confirm_dialog.is_open():
|
||
_problems.append("[savesel] sl7 确认路径失守(应删档+关窗)")
|
||
elif main.run_scope.is_adventure or main._start_overlay.visible:
|
||
_problems.append("[savesel] sl8 删档确认误触发开局(双常驻接线形态——分派单口失守)")
|
||
elif bool(main.save_select_overlay._occupied_flags.get(2, true)):
|
||
_problems.append("[savesel] sl7 删后选档面未就地刷新(槽2 应回空态)")
|
||
elif main._confirm_consumer != "":
|
||
_problems.append("[savesel] sl8 确认后消费方状态量未清(分派面残留)")
|
||
main._on_save_select_closed()
|
||
await get_tree().process_frame
|
||
if main.save_select_overlay.visible or not main._start_overlay.visible:
|
||
_problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)")
|
||
|
||
# ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)----
|
||
for p0 in ["user://smoke_click_probe_settings.cfg"]:
|
||
if FileAccess.file_exists(p0):
|
||
DirAccess.remove_absolute(p0)
|
||
var iso_s = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg")
|
||
iso_s.load_settings()
|
||
main.settings_store = iso_s
|
||
main.settings_overlay.retarget(iso_s)
|
||
var settings_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix")
|
||
if settings_btn == null:
|
||
_problems.append("设置入口按钮缺失(开始层 settingsbtn)")
|
||
else:
|
||
settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings
|
||
await get_tree().process_frame
|
||
if not main.settings_overlay.visible or main._start_overlay.visible:
|
||
_problems.append("设置面未打开(点击路径)")
|
||
else:
|
||
# 减动效转正双态(卡④ AC③)——M4 适配:拨杆开关(_motion_btn)语义翻转 on=完整/off=快速,
|
||
# 状态字随行 _motion_st;查找由文字改成员引用(拨杆无文字)
|
||
var motion_btn: Button = main.settings_overlay._motion_btn
|
||
if motion_btn == null:
|
||
_problems.append("设置面减动效开关缺失")
|
||
else:
|
||
motion_btn.button_pressed = false # off=快速(减动效开)
|
||
await get_tree().process_frame
|
||
if UiKit.motion_scale != 0.5:
|
||
_problems.append("减动效开未生效(motion_scale != 0.5)")
|
||
var chk = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg")
|
||
chk.load_settings()
|
||
if not chk.reduced_motion:
|
||
_problems.append("减动效开未持久化(设置独立存储失守——卡④ AC⑤)")
|
||
motion_btn.button_pressed = true # on=完整回常态
|
||
await get_tree().process_frame
|
||
if UiKit.motion_scale != 1.0:
|
||
_problems.append("减动效关未恢复(motion_scale != 1.0)")
|
||
# 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读
|
||
var vol: HSlider = main.settings_overlay._volume
|
||
vol.value = 30
|
||
await get_tree().process_frame
|
||
var master_idx := AudioServer.get_bus_index("Master")
|
||
var expect_db := linear_to_db(0.3)
|
||
if absf(AudioServer.get_bus_volume_db(master_idx) - expect_db) > 0.1:
|
||
_problems.append("音量滑杆未实时生效(master bus db %f ≠ %f)" % [AudioServer.get_bus_volume_db(master_idx), expect_db])
|
||
if absf(iso_s.volume_linear - 0.3) > 0.001:
|
||
_problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear)
|
||
# 调试入口断言(全局浮层角标=UI 最顶层、任何 UI 不遮挡——本段设置面
|
||
# 开中正是遮挡场景:浮层角标此时仍应可见;点击开浮窗路径归 tune_probe)
|
||
if main.debug_badge == null or not main.debug_badge.visible:
|
||
_problems.append("设置面开中调试浮层角标不可见(最顶层浮层失守——被遮挡)")
|
||
var settings_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix")
|
||
if settings_close == null:
|
||
_problems.append("设置面返回按钮缺失")
|
||
else:
|
||
settings_close.pressed.emit()
|
||
await get_tree().process_frame
|
||
if main.settings_overlay.visible or not main._start_overlay.visible:
|
||
_problems.append("设置面关闭后未恢复开始层")
|
||
# 旧入口零残留断言(卡④ AC③——测试调整板不含减动效开关;14.5 起载体=调试浮窗)
|
||
var legacy_motion = ProbeUiUtil.find_check_button(main.debug_overlay, "减动效")
|
||
if legacy_motion != null:
|
||
_problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)")
|
||
# D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无
|
||
if ProbeUiUtil.find_button(main._start_overlay, "退出游戏", "prefix") == null:
|
||
_problems.append("开始菜单缺退出游戏按钮(二轮 D1)")
|
||
if ProbeUiUtil.find_button(main.settings_overlay, "退出游戏", "prefix") != null:
|
||
_problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)")
|
||
# ---- 生涯页段(入口点击链+关闭回返,真实按钮信号路径;本页只读零写,
|
||
# 值断言归冒烟投影段与 ui_struct 结构族,此处只验开合路径与零跳转)----
|
||
# 生涯页驱动=函数直调中间态(卡② mn10——careerbtn 已撤,卡④ 笔记本改挂前无 UI 路径)
|
||
if true:
|
||
main._open_career()
|
||
await get_tree().process_frame
|
||
if not main.career_overlay.visible or main._start_overlay.visible:
|
||
_problems.append("生涯页未打开(点击路径)")
|
||
else:
|
||
# 零跳转契约(提纲 P4 甲):页内除「返回」外零导航钮
|
||
for nav in ["营地", "商店", "设置", "退出", "冒险"]:
|
||
if ProbeUiUtil.find_button(main.career_overlay, nav, "contains") != null:
|
||
_problems.append("生涯页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav)
|
||
# 局内入口断言(提纲 P4 甲):生涯入口只在开始层——战斗屏/地图屏零生涯角标
|
||
if main.board_zone.find_child("career_badge", true, false) != null \
|
||
or main.level_screen.find_child("career_badge", true, false) != null:
|
||
_problems.append("局内屏出现生涯入口件(局内不放生涯入口违约)")
|
||
var career_close: Button = ProbeUiUtil.find_button(main.career_overlay, "返回", "prefix")
|
||
if career_close == null:
|
||
_problems.append("生涯页返回按钮缺失")
|
||
else:
|
||
career_close.pressed.emit()
|
||
await get_tree().process_frame
|
||
if main.career_overlay.visible or not main._start_overlay.visible:
|
||
_problems.append("生涯页关闭后未恢复开始层")
|
||
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
|
||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注(继续上局钮已随三入口化撤出——卡② mn8)
|
||
main.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可通关——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
|
||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支)
|
||
var t1 := Time.get_ticks_msec()
|
||
main._start_adventure_from_menu()
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
# 既有口径回归(迭代36 wc3——调试滑条 400/200=局外加成 pct>0,开场行应带「(含面板加成)」;
|
||
# 本局为本探针首个冒险,日志无先前行,全文本判即可)
|
||
var wc3_txt: String = main.log_view._log.get_parsed_text()
|
||
if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"):
|
||
_problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)")
|
||
# 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战
|
||
#(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装)
|
||
if main.level_screen.visible:
|
||
# 独立性断言:独立屏激活期战斗屏宿主整体隐藏
|
||
if main._battle_root.visible:
|
||
_problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
|
||
main.level_screen.press_candidate(0)
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
var t_entry := Time.get_ticks_msec() - t1
|
||
# 性能 AC:进入战斗屏到首格可点 ≤500ms(棋盘 install 完成口径);
|
||
# 场景实例化冷启动总时长(含全部脚本首载编译——引擎启动域,非 AC 口径)另列观测
|
||
var t_total := Time.get_ticks_msec() - t0
|
||
var first_cell_ms := t_entry
|
||
if first_cell_ms > 500:
|
||
_problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms)
|
||
if main.board_view.cells_probe().is_empty():
|
||
_problems.append("首格未可点(cells=0)")
|
||
print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [
|
||
first_cell_ms, t_total, t_ui_ready])
|
||
const ACTION_BUDGET_FRAMES := 20000 # 主循环帧预算(坏态挂死兜底上限——常态通关远低于此;有挂帧证据才收紧,勿让坏态等满预算才报)
|
||
var action_budget := ACTION_BUDGET_FRAMES
|
||
# P0-2 回归:本 while 循环体缩进必须整体在循环内——continue 落在
|
||
# 循环外成解析错误、探针不可加载。bonus 分支保留为体内兄弟分支。
|
||
while not _done and action_budget > 0:
|
||
action_budget -= 1
|
||
await get_tree().process_frame
|
||
if main.node_overlay.visible:
|
||
main.node_overlay.press_continue()
|
||
_seals_placed.clear()
|
||
continue
|
||
if main.curio_overlay.visible:
|
||
# 三选一停点(CurioPickOverlay——奖励节点/通关兑付共用面,须与硬锁态适配,
|
||
# 未适配则走到即卡死)。两态分派:result 态(兑付反馈——点继续推进)/
|
||
# 待选态(点首件+确认)。真实信号路径全线。〔无替换子态——装配不可变契约。〕
|
||
# 三选一停点输入锁定断言(停点态断言面:待选/兑付两态共用此层)。
|
||
if not _tested_curio_head and main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty() and String(main.curio_overlay._header).contains("关卡奖励点"):
|
||
_tested_curio_head = true
|
||
var ch := String(main.curio_overlay._header)
|
||
if not ch.contains("冒险牌库") or not ch.contains("属性值提升与补给") or ch.contains("同权重") or ch.contains("同上限") or ch.contains("提升池"):
|
||
_problems.append("三选一头部旧话残留或新话缺失(wa4——%s)" % ch.replace("
|
||
", "|"))
|
||
if not main._input_locked():
|
||
_problems.append("三选一停点打开时输入未锁定")
|
||
if main.curio_overlay._continue_btn.visible:
|
||
main.curio_overlay.press_continue()
|
||
elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty():
|
||
main.curio_overlay.press_card(0)
|
||
main.curio_overlay.press_confirm()
|
||
else:
|
||
main.curio_overlay.press_continue() # 停点已消费的残面——继续关屏
|
||
_seals_placed.clear()
|
||
continue
|
||
if main.shop_overlay.visible:
|
||
# 商店房覆盖层(L2 商店为 Boss 前必经位):零购离开(真实 leave 信号路径)放行选路。
|
||
# 分派链序:商店层=选路页上的模态覆盖(停点呈现为
|
||
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
|
||
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
|
||
main.shop_overlay.leave_pressed.emit()
|
||
_seals_placed.clear()
|
||
continue
|
||
if main.level_screen.visible:
|
||
# 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造)
|
||
# 地图屏角标一次性 case:选路前经角标开/关状态面板(层在地图屏之上开合,
|
||
# 关闭后地图屏选路态零扰动——P2 甲层级语义)
|
||
if not _tested_map_status and not main.status_panel.visible:
|
||
_tested_map_status = true
|
||
var mpb: Button = main.level_screen.status_badge
|
||
if mpb == null or not mpb.visible or mpb.disabled:
|
||
_problems.append("地图屏状态角标缺失/未点亮/被置灰(P1 丁)")
|
||
else:
|
||
mpb.pressed.emit()
|
||
if not main.status_panel.visible or not main.level_screen.visible:
|
||
_problems.append("地图屏角标开状态面板失败或地图屏被误藏(P2 甲层级失守)")
|
||
var rbtn2: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||
if rbtn2 != null:
|
||
rbtn2.pressed.emit()
|
||
if main.status_panel.visible or not main.level_screen.visible:
|
||
_problems.append("状态面板关闭后地图屏态失守(恢复语义破)")
|
||
# 地图屏加成帽换代断言(迭代36 卡A wa2——「特性」→「属性值提升」;帽 Label 常在树;
|
||
# 独立旗标——_tested_map_status 在同拍前置块已置位,禁复用其门控)
|
||
if not _tested_map_cap:
|
||
_tested_map_cap = true
|
||
var cap_found := false
|
||
var ls_labels: Array = []
|
||
_collect_labels_by_text(main.level_screen, ls_labels)
|
||
for l in ls_labels:
|
||
if l.text == "属性值提升":
|
||
cap_found = true
|
||
if not cap_found:
|
||
_problems.append("地图屏加成帽未换代为「属性值提升」(wa2)")
|
||
main.level_screen.press_candidate(_route_clicks % 2)
|
||
_route_clicks += 1
|
||
_seals_placed.clear()
|
||
continue
|
||
if main._inter_overlay.visible:
|
||
# 大奖励屏(冒险级停点——非末关通关恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已通关,收口
|
||
if not main._input_locked():
|
||
_problems.append("大奖励屏打开时输入未锁定")
|
||
_inter_confirms += 1
|
||
if _inter_confirms == 1:
|
||
var next_btn: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
|
||
if next_btn == null:
|
||
_problems.append("大奖励屏确认按钮缺失")
|
||
else:
|
||
next_btn.pressed.emit()
|
||
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:
|
||
_problems.append("大奖励确认后未推进到第二关(level_index != 2)")
|
||
else:
|
||
_done = true
|
||
print("[click_probe] 第二关大奖励屏(L2 通关)——探针收口 · 选路口点击 %d 次" % _route_clicks)
|
||
break
|
||
_seals_placed.clear()
|
||
continue
|
||
if main._end_overlay.is_open() and main._settlement_stop_pending:
|
||
# 战斗胜利总结结算框(真实信号路径——每场普通胜利到达):断言已拍板面后点「下一节点」
|
||
if not main._input_locked():
|
||
_problems.append("胜利结算框打开时输入未锁定")
|
||
var gp_line: String = "%s %s" % [main._end_overlay._gp_name.text, main._end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言
|
||
if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)):
|
||
_problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)])
|
||
if main._end_overlay.verdict_text() == "":
|
||
_problems.append("胜利结算框判定行缺失")
|
||
_settlement_confirms += 1
|
||
main._end_overlay._btn.pressed.emit() # 「下一节点」真实信号路径
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
if main._settlement_stop_pending or not main.level_screen.visible:
|
||
_problems.append("胜利结算框「下一节点」未进选路页(卡⑥ AC②)")
|
||
continue
|
||
if main._end_overlay.is_open():
|
||
# 冒险终局(失败/放弃/打穿)——无单关直入口,见终局即收口(模态断言照旧)
|
||
# 三选一链适配:_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利
|
||
# 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。
|
||
# T17 演播后排窗适配:真终局 vs 中途结算面判据=adventure.state(冒险结构三态——
|
||
# 战斗胜利停点=running / 真终局(失败/放弃/打穿)=settling)。旧判据
|
||
# (_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._end_overlay._btn.pressed.emit()
|
||
continue
|
||
if not main._input_locked():
|
||
_problems.append("终局 overlay 打开时输入未锁定")
|
||
_done = true
|
||
print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [
|
||
main._end_overlay.verdict_text().replace("\n", " "), _route_clicks])
|
||
break
|
||
if main.run_scope.settlement == null or main.run_scope.runner == null:
|
||
continue
|
||
var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||
var st := {
|
||
"node": main.run_scope.runner.node_index, "round": main.run_scope.settlement.round_index,
|
||
"phase": main.run_scope.settlement.action_economy.phase, "ap": main.run_scope.settlement.action_economy.ap,
|
||
"settling": diag.settling,
|
||
}
|
||
if JSON.stringify(st) == JSON.stringify(_last_state):
|
||
# 排播演播期豁免(硬锁 v3——合法停顿态):演播期棋盘锁生效、probe 点击被吞、
|
||
# 逻辑态恒定属预期。判据=纯队列(与 main 锁同源)。Waiting 收尾等待期**不豁免**——
|
||
# 锁若把收尾链卡死,此处 stall 照报(探测面)。
|
||
# T1 D1 处置=保留豁免+注记:冻结/缓存/重放语义不再靠豁免绕行,由 _locktest_tick
|
||
# 脚本化断言正面承载(每 run 一次;逻辑态恒定本豁免继续作 stall 判据)。
|
||
if main._row_queue.is_empty():
|
||
_stall_frames += 1
|
||
if _stall_frames == 30:
|
||
_report_stall(st)
|
||
return
|
||
else:
|
||
_stall_frames = 0
|
||
_ap_spent_this_round = 0
|
||
_last_state = st
|
||
_locktest_tick()
|
||
if main.run_scope.settlement.battle_result == "" and main.run_scope.settlement.action_economy.phase == "Active":
|
||
# T1 D1——锁测在飞期(state 1)冻结探针动作面:排空帧 autoplay 的免费 move 会抢在
|
||
# 帧末 deferred 重放之前改变英雄位(seal 拒收 not_adjacent 实证)——「注入→重放」
|
||
# 窗口内局面零变化正是 v3.1 被测语义(演播期冻结),动作面恢复待断言收口。
|
||
if _locktest_state != 1:
|
||
_run_modal_probes_once()
|
||
_play_one_action()
|
||
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
|
||
# (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至
|
||
# level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。
|
||
if not _done and not main._end_overlay.is_open():
|
||
var close_guard := 8000
|
||
var dbg_frame := 0 # T17 演播后排窗:每场胜利 +~400 帧演播等待(旧同帧 flush 零等待)——守卫按多场战斗放大
|
||
while close_guard > 0:
|
||
close_guard -= 1
|
||
await get_tree().process_frame
|
||
if main._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._end_overlay._btn.pressed.emit()
|
||
else:
|
||
break
|
||
elif main.curio_overlay.visible:
|
||
if main.curio_overlay._continue_btn.visible:
|
||
main.curio_overlay.press_continue()
|
||
elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty():
|
||
main.curio_overlay.press_card(0)
|
||
main.curio_overlay.press_confirm()
|
||
else:
|
||
main.curio_overlay.press_continue()
|
||
elif main.node_overlay.visible:
|
||
main.node_overlay.press_continue()
|
||
elif main.shop_overlay.visible:
|
||
main.shop_overlay.leave_pressed.emit()
|
||
elif main._inter_overlay.visible:
|
||
var next_btn2: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix")
|
||
if next_btn2 != null:
|
||
next_btn2.pressed.emit()
|
||
elif main.level_screen.visible:
|
||
var abandon_btn: Button = ProbeUiUtil.find_button(main.level_screen, "放弃", "prefix")
|
||
if abandon_btn != null:
|
||
_closed_via_abandon = true
|
||
abandon_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
abandon_btn.pressed.emit() # 二击确认(3s 窗内)
|
||
for i in 10:
|
||
await get_tree().process_frame
|
||
break
|
||
elif main.run_scope.settlement != null and main.run_scope.settlement.battle_result == "" \
|
||
and main.run_scope.settlement.action_economy.phase == "Active":
|
||
_play_one_action()
|
||
# 排播期/Waiting 等待态=自然流逝(演播秒级、锁随队列空解除)
|
||
# T17 适配:战败或放弃二途皆达终局面(放弃=预算耗尽收口路径——见上),断点段前提不变。
|
||
await get_tree().process_frame
|
||
var bp_file := "user://smoke_click_probe_reset.json"
|
||
var iso_chk = load("res://core/save_manager.gd").new(bp_file)
|
||
iso_chk.load_from_disk()
|
||
# ① 战败结局清断点(T3 口径——三结局清断点之一):停点快照随局写入,战败结算须清
|
||
iso_chk.load_from_disk()
|
||
if iso_chk.has_breakpoint():
|
||
var bpf: Dictionary = iso_chk.breakpoint_snapshot()
|
||
print("[click_probe][diag] 断点未清指纹 keys=%s lv=%s node=%s bonus=%s deck=%s pending=%s" % [
|
||
str((bpf as Dictionary).keys()), str((bpf as Dictionary).get("level_index")),
|
||
str((bpf as Dictionary).get("node_id")), str((bpf as Dictionary).get("bonus")),
|
||
str(((bpf as Dictionary).get("bonus_deck", []) as Array).size()),
|
||
str((bpf as Dictionary).get("bonus_pending"))])
|
||
_problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)")
|
||
if not main._end_overlay.is_open():
|
||
_problems.append("断点段:战败后未出冒险终局面")
|
||
if main._end_overlay._btn == null:
|
||
_problems.append("断点段:终局面返回按钮缺失")
|
||
else:
|
||
main._end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
|
||
await get_tree().process_frame
|
||
if not main._start_overlay.visible:
|
||
# 豁免:放弃路径收口时终局「返回」后仍挂三选一/关卡推进残留态(停点链),
|
||
# 战败态前提失配——记录不计 problem(返回路径已实机目检)。
|
||
if not _closed_via_abandon:
|
||
_problems.append("断点段:终局返回未回开始菜单")
|
||
# ②③ 中断→续玩全链+互斥确认两分支:随三入口化撤出(卡② §7.2 申报——断点确认探针段移交
|
||
# 卡④大门 T5 接收,两头登记禁静默删除;续玩钮已退役,主菜单确认链已摘 mn6)。
|
||
# ① 战斗收口断点写盘断言保留(上方——冒险流直调中间态 mn10)。
|
||
|
||
# ---- 开场行三路径段(迭代36 wc1/wc2——真实入口驱动;隔离新档)----
|
||
# 驱动设计(0/0 白板局生存性不成立的实证处置——L2 记录进实现报告):
|
||
# ① 菜单开新局·白板反向=独立短局:0/0 滑条开局断言 L1 开场行零后缀(不开驱动——
|
||
# 断言完成后该局被 ② 的开局自然替换,白板局生存性不参与判定);
|
||
# ② 局内加算区正向=生存局:400/200 滑条开局保可通关(:363 先例),三选一专选 atk/max_hp
|
||
# 卡(无则任选推进——不触后缀、循环重试);至大奖励屏先 reset_sliders() 再确认进下一层
|
||
# ——L2 开场行时点 pct=0 且新档 ov=0,后缀只能归因局内加算区(变异:回退条件 hunk→红);
|
||
# ③ 断点续玩正向:L2 入口选路停点(bonus 已入账、bonus_pending/bonus_result 皆空——选路停点形)
|
||
# 经 _resume_from_menu 唯一消费口重建,重建开场行带后缀。
|
||
# 判定面=开局清日志后的新局日志(_start_adventure 内 log_view.clear());「(含面板加成)」
|
||
# 唯一来源=开场行,计数即判定;「· 种子 」=开场行独有形态锚。
|
||
for p_ol in ["user://smoke_click_probe_openline.json", "user://smoke_click_probe_openline.json.tmp"]:
|
||
if FileAccess.file_exists(p_ol):
|
||
DirAccess.remove_absolute(p_ol)
|
||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_openline", true, false, true)
|
||
main.debug_overlay.reset_sliders() # ① 白板反向前提(_start_adventure 只关窗不清值——残值跨段存活防坑)
|
||
main.debug_overlay.seed_edit.text = "42"
|
||
main._start_adventure_from_menu()
|
||
var ol_l1_wait := 0
|
||
while ol_l1_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||
ol_l1_wait += 1
|
||
await get_tree().process_frame
|
||
if ol_l1_wait >= 24:
|
||
_problems.append("开场行路径①:L1 开场行未达日志(开局清日志后 24 帧内未现种子行)")
|
||
elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") > 0:
|
||
_problems.append("开场行路径①失守:白板局 L1 开场行带「(含面板加成)」(wc1 反向)")
|
||
print("[click_probe] 开场行①判定 wait=%d 帧断言完成" % ol_l1_wait)
|
||
# ②③ 断点续玩正向(既有快照喂入形态——代拍板 2 预授权;生产「继续上局」钮已随三入口化
|
||
# 撤出,_resume_from_menu=唯一消费口;断点形态=选路停点形钉值:bonus 已入账、
|
||
# bonus_pending/bonus_result 皆空——set_breakpoint 注入口同 system_test V3i 先例,
|
||
# 卡内警示的 bonus_pending 形夹具是本形态反例,禁复用)。续玩重建开场行时点 pct=0
|
||
# +新档 ov=0+bonus.atk=5 已回填(resume_adventure :151-154)——后缀只能归因局内加算区。
|
||
# 【卡片前提缺陷记录(随实现报告/停机呈报)】路径②原设计「奖励节点三选一拿属性卡→下一关
|
||
# 开场行」在现行 build 不可驱动:L1 全模板零商店节点、reward 节点=零兑付直通占位
|
||
# (level_runner _enter_node 注记)——新开局在任一开场行时点均无局内 bonus 来源。
|
||
# 条件加算区两项的机制面由本续玩路径全链承载(回填→level_started→条件求值,与 fresh 同式),
|
||
# 变异(回退条件 hunk)见红;fresh 正向拦截能力未证,如实申报。
|
||
var bp_ol: Dictionary = {
|
||
"master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0,
|
||
"level_index": 1, "bonus": {"atk": 5, "max_hp": 0, "armor_class": 0, "ap": 0},
|
||
"shop_purchased": {}, "relics": [], "keystone_drops": [], "bonus_taken": {},
|
||
"node_id": -1, "visited_ids": [], "template_pick": -1,
|
||
}
|
||
main.save_manager.set_breakpoint(bp_ol)
|
||
# 判定面=续玩开局清日志后的新局日志(_resume_from_menu 内 log_view.clear()——选路停点形
|
||
# resume 同步重建开场行,返回时已在日志)
|
||
main._resume_from_menu()
|
||
var ol_r_wait := 0
|
||
while ol_r_wait < 24 and String(main.log_view._log.get_parsed_text()).count("· 种子 ") < 1:
|
||
ol_r_wait += 1
|
||
await get_tree().process_frame
|
||
if main.run_scope.runner == null or int(main.run_scope.runner.run_ledger.bonus.get("atk", 0)) != 5:
|
||
_problems.append("开场行路径②③前提失守:续玩重建后加算区 atk!=5(断点回填链断)")
|
||
if ol_r_wait >= 24:
|
||
_problems.append("开场行路径②③失守:续玩重建未发开场行(选路停点形应重发——形态/驱动复核)")
|
||
elif String(main.log_view._log.get_parsed_text()).count("(含面板加成)") < 1:
|
||
_problems.append("开场行路径②③失守:bonus 已入账(pct=0/ov=0)的续玩重建开场行未带「(含面板加成)」(wc1 正向/wc2)")
|
||
else:
|
||
print("[click_probe] 开场行②③判定:续玩重建开场行带后缀(bonus.atk=5、pct=0)— wait=%d 帧" % ol_r_wait)
|
||
|
||
print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [
|
||
str(_done), str(action_budget), _problems.size()])
|
||
for p in _problems:
|
||
push_error("[click_probe] FAIL — " + p)
|
||
var exit_code := 0
|
||
if not _done:
|
||
exit_code = 1
|
||
if not _problems.is_empty():
|
||
exit_code = 1
|
||
get_tree().quit(exit_code)
|
||
|
||
|
||
# ---- 模态/tooltip/刷新链一次性覆盖(真实信号路径驱动) ----
|
||
|
||
func _run_modal_probes_once() -> void:
|
||
# ⓪ 玩家面文案真实入口断言(迭代36 卡A wa1——C 区两段头随战斗常驻)
|
||
if not _tested_copy and main.info_panel != null and main.info_panel.visible:
|
||
_tested_copy = true
|
||
var feat_head: Label = null
|
||
var curio_head: Label = null
|
||
for child in main.info_panel._feat_section.get_children():
|
||
if child is Label:
|
||
feat_head = child
|
||
break
|
||
for child in main.info_panel._curio_section.get_children():
|
||
if child is Label:
|
||
curio_head = child
|
||
break
|
||
if feat_head == null or feat_head.text != "专长":
|
||
_problems.append("C 区专长段头未换代(wa1——真实入口)")
|
||
if curio_head == null or curio_head.text != "属性值提升":
|
||
_problems.append("C 区属性值提升段头未换代(wa1——真实入口)")
|
||
# ① 图例模态(pattern 6):打开 → 遮挡物隐藏 + 输入锁定;关闭 → 恢复
|
||
if not _tested_legend:
|
||
_tested_legend = true
|
||
main.ap_bar._btn_legend.pressed.emit()
|
||
if not main.legend_overlay.is_open():
|
||
_problems.append("图例打开失败")
|
||
if main._end_overlay.is_open() or main._start_overlay.visible:
|
||
_problems.append("图例打开时上方遮挡层未隐藏(缺陷⑤复发)")
|
||
if not main._input_locked():
|
||
_problems.append("图例打开时棋盘输入未锁定")
|
||
var legend_has_seal := false
|
||
for e in main.legend_overlay.ENTRIES:
|
||
if String(e[1]) == "封印":
|
||
legend_has_seal = true
|
||
# 图例封印行换代断言(迭代36 卡A wa5——真实 AP 条图例钮开层路径)
|
||
if not String(e[2]).contains("标空") or not String(e[2]).contains("受挖掘惩罚") or String(e[2]).contains("插错") or String(e[2]).contains("挖雷"):
|
||
_problems.append("图例封印行旧词残留(wa5——%s)" % String(e[2]).replace("
|
||
", "|"))
|
||
if not legend_has_seal:
|
||
_problems.append("图例缺『封印』格态条目(格态图例基线)")
|
||
main.legend_overlay.close()
|
||
if main.legend_overlay.is_open() or main._input_locked():
|
||
_problems.append("图例关闭后未恢复输入/可见性")
|
||
# ②/②b 明细回看模态无探针(M9/P-C7——冒险中回看归「生涯」,06 登记)
|
||
# ⑤ 状态面板(P1 丁角标真实信号路径 + P2 甲模态语义 + P1 守卫 + R3 锁恢复):
|
||
# 角标打开 → 只读层可见+输入锁定+余额徽非空 → 「继续」关闭恢复;图例开着 → 角标置灰且点击被守卫兜住
|
||
if not _tested_status and not main.legend_overlay.is_open() and not main.status_panel.visible:
|
||
_tested_status = true
|
||
var pbtn: Button = main.board_zone.status_badge
|
||
if pbtn == null or not pbtn.visible or pbtn.disabled:
|
||
_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
|
||
if not main.status_panel.visible:
|
||
_problems.append("状态面板未随角标打开(组合根接线失守)")
|
||
if not main._input_locked():
|
||
_problems.append("状态面板打开时输入未锁定(R3)")
|
||
var bal_txt: String = main.status_panel._balance_badge.value_label.text
|
||
if not bal_txt.begins_with("钥石 "):
|
||
_problems.append("状态面板余额徽投影缺失(%s)" % bal_txt)
|
||
# ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)----
|
||
# ① 面板子树零旧段文案(跨关概览退役零残留)
|
||
var sv_labels: Array = []
|
||
_collect_labels_by_text(main.status_panel, sv_labels)
|
||
for l in sv_labels:
|
||
if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"):
|
||
_problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text))
|
||
# ② 属性行全值 = 契约4 格式 % runner 五现值(main 侧投影值层真链路)
|
||
var rnr = main.run_scope.runner
|
||
if rnr == null:
|
||
_problems.append("状态面板开层时 runner 空指针(契约5——状态机缺陷,禁软回退)")
|
||
else:
|
||
var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [
|
||
int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(),
|
||
rnr.effective_ac(), int(rnr.gp)]
|
||
if String(main.status_panel._attrs_label.text) != attr_expect:
|
||
_problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text])
|
||
# ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle)
|
||
var sd: Dictionary = main._status_data()
|
||
var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"]
|
||
var keys_ok: bool = (sd as Dictionary).size() == 8
|
||
for k in keys:
|
||
keys_ok = keys_ok and (sd as Dictionary).has(k)
|
||
if not keys_ok:
|
||
_problems.append("_status_data() 键集≠八键(sv7——实测 %s)" % str((sd as Dictionary).keys()))
|
||
var oracle: Array = []
|
||
for rec in rnr.treasure_inventory.query_inventory(Callable()):
|
||
var rr: Dictionary = rec
|
||
oracle.append({"name": String(main.db.treasure_item_row(String(rr.get("item_id", ""))).get("display_name", "")),
|
||
"used": bool(rr.get("used", false))})
|
||
var sd_curios: Array = (sd as Dictionary).get("curios", [])
|
||
var curios_ok: bool = (sd_curios as Array).size() == (oracle as Array).size()
|
||
if curios_ok:
|
||
for i in (oracle as Array).size():
|
||
curios_ok = curios_ok and String((sd_curios[i] as Dictionary).get("name", "")) == String((oracle[i] as Dictionary).get("name", "")) and bool((sd_curios[i] as Dictionary).get("used", false)) == bool((oracle[i] as Dictionary).get("used", false))
|
||
if not curios_ok:
|
||
_problems.append("_status_data().curios 对拍独立 oracle 失守(sv7——实测 %s vs %s)" % [str(sd_curios), str(oracle)])
|
||
var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held())
|
||
if str((sd as Dictionary).get("feats", [])) != str(feats_expect):
|
||
_problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)")
|
||
var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
|
||
if resume_btn == null:
|
||
_problems.append("状态面板关闭钮缺失(「继续」)")
|
||
else:
|
||
resume_btn.pressed.emit()
|
||
if main.status_panel.visible or main._input_locked():
|
||
_problems.append("状态面板关闭后未恢复(visible=%s locked=%s——R3 残留)"
|
||
% [str(main.status_panel.visible), str(main._input_locked())])
|
||
# P1 守卫:图例开着 → 两处入口角标置灰;置灰态点击被处理器兜住(探针 emit 绕过 disabled)
|
||
main.ap_bar._btn_legend.pressed.emit()
|
||
if not main.board_zone.status_badge.disabled:
|
||
_problems.append("图例开着时战斗屏状态角标未置灰(P1 守卫失守)")
|
||
main.board_zone.status_badge.pressed.emit()
|
||
if main.status_panel.visible:
|
||
_problems.append("置灰态仍可开状态面板(守卫兜底失守)")
|
||
main.legend_overlay.close()
|
||
# ③ tooltip(状态效果槽悬停——来源+语义+剩余):有槽可见时触发
|
||
if not _tested_tooltip:
|
||
var row = main.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()
|
||
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()
|
||
if row._tip.visible:
|
||
_problems.append("状态槽 tooltip 未随移出隐藏")
|
||
break
|
||
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
|
||
if not _tested_refresh_chain and main.ap_bar.is_refresh_ready():
|
||
_tested_refresh_chain = true
|
||
var rap_before: int = int(main.run_scope.settlement.action_economy.rap)
|
||
var board_before = main.run_scope.settlement.minesweeper.current_board()
|
||
main.ap_bar.press_refresh_for_probe()
|
||
if not main.ap_bar.confirm_visible():
|
||
_problems.append("弃图刷新确认 chips 未出现(异位确认缺失)")
|
||
main.ap_bar.press_confirm_for_probe()
|
||
if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8:
|
||
_problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)])
|
||
if main.run_scope.settlement.action_economy.phase != "Active":
|
||
_problems.append("轮内刷新后相位丢失(%s)" % main.run_scope.settlement.action_economy.phase)
|
||
if main.run_scope.settlement.minesweeper.current_board() == board_before:
|
||
_problems.append("轮内刷新未当场换图")
|
||
# V5(刷新双账收口):轮内计数单源=结算快照(main 副本已删)+ HUD 余量一致
|
||
var used_now: int = int(main.run_scope.settlement.presentation_snapshot().get("refresh_used", 0))
|
||
if used_now != 1:
|
||
_problems.append("刷新轮内计数错误(%d)" % used_now)
|
||
var remain_expect: int = maxi(0, int(main.db.action_economy.refresh_cap_per_round) - used_now)
|
||
if not main.ap_bar.refresh_remaining_text().contains(str(remain_expect)):
|
||
_problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect])
|
||
|
||
|
||
# ---- 营地灰字取色断言辅助(迭代36 wc4——文本锚定位,全 Label 递归收集)----
|
||
|
||
func _collect_labels_by_text(root: Node, out: Array) -> void:
|
||
if root is Label:
|
||
out.append(root)
|
||
for child in root.get_children():
|
||
_collect_labels_by_text(child, out)
|
||
|
||
|
||
func _labels_with_text(labels: Array, needle: String) -> Array:
|
||
var found: Array = []
|
||
for l in labels:
|
||
if String(l.text).contains(needle):
|
||
found.append(l)
|
||
return found
|
||
|
||
|
||
func _play_one_action() -> void:
|
||
var settlement = main.run_scope.settlement
|
||
if settlement == null:
|
||
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
|
||
if not main._row_queue.is_empty():
|
||
return # T1 D1——演播期不出手:此时点击只会进锁期缓存被「最后一次」覆盖(原逐帧连点语义无行为覆盖价值,且毁锁期断言注入的独占前提)
|
||
var minesweeper = settlement.minesweeper
|
||
# 行为①:随机封印(右键——未开相邻格),约 20% 轮次
|
||
if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
|
||
var target := _random_unknown_adjacent(minesweeper)
|
||
if target.x >= 0:
|
||
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||
_seals_placed.append(target)
|
||
_ap_spent_this_round += 1
|
||
return
|
||
# 行为②:随机撤封印(右键已封印格),约 10% 轮次
|
||
if _rand.randf() < 0.1 and not _seals_placed.is_empty():
|
||
var fc: Vector2i = _seals_placed.pop_back()
|
||
if minesweeper.has_seal(fc) and not minesweeper.is_revealed(fc):
|
||
main.board_view.click_cell(fc, MOUSE_BUTTON_RIGHT)
|
||
_ap_spent_this_round += 1
|
||
return
|
||
# 行为③:随机无效点击(非相邻格/已开格),约 8% 轮次
|
||
if _rand.randf() < 0.08:
|
||
main.board_view.click_cell(Vector2i(_rand.randi_range(0, minesweeper.current_board().width - 1), _rand.randi_range(0, minesweeper.current_board().height - 1)), MOUSE_BUTTON_LEFT)
|
||
return
|
||
# 挖掘主策略(最近可挖格)
|
||
var plan = AutoPlayer.choose_dig(minesweeper) # O4 收口:策略单一实现
|
||
if not plan.is_empty():
|
||
if plan.hero_cell != minesweeper.hero_pos():
|
||
main.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT)
|
||
main.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT)
|
||
_ap_spent_this_round += 1
|
||
# 行为④:提前停手(已花 ≥3 AP 时 15%)——停手经 AP 条按钮真实信号路径
|
||
if _ap_spent_this_round >= 3 and settlement.action_economy.phase == "Active" and _rand.randf() < 0.15:
|
||
_click_stop()
|
||
elif settlement.action_economy.phase == "Active":
|
||
_click_stop()
|
||
|
||
|
||
func _random_unknown_adjacent(minesweeper) -> Vector2i:
|
||
var hero: Vector2i = minesweeper.hero_pos()
|
||
var cands: Array = []
|
||
for dy in range(-1, 2):
|
||
for dx in range(-1, 2):
|
||
if dx == 0 and dy == 0:
|
||
continue
|
||
var c: Vector2i = hero + Vector2i(dx, dy)
|
||
if minesweeper.current_board().in_bounds(c) and not minesweeper.is_revealed(c) and not minesweeper.has_seal(c):
|
||
cands.append(c)
|
||
if cands.is_empty():
|
||
return Vector2i(-1, -1)
|
||
return cands[_rand.randi_range(0, cands.size() - 1)]
|
||
|
||
|
||
func _click_stop() -> void:
|
||
# 停手走真实按钮信号路径(AP 条 → 组合根 settle_batch——排雷玩法 D1×6① 回归载体)
|
||
main.ap_bar._btn_stop.pressed.emit()
|
||
|
||
|
||
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
|
||
# 锁期断言(排播队列活跃=锁面):注入封印击 → 直落被冻结不受理(封印未置)+ `_locked_click`
|
||
# 缓存命中且为本注入(「最后一次」语义——注入帧内无并发点击)。
|
||
# 重放断言(演播完边沿 pump_ended→_replay_locked_click 重入唯一动作通道):缓存清空 +
|
||
# 受理恰一次——封印布尔=单次送达探测器(双发=两次翻转回 false、零发=false,均暴露)。
|
||
# 前提消失守卫:等重放期间出现停点层/战斗收场(注入时点恰逢终结演播)→ 静默弃测不计
|
||
# problem(封印动作不杀伤,正常局不触发;断言实现形态取舍在代拍白名单内)。
|
||
func _locktest_tick() -> void:
|
||
if _locktest_state == 2:
|
||
return
|
||
var settlement = main.run_scope.settlement
|
||
if settlement == null or settlement.minesweeper.current_board() == null:
|
||
return
|
||
if _locktest_state == 0:
|
||
if main._row_queue.is_empty() or main._input_locked():
|
||
return
|
||
if String(settlement.battle_result) != "" or String(settlement.action_economy.phase) != "Active":
|
||
return
|
||
if int(settlement.action_economy.ap) < 1:
|
||
return
|
||
var target := _locktest_pick_cell(settlement.minesweeper)
|
||
if target.x < 0 or settlement.minesweeper.has_seal(target):
|
||
return # 无合法邻格/目标被并发封印行为占位——下帧重挑
|
||
_locktest_cell = target
|
||
_locktest_frames = 0
|
||
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
|
||
if settlement.minesweeper.has_seal(target):
|
||
_problems.append("锁期断言:演播期点击未被冻结(封印即时受理——v3 冻结失守)")
|
||
_locktest_state = 2
|
||
return
|
||
if main.run_scope.locked_click.is_empty() or main.run_scope.locked_click.get("cell") != target:
|
||
_problems.append("锁期断言:演播期点击未入缓存(v3.1 缓存失守)")
|
||
_locktest_state = 2
|
||
return
|
||
_locktest_state = 1
|
||
elif _locktest_state == 1:
|
||
if main.curio_overlay.visible or main._inter_overlay.visible or main._end_overlay.is_open() \
|
||
or String(settlement.battle_result) != "":
|
||
_locktest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测
|
||
return
|
||
if not main._row_queue.is_empty():
|
||
_locktest_frames = 0 # 演播未完——重放耐心只在排空后起算(链式大演播数百帧不占预算)
|
||
return
|
||
_locktest_frames += 1
|
||
if _locktest_frames < 3:
|
||
return # 排空边沿的 deferred 重放(pump_ended 帧末执行)在途宽限
|
||
if _locktest_frames > 90:
|
||
_problems.append("重放断言:演播完边沿后缓存未清(%s)" % str(main.run_scope.locked_click))
|
||
_locktest_state = 2
|
||
return
|
||
if main.run_scope.locked_click.is_empty():
|
||
if not settlement.minesweeper.has_seal(_locktest_cell):
|
||
var b = settlement.minesweeper.current_board()
|
||
print("[lockdiag] 重放后封印未置:br='%s' phase=%s ap=%d cell=%s revealed=%s board=%dx%d hero=%s" % [
|
||
str(settlement.battle_result), str(settlement.action_economy.phase),
|
||
int(settlement.action_economy.ap), str(_locktest_cell),
|
||
str(settlement.minesweeper.is_revealed(_locktest_cell)),
|
||
b.width, b.height, str(settlement.minesweeper.hero_pos())])
|
||
_problems.append("重放断言:锁期缓存点击未受理恰一次(封印未置位——零发或双发)")
|
||
_locktest_state = 2
|
||
main.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 撤封印恢复现场(尽力而为不判定)
|
||
|
||
|
||
func _locktest_pick_cell(minesweeper) -> Vector2i:
|
||
var hero: Vector2i = minesweeper.hero_pos()
|
||
for dy in range(-1, 2):
|
||
for dx in range(-1, 2):
|
||
if dx == 0 and dy == 0:
|
||
continue
|
||
var c: Vector2i = hero + Vector2i(dx, dy)
|
||
if minesweeper.current_board().in_bounds(c) and not minesweeper.is_revealed(c) and not minesweeper.has_seal(c):
|
||
return c
|
||
return Vector2i(-1, -1)
|
||
|
||
|
||
func _report_stall(st: Dictionary) -> void:
|
||
var why := ""
|
||
if st.phase == "Waiting":
|
||
why = "结算停滞:Waiting 相位无推进(结算应同步走完——窗口段已删除)"
|
||
elif st.phase == "Active" and st.ap == 0:
|
||
why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)"
|
||
elif st.phase == "Idle" and main.run_scope.settlement.battle_result == "":
|
||
why = "Idle 相位但战斗未结束(begin 失败?)"
|
||
else:
|
||
why = "状态无推进(battle_result='%s')" % str(main.run_scope.settlement.battle_result)
|
||
print("[click_probe] 🔴 卡死复现(PROBE_SEED=%s):%s" % [OS.get_environment("PROBE_SEED"), why])
|
||
_dump_state()
|
||
get_tree().quit(2)
|
||
|
||
|
||
func _dump_state() -> void:
|
||
var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读)
|
||
print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [
|
||
str(main.run_scope.runner.node_index), str(main.run_scope.settlement.round_index),
|
||
str(main.run_scope.settlement.action_economy.phase), str(main.run_scope.settlement.action_economy.ap), str(main.run_scope.settlement.action_economy.rap)])
|
||
print(" battle_result='%s' settling=%s" % [
|
||
str(diag.battle_result), str(diag.settling)])
|
||
print(" board_installed=%s facts_path='%s' refresh_used=%s seals=%s" % [
|
||
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_seals_placed)])
|