198 lines
8.9 KiB
GDScript
198 lines
8.9 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 探针共用面:树寻址助手 + 存档/设置/元进度隔离样板单点。
|
||
# 覆盖:ui_probe / ui_click_probe / shot_probe / fx_shot_probe 四探针的树寻址样板
|
||
# 与隔离样板(各探针隔离文件名后缀 tag 保持不变)。
|
||
# 用法:探针侧 `const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")` 后静态调用
|
||
#(探针载体含 --script 场景两种,均无全局标识符——禁 class_name 裸引用)。
|
||
extends RefCounted
|
||
|
||
const SaveManagerScript := preload("res://core/save_manager.gd")
|
||
const MetaProgressionScript := preload("res://core/meta_progression.gd")
|
||
const SettingsStoreScript := preload("res://presentation/settings_store.gd")
|
||
|
||
# ── 树寻址(递归只读——按钮/标签句柄未存组合根成员的探针专用面) ──────────────
|
||
# 匹配模式参数化(历史两种语义,禁混用禁静默改变):
|
||
# mode="prefix" = text.begins_with(needle)(ui_probe / ui_click_probe 历史语义)
|
||
# mode="contains" = text.contains(needle)(shot_probe / fx_shot_probe 历史语义)
|
||
# 调用点替换时逐一核对原语义后选参。
|
||
|
||
static func find_button(root: Node, needle: String, mode: String = "prefix") -> Button:
|
||
if root == null:
|
||
return null
|
||
if root is Button and _text_match(String(root.text), needle, mode):
|
||
return root
|
||
for child in root.get_children():
|
||
var found := find_button(child, needle, mode)
|
||
if found != null:
|
||
return found
|
||
return null
|
||
|
||
|
||
# CheckButton 同款寻址(ui_click_probe 语义 = contains——断言设置面旧入口不在)
|
||
static func find_check_button(root: Node, needle: String) -> CheckButton:
|
||
if root == null:
|
||
return null
|
||
if root is CheckButton and String(root.text).contains(needle):
|
||
return root
|
||
for child in root.get_children():
|
||
var found := find_check_button(child, needle)
|
||
if found != null:
|
||
return found
|
||
return null
|
||
|
||
|
||
# Label 按文案包含寻址(shot_probe GP 徽章读数锚)
|
||
static func find_label(root: Node, text_fragment: String) -> Label:
|
||
if root == null:
|
||
return null
|
||
if root is Label and String(root.text).contains(text_fragment):
|
||
return root
|
||
for child in root.get_children():
|
||
var found := find_label(child, text_fragment)
|
||
if found != null:
|
||
return found
|
||
return null
|
||
|
||
|
||
# 控件实测几何(shot_probe checker 文字框取样坐标源——禁硬编码像素,分辨率档位自适应)
|
||
static func label_rect(root: Node, text_fragment: String) -> Dictionary:
|
||
if root == null:
|
||
return {}
|
||
if root is Label and String(root.text).contains(text_fragment):
|
||
return {"x": root.global_position.x, "y": root.global_position.y,
|
||
"w": root.size.x, "h": root.size.y}
|
||
for child in root.get_children():
|
||
var r := label_rect(child, text_fragment)
|
||
if not r.is_empty():
|
||
return r
|
||
return {}
|
||
|
||
|
||
# 按脚本名寻址(resource_path contains——shot_probe hero 面板锚)
|
||
static func find_by_script(root: Node, script_name: String) -> Node:
|
||
if root == null:
|
||
return null
|
||
var s: Script = root.get_script()
|
||
if s != null and String(s.resource_path).contains(script_name):
|
||
return root
|
||
for child in root.get_children():
|
||
var r := find_by_script(child, script_name)
|
||
if r != null:
|
||
return r
|
||
return null
|
||
|
||
|
||
static func _text_match(text: String, needle: String, mode: String) -> bool:
|
||
return text.begins_with(needle) if mode == "prefix" else text.contains(needle)
|
||
|
||
|
||
# ── 存档/设置/元进度三面隔离(探针禁碰真实档 user://save.json——SaveManager 路径注入纪律) ──
|
||
# tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。
|
||
# mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注
|
||
#(隔离 cfg 名 = <tag>_settings.cfg,与被收口探针原文件名逐一同名);
|
||
# 「继续上局」按钮重刷;swap_save = 是否换注全局 SaveManager(营地先行段只换 meta 侧不换 SaveManager)。
|
||
# 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。
|
||
# SaveManager/MetaProgression 是 autoload 单例——隔离面走单例原地重置
|
||
#(attach_path 沿 RngService.set_master_seed 先例;MetaProgression 持 save 引用透传,
|
||
# swap_save=false 分支换 meta 侧引用=独立 iso 实例,只换 meta 不换 save_manager)。
|
||
static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false,
|
||
swap_save: bool = true): # refresh_resume 参数已随卡② _refresh_resume_button 删除退役(mn7)
|
||
var save_path := "user://%s.json" % tag
|
||
for p in [save_path, save_path + ".tmp", "user://%s_settings.cfg" % tag]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
var iso = SaveManagerScript.new(save_path)
|
||
iso.load_from_disk()
|
||
if mark_first_done:
|
||
iso.meta_state.mark_first_adventure_done()
|
||
# --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析
|
||
var sm = main.get_node("/root/SaveManager")
|
||
if swap_save:
|
||
sm.slots.attach_path(save_path)
|
||
if mark_first_done:
|
||
sm.meta_state.mark_first_adventure_done()
|
||
iso = sm
|
||
else:
|
||
main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动
|
||
if with_settings:
|
||
main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag)
|
||
return iso
|
||
|
||
# ---- 真输入点击承载(test-conventions §8.3「真实入口」判定口径)----
|
||
# 合成鼠标事件经 Viewport.push_input 进引擎输入管线(hitbox/命中测试参与),press+release 成对
|
||
#(BaseButton pressed 信号走 release 沿——ui_input_probe :717-733 / screen_flow_probe :484-499 同式)。
|
||
# 信号直发(pressed.emit())只许标「接线验证」,不得充当真实入口断言的驱动。
|
||
static func push_click(main, ctrl: Control) -> void:
|
||
push_click_at(main, ctrl.get_global_rect().get_center())
|
||
|
||
|
||
static func push_click_at(main, pos: Vector2) -> void:
|
||
var down := InputEventMouseButton.new()
|
||
down.position = pos
|
||
down.global_position = pos
|
||
down.button_index = MOUSE_BUTTON_LEFT
|
||
down.pressed = true
|
||
main.get_viewport().push_input(down, true)
|
||
var up := InputEventMouseButton.new()
|
||
up.position = pos
|
||
up.global_position = pos
|
||
up.button_index = MOUSE_BUTTON_LEFT
|
||
up.pressed = false
|
||
main.get_viewport().push_input(up, true)
|
||
|
||
|
||
# 真输入拖拽沿(press→motion×steps→release 成对经 push_input 进引擎输入管线——卷轴视口批
|
||
# R3/R5/R6 驱动沿;沿 push_click_at 同式。relative 按步显式置值——合成事件的位移量由事件
|
||
# 自身承载,被测面按 relative 逐帧平移)
|
||
static func push_drag_at(main, pos: Vector2, delta: Vector2, steps: int = 4) -> void:
|
||
var down := InputEventMouseButton.new()
|
||
down.position = pos
|
||
down.global_position = pos
|
||
down.button_index = MOUSE_BUTTON_LEFT
|
||
down.pressed = true
|
||
main.get_viewport().push_input(down, true)
|
||
for i in range(1, steps + 1):
|
||
var cur := pos + delta * float(i) / float(steps)
|
||
var mv := InputEventMouseMotion.new()
|
||
mv.position = cur
|
||
mv.global_position = cur
|
||
mv.relative = delta / float(steps)
|
||
main.get_viewport().push_input(mv, true)
|
||
var up := InputEventMouseButton.new()
|
||
up.position = pos + delta
|
||
up.global_position = pos + delta
|
||
up.button_index = MOUSE_BUTTON_LEFT
|
||
up.pressed = false
|
||
main.get_viewport().push_input(up, true)
|
||
|
||
|
||
# 真输入滚轮沿(单档滚轮事件经 push_input——卷轴视口批 R4 驱动沿;方向映射在被测面,
|
||
# 本工具只发原始滚轮档)
|
||
static func push_wheel_at(main, pos: Vector2, wheel_down: bool = true) -> void:
|
||
var ev := InputEventMouseButton.new()
|
||
ev.position = pos
|
||
ev.global_position = pos
|
||
ev.button_index = MOUSE_BUTTON_WHEEL_DOWN if wheel_down else MOUSE_BUTTON_WHEEL_UP
|
||
ev.pressed = true
|
||
main.get_viewport().push_input(ev, true)
|
||
|
||
|
||
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤;槽模板注入隔离命名空间,防碰真实槽
|
||
# user://save_slot_N.json——真实档红线)。创建=激活=暂行开营地(卡③ Q3 乙落点);
|
||
# 驱动后写盘目标=隔离槽文件(持久化断言读 user://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
|
||
static func open_camp_via_savesel(main, tag: String) -> bool:
|
||
var sm = main.get_node("/root/SaveManager")
|
||
sm.slots.attach_slot_template("user://%s_slot_%%d.json" % tag)
|
||
for i in range(1, 4): # 夹具清场——空槽起(陈旧槽文件会翻有档态、新档钮缺席)
|
||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||
var sp: String = sm.slots.slot_path(i) + suf
|
||
if FileAccess.file_exists(sp):
|
||
DirAccess.remove_absolute(sp)
|
||
main.open_save_select()
|
||
await main.get_tree().process_frame # 排版帧——真点击命中的是控件 rect,开层同帧容器未排(emit 无此依赖)
|
||
var new_btn: Button = main.ui.modals.menu.save_select_overlay._new_btns.get(1)
|
||
if new_btn == null:
|
||
return false
|
||
push_click(main, new_btn) # 真输入 → _on_save_select_new → create_slot+open_camp
|
||
return true
|