test(smoke): 元进度实现批 T5——_smoke_meta 三段扩至 27 断言(549 绿)+ ui_probe/ui_click_probe 营地段(隔离存档换注保确定性)
This commit is contained in:
1 parent
2e419a17a3
commit
816d0b9279
3 files changed
+280
-2
No files matched your search
@@ -3084,6 +3084,177 @@ func _smoke_meta() -> void:
|
||||
var brow: Dictionary = _db.meta_boss_keystone_row(li)
|
||||
boss_ok = boss_ok and not brow.is_empty() and int(brow.get("first_kill", 0)) >= int(brow.get("repeat_kill", -1))
|
||||
check(boss_ok, "boss_keystone L1–L3 行连续可达且 first_kill ≥ repeat_kill 全行(首杀全量/重复小额)")
|
||||
_meta_pricing_and_buy()
|
||||
_meta_settle_and_workshop()
|
||||
_meta_injection()
|
||||
|
||||
|
||||
# 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档)
|
||||
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
|
||||
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
|
||||
"user://smoke_meta_cap.json", "user://smoke_meta_white.json"]
|
||||
|
||||
|
||||
func _meta_smoke_cleanup() -> void:
|
||||
for p in _META_SMOKE_PATHS:
|
||||
for suffix in ["", ".tmp", ".corrupt.bak"]:
|
||||
if FileAccess.file_exists(p + suffix):
|
||||
DirAccess.remove_absolute(p + suffix)
|
||||
|
||||
|
||||
func _meta_new_save(path: String):
|
||||
var sv = load("res://core/save_manager.gd").new(path)
|
||||
sv.load_from_disk()
|
||||
return sv
|
||||
|
||||
|
||||
# ---- 元进度 ②几何定价 ③购买(余额充足持久 / 不足拒绝 / attr 出域拒绝)----
|
||||
func _meta_pricing_and_buy() -> void:
|
||||
print("--- 元进度·几何定价与购买(隔离档 smoke_meta_buy/dry)---")
|
||||
_meta_smoke_cleanup()
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
# ② 几何定价:tier 0/1/2 价格 == 迭代 floor(p × ratio/100) 复算值(tier 0 = base_price)且严格递增
|
||||
for attr in MetaTable.FORGE_ATTRS:
|
||||
var row: Dictionary = _db.meta_forge_row(attr)
|
||||
var base := int(row.get("base_price", 0))
|
||||
var ratio := int(row.get("price_ratio_pct", 100))
|
||||
var all_ok := true
|
||||
var prev := 0
|
||||
for tier in 3:
|
||||
sv.set_forge_level(attr, tier)
|
||||
var expect := base
|
||||
for _i in tier:
|
||||
expect = int(floor(float(expect) * float(ratio) / 100.0))
|
||||
all_ok = all_ok and mp.forge_price(attr) == expect and (tier == 0 or expect > prev)
|
||||
prev = expect
|
||||
check(all_ok, "%s 几何定价 tier 0/1/2 = 迭代 floor 复算且严格递增(base %d × ratio %d%%)" % [attr, base, ratio])
|
||||
sv.set_forge_level("atk", 0)
|
||||
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久
|
||||
sv.add_keystone(1000)
|
||||
var bal0: int = sv.keystone_balance()
|
||||
var lvl0: int = sv.forge_level("atk")
|
||||
var price0: int = mp.forge_price("atk")
|
||||
check(mp.buy_forge("atk") and sv.forge_level("atk") == lvl0 + 1 and sv.keystone_balance() == bal0 - price0,
|
||||
"购买成功(Lv %d→%d、余额 %d−%d)" % [lvl0, lvl0 + 1, bal0, price0])
|
||||
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
check(sv_r.forge_level("atk") == lvl0 + 1 and sv_r.keystone_balance() == bal0 - price0, "购买持久(reload 后级与余额不变)")
|
||||
# ③ 余额不足 / attr 出域:false 零副作用
|
||||
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
|
||||
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
|
||||
check(not mp_dry.buy_forge("atk") and sv_dry.keystone_balance() == 0 and sv_dry.forge_level("atk") == 0,
|
||||
"余额不足 false 零副作用(0 余额购买拒绝)")
|
||||
check(not mp_dry.buy_forge("luck") and sv_dry.keystone_balance() == 0, "attr 出域 false 零副作用(luck 不在词表)")
|
||||
|
||||
|
||||
# ---- 元进度 ④注入可见 + 白板成立(验收 4/2/3——程序面;MC 逐位回归归 T7 全量验证)----
|
||||
func _meta_injection() -> void:
|
||||
print("--- 元进度·注入可见与白板(隔离档 smoke_meta_white/dry)---")
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[1])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.add_keystone(mp.forge_price("atk"))
|
||||
check(mp.buy_forge("atk") and int(mp.hero_override().get("atk", -1)) == int(_db.meta_forge_row("atk").get("flat_value", 0)),
|
||||
"买 atk 1 级 → hero_override().atk == 行 flat(验收4)")
|
||||
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
check(adr.start_adventure(920251, {"hero_override": mp.hero_override()}), "注入参数冒险启动(start_adventure params 通道)")
|
||||
var r = adr.runner()
|
||||
var ov: Dictionary = mp.hero_override()
|
||||
check(int(r.effective_atk()) == int(_db.hero.atk) + int(ov.get("atk", 0)),
|
||||
"首战 hero_atk = 表值 + 永久强化 flat(begin_battle 同表达式——下一局能用到强化)")
|
||||
check(int(r.effective_max_hp()) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0))
|
||||
and int(adr.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(ov.get("max_hp", 0)),
|
||||
"首战 hero_max_hp 与开局生命账本 = 表值 + 注入档")
|
||||
# 白板:同种子下 空参数 = 零强化参数 = 纯表值(逐位一致——注入通道默认值漏改在此转红)
|
||||
var adr_w = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
check(adr_w.start_adventure(920251), "白板冒险启动(无 params 实参——B1 空参数路径)")
|
||||
var sv_white = _meta_new_save(_META_SMOKE_PATHS[5])
|
||||
var adr_z = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(920251))
|
||||
check(adr_z.start_adventure(920251, {"hero_override": load("res://core/meta_progression.gd").new(_db, sv_white).hero_override()}),
|
||||
"零强化参数冒险启动(新档 hero_override 全零)")
|
||||
var rw = adr_w.runner()
|
||||
var rz = adr_z.runner()
|
||||
check(int(rw.effective_atk()) == int(_db.hero.atk) and int(rw.effective_max_hp()) == int(_db.hero.max_hp)
|
||||
and int(rz.effective_atk()) == int(rw.effective_atk()) and int(rz.effective_max_hp()) == int(rw.effective_max_hp())
|
||||
and int(rz.effective_ac()) == int(rw.effective_ac()),
|
||||
"白板成立:空参数 = 零强化参数 = 纯表值(atk/max_hp/ac 三轴逐位一致——验收 2/3)")
|
||||
check(int(rw.effective_ac()) == int(_db.hero.armor_class) and int(r.effective_ac()) == int(_db.hero.armor_class),
|
||||
"ac 轴:白板恒表值、未换强化时注入零位移")
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
# ---- 元进度 ⑥Boss 两档 ⑦钥石不丢 ⑧工坊解锁 ⑩摘要副本与 cap(合成摘要驱动——settle 纯账面零跑局)----
|
||||
func _meta_settle_and_workshop() -> void:
|
||||
print("--- 元进度·收束入账与工坊(隔离档 smoke_meta_settle/workshop/cap)---")
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[2])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var row1: Dictionary = _db.meta_boss_keystone_row(1)
|
||||
var row2: Dictionary = _db.meta_boss_keystone_row(2)
|
||||
# ⑥ 新档首通 L1:first = true、金额 = first_kill;total = big_reward + Σ drops;余额 = 累计入账
|
||||
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 11})
|
||||
var drop1: Dictionary = d1["boss_drops"][0]
|
||||
check(bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1))
|
||||
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5)")
|
||||
check(int(d1.get("total", 0)) == 3 + int(row1.get("first_kill", 0)), "明细 total = big_reward + Σ drops")
|
||||
check(sv.keystone_balance() == 3 + int(row1.get("first_kill", 0)), "余额 = 累计入账(大奖励 + Boss 两档)")
|
||||
# ⑥ 同档再通 → 重复杀档且恒定不递减(第三轮同额)
|
||||
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 12})
|
||||
var drop2: Dictionary = d2["boss_drops"][0]
|
||||
check(not bool(drop2.get("first", false)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)")
|
||||
var d3: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 13})
|
||||
check(not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"重复杀第三轮恒定不递减(验收5——无衰减曲线)")
|
||||
# 混合归因:通关两关 → L1 重复 + L2 首杀
|
||||
var d4: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
"stopped_at": 0, "keystone_total": 4, "master_seed": 14})
|
||||
check(not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
|
||||
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)),
|
||||
"两关通关逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)")
|
||||
# ⑦ 败北零通关摘要:余额只增不减(零入账)、clear_counts 不变
|
||||
var bal_before: int = sv.keystone_balance()
|
||||
var cc1_before: int = sv.clear_count(1)
|
||||
var dz: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
|
||||
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
|
||||
check(dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before,
|
||||
"败北零通关摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)")
|
||||
# ⑧ 工坊解锁:settle 内已随 refresh——总通关次数达阈值件数 == unlocked 数;重复 refresh 幂等
|
||||
var total_clears: int = sv.clear_count(1) + sv.clear_count(2) + sv.clear_count(3)
|
||||
var expect_n := 0
|
||||
for row in _db.meta_workshop_items():
|
||||
if total_clears >= int(row.get("clear_threshold", 99)):
|
||||
expect_n += 1
|
||||
var got_n := 0
|
||||
for st_row in mp.workshop_status():
|
||||
got_n += 1 if bool(st_row.get("unlocked", false)) else 0
|
||||
check(got_n == expect_n and mp.refresh_workshop_unlocks().is_empty(),
|
||||
"工坊解锁数 = 总通关 %d 次达阈值件数(%d 件)且重复 refresh 幂等" % [total_clears, expect_n])
|
||||
var sv_r = _meta_new_save(_META_SMOKE_PATHS[2])
|
||||
var r_n := 0
|
||||
for st_row in load("res://core/meta_progression.gd").new(_db, sv_r).workshop_status():
|
||||
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
|
||||
check(r_n == expect_n, "工坊解锁持久(reload 后一致)")
|
||||
# ⑧ 干净档:通关 1 次 → 恰两件阈值 1 道具新解锁(new_unlocks 明细)
|
||||
var sv_w = _meta_new_save(_META_SMOKE_PATHS[3])
|
||||
var mp_w = load("res://core/meta_progression.gd").new(_db, sv_w)
|
||||
var u1: Dictionary = mp_w.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 16})
|
||||
check(u1["new_unlocks"].size() == 2 and sv_w.item_unlocked("probe_dust") and sv_w.item_unlocked("sapper_order")
|
||||
and not sv_w.item_unlocked("loot_compass"), "通关 1 次 → 恰两件阈值 1 道具解锁(new_unlocks 明细正确)")
|
||||
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
|
||||
var rs: Array = sv.recent_summaries()
|
||||
check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
|
||||
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含 boss_drops 副本(复盘可归因——验收5)")
|
||||
var sv_c = _meta_new_save(_META_SMOKE_PATHS[4])
|
||||
var mp_c = load("res://core/meta_progression.gd").new(_db, sv_c)
|
||||
for i in 12:
|
||||
mp_c.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
|
||||
"stopped_at": 1, "keystone_total": 0, "master_seed": i})
|
||||
check(sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11,
|
||||
"摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)")
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
# ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除
|
||||
|
||||
@@ -37,7 +37,53 @@ func _ready() -> void:
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
var t_ui_ready := Time.get_ticks_msec() - t0
|
||||
# 满配直达 Boss(基线在节点 2-4 就会败北——探针目标是 Boss 战的结算路径)
|
||||
# ---- 营地购买合成点击路径(元进度实现批——campbtn/购买/关闭全走真实按钮信号路径;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)----
|
||||
for p0 in ["user://smoke_click_probe_camp.json", "user://smoke_click_probe_camp.json.tmp",
|
||||
"user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]:
|
||||
if FileAccess.file_exists(p0):
|
||||
DirAccess.remove_absolute(p0)
|
||||
var iso_c = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json")
|
||||
iso_c.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
if campbtn == null:
|
||||
_problems.append("营地入口按钮缺失(开始层 campbtn)")
|
||||
else:
|
||||
campbtn.pressed.emit() # 真实按钮信号路径 → 组合根 _open_camp
|
||||
await get_tree().process_frame
|
||||
if not main.camp_overlay.visible:
|
||||
_problems.append("营地层未打开(点击路径)")
|
||||
elif main.camp_overlay._forge_rows.is_empty():
|
||||
_problems.append("锻造柜台行缺失(点击路径)")
|
||||
else:
|
||||
var buy_btn: Button = main.camp_overlay._forge_rows[0].btn
|
||||
if not buy_btn.disabled:
|
||||
_problems.append("零余额锻造购买按钮未禁用(禁用态失守)")
|
||||
iso_c.add_keystone(9999)
|
||||
main.camp_overlay.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
if buy_btn.disabled:
|
||||
_problems.append("余额充足后购买按钮仍禁用(禁用态未随刷新)")
|
||||
var bal_before: int = iso_c.keystone_balance()
|
||||
var buy_attr: String = String(main.camp_overlay._forge_rows[0].attr)
|
||||
var price: int = main.meta.forge_price(buy_attr)
|
||||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_buy → meta.buy_forge(写调用唯一归组合根)
|
||||
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.json")
|
||||
iso_chk.load_from_disk()
|
||||
if int(iso_chk.forge_level(buy_attr)) != 1:
|
||||
_problems.append("营地购买未持久(reload 后级 != 1)")
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
_problems.append("营地关闭后未恢复开始层")
|
||||
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
|
||||
var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json")
|
||||
iso_reset.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset)
|
||||
main.tune_panel.set_sliders(200, 100)
|
||||
var t1 := Time.get_ticks_msec()
|
||||
main._start_full_level()
|
||||
@@ -272,3 +318,14 @@ func _dump_state() -> void:
|
||||
str(diag.battle_result), str(diag.settling)])
|
||||
print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [
|
||||
str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)])
|
||||
|
||||
|
||||
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;与 ui_probe 同款助手)
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root is Button and String(root.text).begins_with(prefix):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_prefix(child, prefix)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
@@ -13,15 +13,54 @@ func _ready() -> void:
|
||||
var main = load("res://presentation/main.tscn").instantiate()
|
||||
add_child(main)
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
# ---- 放弃入口(2026-09-21 用户拍板——台账 119):默认隐藏(单关测试直入口无冒险可放弃)----
|
||||
if main.route_overlay.abandon_btn == null or main.route_overlay.abandon_btn.visible:
|
||||
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(单关测试直入口关)")
|
||||
get_tree().quit(1)
|
||||
return
|
||||
# ---- 营地覆盖层(元进度实现批——主菜单入口/开合/余额文案/购买禁用态;
|
||||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律)----
|
||||
var iso = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
|
||||
iso.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
|
||||
var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地")
|
||||
if campbtn == null:
|
||||
problems.append("营地入口按钮缺失(开始层 campbtn)")
|
||||
else:
|
||||
campbtn.pressed.emit() # 合成信号路径(真实按钮 → 组合根 _open_camp)
|
||||
await get_tree().process_frame
|
||||
if not main.camp_overlay.visible:
|
||||
problems.append("营地层未打开(campbtn 路径)")
|
||||
elif main._start_overlay.visible:
|
||||
problems.append("营地层打开时开始层未隐藏(规范 2.3 遮挡纪律)")
|
||||
else:
|
||||
if not String(main.camp_overlay._balance_label.text).begins_with("钥石"):
|
||||
problems.append("营地余额文案缺失(%s)" % main.camp_overlay._balance_label.text)
|
||||
if main.camp_overlay._forge_rows.size() != 3:
|
||||
problems.append("锻造柜台行数错误(%d ≠ 3)" % main.camp_overlay._forge_rows.size())
|
||||
else:
|
||||
var zero_disabled := true
|
||||
for card in main.camp_overlay._forge_rows:
|
||||
zero_disabled = zero_disabled and bool(card.btn.disabled) # 零余额全禁用
|
||||
if not zero_disabled:
|
||||
problems.append("零余额下锻造购买按钮未全禁用(余额不足禁用态失守)")
|
||||
if main.camp_overlay._workshop_rows.size() != 6:
|
||||
problems.append("工坊柜台行数错误(%d ≠ 6)" % main.camp_overlay._workshop_rows.size())
|
||||
main._on_camp_closed()
|
||||
await get_tree().process_frame
|
||||
if main.camp_overlay.visible or not main._start_overlay.visible:
|
||||
problems.append("营地关闭后未恢复开始层(开合往返失守)")
|
||||
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
|
||||
for p in ["user://smoke_ui_probe_camp.json", "user://smoke_ui_probe_camp.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
var iso_zero = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
|
||||
iso_zero.load_from_disk()
|
||||
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero)
|
||||
main._start_full_level()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var problems: Array = []
|
||||
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
|
||||
if main.route_overlay.abandon_btn == null or not main.route_overlay.abandon_btn.visible:
|
||||
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
|
||||
@@ -154,3 +193,14 @@ func _ready() -> void:
|
||||
push_error("[ui_probe] FAIL — " + p)
|
||||
print("[ui_probe] FAIL — " + "; ".join(problems))
|
||||
get_tree().quit(1)
|
||||
|
||||
|
||||
# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;text 前缀匹配首枚按钮)
|
||||
func _find_button_by_prefix(root: Node, prefix: String) -> Button:
|
||||
if root is Button and String(root.text).begins_with(prefix):
|
||||
return root
|
||||
for child in root.get_children():
|
||||
var found := _find_button_by_prefix(child, prefix)
|
||||
if found != null:
|
||||
return found
|
||||
return null
|
||||
Reference in new issue
Block a user