# 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)