# VERTICAL SLICE - NOT FOR PRODUCTION # 实现 GDD:design/ux/battle.md(场景级 headless 探针——四区布局实例化断言;entry 17②③)。 # Date: 2026-09-12(战斗屏改造批扩展;基础 = 2026-09-11 首战棋盘探针——既有断言全保留) # 项目运行模式(autoload 在位)实例化 main.tscn,直接驱动 _start_adventure_from_menu()(完整冒险——唯一试玩入口), # 断言真实构建(棋盘/主角站桩/敌方站桩/AP 点阵/明细带/数据框;A 区进度条断言随 停点前移批 2026-09-23 退役; # E 区道具面板已删除;2026-09-20 全站桩拍板——英雄卡/敌人卡断言随站桩改造批换装)。 # 运行:Godot_console.exe --headless --path . res://smoke/ui_probe.tscn extends Node func _ready() -> void: await get_tree().process_frame # autoload 就绪 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.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 路径注入纪律)---- 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() iso_zero.mark_first_adventure_done() # 对齐真实档游玩史口径(模板抽签路径不变) main.save_manager = iso_zero # 迭代 8 卡①——断点写盘面同步隔离(禁碰真实档) main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero) main._refresh_resume_button() main._start_adventure_from_menu() await get_tree().process_frame await get_tree().process_frame # ---- 入口选路停点(停点前移批 2026-09-23:关卡开始即出节点图——level_started 后、首战前弹选路层; # press_candidate 图上命中路径 pick 入口单候选进首战——候选按钮退役 2026-09-28)---- # ---- 独立性断言(形态升格批 2026-09-28:独立屏激活期战斗屏宿主整体隐藏,零透出)---- if not main.level_screen.visible: problems.append("关卡开始未弹入口选路停点(level_screen 应可见)") elif main._battle_root.visible: problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") elif main.runner == null or not main.runner.awaiting_choice() \ or main.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.adventure == null or String(main.adventure.state) != "running": problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") # ---- 既有断言(回归基线;A 区进度条断言随 停点前移批 2026-09-23 退役——节点链进度由关卡画面独立屏承载)---- if main.board_view._cells.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.settlement == null or main.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 区精简批)") if 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 区道具面板已随可选类删除下线(格面资源批 2026-09-15)——四区构建由其余断言覆盖 # ---- 敌方站桩投影(顺位徽标/意图三档动作动画占位——敌人系统×4;2026-09-20 全站桩拍板)---- # B 区站桩 Node2D 迁移批 T3(2026-09-26)断言等价改写(台账 171 改判记录):enemy_box 子件 # 伴随件化(壳层 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("普攻应零表示(意图指示应隐藏)") # 站桩纹理非空断言(迭代 6 T2——八敌真图落位;缺位回退链保持的对照面) 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 纹理,2026-09-24 状态图标接入批〕、 # 格面资源 cell_type 九型全映射〔gp 键 = 九型批 2026-09-20〕;查不到的键 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": "flag_miss", "round_index": 1, "effective_value": 25}, {"effect_id": "off_guard", "display_name": "失衡", "layers": 1, "remaining_settlements": 1, "source_class": "flag_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 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)---- # 2026-09-13 单场用户入口已删——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线; # _reset_run_scope 复现原 _start_single 的提示去重复位) main._reset_run_scope() 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: # 地形变异 首批类型定稿批(2026-09-19):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 终局行(迭代 6 T7 卡⑩):影子账清零转存+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板)---- # (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性) main._adventure_gp_view = 57 main._on_wallet_changed("clear", 0, {}) if int(main._adventure_gp_final) != 57 or int(main._adventure_gp_view) != 0: problems.append("GP 影子账清零转存失败(final=%s view=%s)" % [str(main._adventure_gp_final), str(main._adventure_gp_view)]) var meta_saved = main.meta main.meta = null # 合成摘要不落结算(settle_adventure 会写隔离档→跨跑自污染),GP 行只验呈现 main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3, "stopped_at": 0, "keystone_total": 30, "master_seed": 42}) main.meta = meta_saved if not main._end_overlay.is_open(): problems.append("冒险终局面板未开(合成摘要路径)") elif not main._end_overlay._gp_line.visible or not main._end_overlay._gp_line.text.contains("57"): problems.append("终局面板 GP 行缺失或取数不对(%s)" % main._end_overlay._gp_line.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.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) # 开始层按钮按文案前缀寻址(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