243 lines
16 KiB
GDScript
243 lines
16 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 实现 GDD:design/ux/battle.md(场景级 headless 探针——四区布局实例化断言;entry 17②③)。
|
||
# 项目运行模式(autoload 在位)实例化 main.tscn,直接驱动 _start_adventure_from_menu()(完整冒险——唯一试玩入口),
|
||
# 断言真实构建(棋盘/主角站桩/敌方站桩/AP 点阵/明细带/数据框;无 A 区进度条断言与 E 区道具面板;
|
||
# 英雄/敌人 = 站桩断言)。
|
||
# 运行:Godot_console.exe --headless --path . res://probe/ui_probe.tscn
|
||
extends Node
|
||
|
||
const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点
|
||
|
||
|
||
func _ready() -> void:
|
||
await get_tree().process_frame # autoload 就绪
|
||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||
add_child(main)
|
||
await get_tree().process_frame
|
||
var problems: Array = []
|
||
# ---- 放弃入口(台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)----
|
||
if main.level_screen.abandon_btn == null or main.level_screen.abandon_btn.visible:
|
||
print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)")
|
||
get_tree().quit(1)
|
||
return
|
||
# ---- 营地覆盖层(主菜单入口/开合/余额文案/购买禁用态;
|
||
# 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律;
|
||
# 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)----
|
||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false)
|
||
if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_probe_camp"):
|
||
problems.append("营地入口缺失(选档层新档钮——mn9 新路径驱动中断)")
|
||
else:
|
||
await get_tree().process_frame
|
||
if not main.camp_overlay.visible:
|
||
problems.append("营地层未打开(进档开层新路径)")
|
||
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._tree_buy_btns.is_empty():
|
||
problems.append("树淬炼柜台购买钮缺失(四柜台重排后树=主界面)")
|
||
else:
|
||
var btn_by_id := {}
|
||
for card2 in main.camp_overlay._tree_buy_btns:
|
||
btn_by_id[String(card2.node_id)] = card2.btn
|
||
if not btn_by_id.has("sp_hp") or bool(btn_by_id["sp_hp"].disabled):
|
||
problems.append("脊柱生命 L1 白板可购钮未启用(无门槛——can_buy 不含余额面语义)")
|
||
if not btn_by_id.has("att_socket2") or not bool(btn_by_id["att_socket2"].disabled):
|
||
problems.append("同调位 2 前置未亮应禁用(点亮前置语义)")
|
||
if main.camp_overlay._workshop_rows.size() != 12:
|
||
problems.append("秘宝匣柜台行数错误(%d ≠ 12——迭代 22 秘宝批换代)" % main.camp_overlay._workshop_rows.size())
|
||
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("营地关闭后未恢复开始层(开合往返失守)")
|
||
# 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入)
|
||
ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true)
|
||
main.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机;本探针 start_single 第五路同吃此框,确定性禁依赖默认分支)
|
||
main._start_adventure_from_menu()
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
# ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层;
|
||
# press_candidate 图上命中路径 pick 入口单候选进首战)----
|
||
# ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)----
|
||
if not main.level_screen.visible:
|
||
problems.append("关卡开始未弹入口选路停点(level_screen 应可见)")
|
||
elif main._battle_root.visible:
|
||
problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)")
|
||
elif main.run_scope.runner == null or not main.run_scope.runner.awaiting_choice() \
|
||
or main.run_scope.runner.next_candidate_ids().size() != 1:
|
||
problems.append("入口停点状态失守(选路态 + 入口单候选)")
|
||
else:
|
||
main.level_screen.press_candidate(0)
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
if not main._battle_root.visible or main.level_screen.visible:
|
||
problems.append("选路提交战斗节点后未切回战斗屏(屏级互斥恢复失守)")
|
||
# ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)----
|
||
if main.level_screen.abandon_btn == null or not main.level_screen.abandon_btn.visible:
|
||
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
|
||
else:
|
||
main.level_screen.abandon_btn.pressed.emit()
|
||
await get_tree().process_frame
|
||
if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running":
|
||
problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)")
|
||
# ---- 既有断言(回归基线;A 区无进度条——节点链进度由关卡画面独立屏承载)----
|
||
if main.board_view.cells_probe().is_empty():
|
||
problems.append("棋盘格未生成(cells=0)")
|
||
if main.board_view.board() == null:
|
||
problems.append("board 未装载")
|
||
if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"):
|
||
problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()])
|
||
if main.run_scope.settlement == null or main.run_scope.settlement.battle_result != "":
|
||
problems.append("首战未处于进行中")
|
||
if not main.log_view.parsed_text().contains("节点 1/"):
|
||
problems.append("节点头日志缺失")
|
||
# ---- 四区布局断言(battle.md §Layout)----
|
||
if not main.stage.round_label.text.contains("第 1 轮"):
|
||
problems.append("B 区轮次·阶段指示缺失(%s)" % main.stage.round_label.text)
|
||
if main.stage.enemy_box.get_child_count() < 1:
|
||
problems.append("B 区敌方站桩未构建(%d 个)" % main.stage.enemy_box.get_child_count())
|
||
if main.ap_bar._dots.get_child_count() != int(main.db.action_economy.ap_per_round):
|
||
problems.append("D 区 AP 点阵数量错误(%d)" % main.ap_bar._dots.get_child_count())
|
||
if main.ap_bar.refresh_remaining_text() == "":
|
||
problems.append("D 区弃图刷新按钮文案缺失")
|
||
if main.board_zone.toast == null:
|
||
problems.append("D 区 toast 槽缺失")
|
||
if main.info_panel.item_slots == null or main.info_panel.item_slots.get_child_count() != 4:
|
||
problems.append("C 区秘宝槽(预留)缺失")
|
||
if main.stage.hero_standee.slot_row == null:
|
||
problems.append("B 区站桩状态行缺失(状态效果唯一展示面——2026-09-26 C 区精简批)")
|
||
# B 区明细带挖掘期收起断言(T17 视觉扩展批适配):结算演播期 settling 视觉延伸=mode=settle
|
||
# 属新语义(演播放完回 dig——main._is_settling 排播项,判据=纯队列),本断言仅在排播空闲态判定。
|
||
if main._row_queue.is_empty() \
|
||
and (main.log_view.mode() != "dig" or main.log_view.custom_minimum_size.y != 24.0):
|
||
problems.append("B 区明细带挖掘期应收起 24px(mode=%s h=%s)" % [main.log_view.mode(), str(main.log_view.custom_minimum_size.y)])
|
||
# E 区无道具面板——四区构建由其余断言覆盖
|
||
|
||
# ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4,全站桩)----
|
||
# B 区站桩子件伴随件化(壳层 tooltip 载体)——断言口径见台账 171:
|
||
# 伴随件化(壳层 tooltip 载体),穿透明(子件 _name_label/_armor_icon/_intent_badge)改本体
|
||
# 探针出口 stage.enemy_body_at(i).name_line_probe()/intent_probe()——呈现口径逐字保持
|
||
var standee = main.stage.enemy_body_at(0)
|
||
if standee == null:
|
||
problems.append("敌立绘本体缺失(enemy_body_at(0) 无世界站桩)")
|
||
else:
|
||
var name_line: Dictionary = standee.name_line_probe()
|
||
if String(name_line.name) == "" or String(name_line.armor_icon) == "":
|
||
problems.append("敌立绘名字行缺失(名字或护甲类型图标占位为空——2026-09-26 终裁:AC 不展示、类型图标化)")
|
||
# 三档动作动画占位契约(不显示技能名——图标档位 + 一次性动画)
|
||
standee.set_pending("damage", "")
|
||
var intent_on: Dictionary = standee.intent_probe()
|
||
if not bool(intent_on.visible) or String(intent_on.text) != "⚔":
|
||
problems.append("攻击系意图未呈现(visible=%s text=%s)" % [str(intent_on.visible), str(intent_on.text)])
|
||
standee.set_pending("basic", "")
|
||
if bool(standee.intent_probe().visible):
|
||
problems.append("普攻应零表示(意图指示应隐藏)")
|
||
# 站桩纹理非空断言(八敌真图落位;缺位回退链保持的对照面)
|
||
if not bool(standee.portrait_probe().get("has_tex", false)):
|
||
problems.append("敌立绘纹理未落位(素材管线 48px 档——迭代 6 T2;缺位回退链不应在此态触发)")
|
||
if main.stage.hero_standee == null or not main.stage.hero_standee.has_world_hp_bar:
|
||
problems.append("B 区主角站桩缺失(世界血条面缺失)")
|
||
elif not bool(main.stage.hero_standee.portrait_probe().get("has_tex", false)):
|
||
problems.append("主角立绘纹理未落位(48px 档——迭代 6 T2/T3)")
|
||
# ---- BUG-0001 回归(tooltip 入树后 set_effects 槽位不错位——结算软锁修复沉淀)----
|
||
var slots_row = main.stage.hero_standee.slot_row # BUG-0001 回归随组件迁入站桩(tooltip/槽位错位同源)
|
||
var probe_eff: Array = [{"effect_id": "attack_up", "display_name": "增伤", "layers": 1,
|
||
"remaining_settlements": 1, "source_class": "safe_cell", "round_index": 1, "effective_value": 50}]
|
||
slots_row.set_effects(probe_eff)
|
||
if slots_row._slots.size() != 1:
|
||
problems.append("BUG-0001 回归:set_effects 未建槽(%d)" % slots_row._slots.size())
|
||
slots_row._on_slot_hover(slots_row._slots[0]) # 悬停 → tooltip _tip 入子节点表(野外崩溃前置态)
|
||
slots_row.set_effects(probe_eff) # 修复前:_tip 被按位序当作槽 0 → _fill_slot 崩溃 → 结算协程中断软锁
|
||
if not (slots_row._tip in slots_row.get_children()):
|
||
problems.append("BUG-0001 回归:悬停后 tooltip 未入树(回归前置失真)")
|
||
elif slots_row._slots.size() != 1 or slots_row._slots[0] == slots_row._tip:
|
||
problems.append("BUG-0001 回归:tooltip 入树后槽引用错位")
|
||
else:
|
||
var icon_rect: TextureRect = slots_row._slots[0].get_child(0).get_child(0)
|
||
if icon_rect.texture == null:
|
||
problems.append("BUG-0001 回归:悬停后重刷槽未填充(texture=null)")
|
||
slots_row.set_effects([probe_eff[0], probe_eff[0], probe_eff[0], probe_eff[0], probe_eff[0], probe_eff[0]])
|
||
if slots_row._slots.size() != 4:
|
||
problems.append("BUG-0001 回归:超上限效果未截断至 4 槽(%d)" % slots_row._slots.size())
|
||
slots_row.set_effects([])
|
||
if not slots_row._slots.is_empty():
|
||
problems.append("BUG-0001 回归:空态未清槽(%d)" % slots_row._slots.size())
|
||
# ---- 图标键回归(HUD框架 图标键契约:状态表九行全映射〔定稿 PNG 纹理〕、
|
||
# 格面资源 cell_type 九型全映射〔含 gp 键〕;查不到的键 fail-fast + ? 占位
|
||
# ——此处钉死映射不残缺,未知键断言由 _icon_tex/_badge_glyph 承担)----
|
||
for eid in ["attack_up", "ac_up", "alert", "weaken", "enrage", "off_guard", "sapped", "empower", "fortify"]:
|
||
if not StatusSlotRow.EFFECT_ICON.has(eid) or StatusSlotRow.EFFECT_ICON[eid] == null:
|
||
problems.append("状态图标九键纹理映射缺失(%s)" % eid)
|
||
for cat in ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp"]:
|
||
if not BoardView.REWARD_BADGE.has(cat):
|
||
problems.append("奖励图标表缺键(%s)" % cat)
|
||
var probe_kb: Array = [
|
||
{"effect_id": "enrage", "display_name": "暴走", "layers": 1, "remaining_settlements": 1, "source_class": "seal_miss", "round_index": 1, "effective_value": 25},
|
||
{"effect_id": "off_guard", "display_name": "失衡", "layers": 1, "remaining_settlements": 1, "source_class": "seal_miss", "round_index": 1, "effective_value": 25},
|
||
]
|
||
slots_row.set_effects(probe_kb)
|
||
if slots_row._slots.size() != 2:
|
||
problems.append("图标键回归:enrage/off_guard 未建槽(%d)" % slots_row._slots.size())
|
||
else:
|
||
var ic_enrage: TextureRect = slots_row._slots[0].get_child(0).get_child(0)
|
||
var ic_off: TextureRect = slots_row._slots[1].get_child(0).get_child(0)
|
||
if ic_enrage.texture == null:
|
||
problems.append("暴走图标键未映射(texture=null)")
|
||
if ic_off.texture == null:
|
||
problems.append("失衡图标键未映射(texture=null)")
|
||
slots_row.set_effects([])
|
||
# ---- L2 单场段(debug_session 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)----
|
||
# 单场无用户入口——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线;
|
||
# _reset_run_scope 复现 _start_single 的提示去重复位)
|
||
main._begin_session(false)
|
||
main._debug.start_single(2)
|
||
await get_tree().process_frame
|
||
await get_tree().process_frame
|
||
if main.board_view.board() == null:
|
||
problems.append("L2 单场未启动(board=null)")
|
||
else:
|
||
# 地形变异:L2 板允许带变异(岩浆/烬覆)——布置域断言 =
|
||
# 变异格全在初始展开区外(closed_only 结构保障)
|
||
var b0 = main.board_view.board()
|
||
var r0p := {}
|
||
for c in b0.compute_r0():
|
||
r0p[c] = true
|
||
for c in b0.variants.keys():
|
||
if r0p.has(c):
|
||
problems.append("L2 板变异格落在初始展开区(地形变异 布置域违例)")
|
||
break
|
||
main.board_view.set_desaturated(true)
|
||
main.board_view.set_desaturated(false)
|
||
# ---- GP 终局行:summary gp_final 键取数+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板;
|
||
# 断言面 = summary 键,无影子账清零转存)----
|
||
# (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性)
|
||
var meta_saved = main.meta.save
|
||
for p2 in ["user://smoke_ui_probe_settle_scratch.json", "user://smoke_ui_probe_settle_scratch.json.tmp"]:
|
||
if FileAccess.file_exists(p2):
|
||
DirAccess.remove_absolute(p2)
|
||
main.meta.save = load("res://core/save_manager.gd").new("user://smoke_ui_probe_settle_scratch.json")
|
||
# 合成摘要不落隔离档(settle_adventure 写盘改道 scratch——跨跑自污染断根),GP 行只验呈现
|
||
main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3,
|
||
"stopped_at": 0, "keystone_total": 30, "master_seed": 42, "gp_final": 57})
|
||
main.meta.save = meta_saved
|
||
if not main._end_overlay.is_open():
|
||
problems.append("冒险终局面板未开(合成摘要路径)")
|
||
elif not main._end_overlay._gp_row.visible or not main._end_overlay._gp_val.text.contains("57"):
|
||
problems.append("终局面板 GP 行缺失或取数不对(%s)" % main._end_overlay._gp_val.text)
|
||
main._end_overlay.close()
|
||
main._close_modal(main._end_overlay)
|
||
if problems.is_empty():
|
||
print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [
|
||
main.board_view.cells_probe().size(), main.board_view.board().width, main.board_view.board().height,
|
||
main.info_panel.attack_text(),
|
||
main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()])
|
||
get_tree().quit(0)
|
||
else:
|
||
for p in problems:
|
||
push_error("[ui_probe] FAIL — " + p)
|
||
print("[ui_probe] FAIL — " + "; ".join(problems))
|
||
get_tree().quit(1)
|