# VERTICAL SLICE - NOT FOR PRODUCTION # UI 结构探针:全 UI 屏结构断言一次跑完:逐屏实例化(菜单簇真驱动 + 冒险簇真流推进 + 兑付屏真实 open 面合成喂入), # 断言族: # ①overlap 同屏内可见未禁用按钮两两 global_rect 不相交(互压=红;层间模态盖底层系设计内——各屏采样以屏根为界天然排除) # ②equal_width 同一直接父容器内 ≥2 可见按钮宽度差 >1px=红(容器纪律:组内等宽;钮高族 # equal_height——同容器高度差 >1px=红,纪律=ui_kit BUTTON_MIN_H) # ③offscreen 可见件 global_rect 完全落在视口外=红 # ④zero_size 可见件宽或高 ≤0=红 # ⑤start 专项 标题屏左列(start_box)与右列(start_right)矩形不相交(双列契约——叠放缺陷回归闸) # 豁免:WL 常量表按「屏名/规则名/件名」登记合法例外(登记须注理由于行尾注释)。 # 运行:Godot_console.exe --headless --path . res://probe/ui_struct_probe.tscn extends Node const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # 豁免清单(空 = 零豁免起步;红灯逐条核对后合法者入表并注理由) const WL := {} var problems: Array = [] 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 # ---- 菜单簇(开始层可见态;逐屏开→采样→关,模态恢复走组合根真实路径)---- ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_menu", true, false, true) _settle() await _audit("start", [main.ui.screens.start_overlay]) _start_columns_contract(main) _modal_registry_contract(main) main.open_settings() _settle() await _audit("settings", [main.ui.modals.menu.settings_overlay]) main._on_settings_closed() _settle() if not await ProbeUiUtil.open_camp_via_savesel(main, "smoke_ui_struct_probe_menu"): problems.append("[camp] 营地入口缺失(选档层新档钮——mn9 新路径驱动中断)〔句柄子容器批 V1〕") else: _settle() await _audit("camp", [main.ui.modals.camp.scene]) # 审计根=营地场景图(卡④ T5——CampOverlay 退役) # 营地灰字源 token 扫描(迭代36 wc4 静态半——营地域视图源内零裸 UiKit.STONE;判别式= # STONE 出现次数 == STONE_DEEP 出现次数;卡④ T5 改扫场景根+四子界面+公共壳六件) var camp_stone := 0 var camp_stone_deep := 0 for vf in ["camp_scene_view", "forge_view", "treasure_view", "tree_view", "codex_view", "camp_sub_view_base"]: var vf_src := FileAccess.get_file_as_string("res://presentation/views/%s.gd" % vf) camp_stone += vf_src.count("UiKit.STONE") camp_stone_deep += vf_src.count("UiKit.STONE_DEEP") if camp_stone != camp_stone_deep: problems.append("[camp] 营地域视图存在裸 UiKit.STONE(非 _DEEP 后缀——wc4 源扫描)〔句柄子容器批 V1〕") # ---- 营地场景层序段(卡④ V1①——子节点序=layout 表 z 序逐层相等、各层纹理矩形与表逐值一致; # 锻炉可达(V2 场景点选路由)+顶栏位在座(V8 结构)+根态反参照(V9——根态零文字按钮))---- var layout: CampSceneLayoutTable = main.db.presentation.camp_scene_layout var order_ok := true var rect_ok := true for i in layout.layers.size(): var layer_node: Control = main.ui.modals.camp.scene.get_child(i) if String(layer_node.name) != "Layer_%s" % layout.layers[i].key: order_ok = false if layer_node.position != layout.layers[i].texture_rect.position or layer_node.size != layout.layers[i].texture_rect.size: rect_ok = false if not order_ok: problems.append("[camp] 场景子节点序≠layout 表 z 序(逐层相等失守)〔营地图批 V1〕") if not rect_ok: problems.append("[camp] 层贴图矩形≠layout 表(逐值一致失守)〔营地图批 V1〕") ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"forge").get_center()) _settle() if not main.ui.modals.camp.forge.visible: problems.append("[camp] 点选锻炉→锻造子界面未开(场景点选路由中断)〔营地图批 V2〕") var hud_slot: Control = main.ui.modals.camp.scene.get_node_or_null("HudBandSlot") if hud_slot == null or hud_slot.custom_minimum_size.y != 48: problems.append("[camp] 顶栏 HUD 预留位缺失或高≠48(浮层覆盖不挤压画面)〔营地图批 V8〕") var scene_buttons := 0 for n in main.ui.modals.camp.scene.get_children(): if n is Button: scene_buttons += 1 if scene_buttons != 0: problems.append("[camp] 场景根态存在文字按钮(反参照纪律——禁文字按钮列表形态)〔营地图批 V9〕") ProbeUiUtil.push_click(main, ProbeUiUtil.find_button(main.ui.modals.camp.forge, "返回营地")) _settle() main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3) _settle() # ---- 生涯页:入口=帐篷旁笔记本(卡④ T3 改挂——场景点选信号真实路由→组合根 _open_career)→ # 结构断言族→只读巡检(契约 3)+零动效契约+零跳转契约(提纲 P4 甲)→组合根真实路径关闭。 #(登记序/树序契约统一在全模态 _modal_registry_contract——boot 段跑)---- # oracle: design/ux/camp.md:19 帐篷旁笔记本→生涯记录;生涯页返回钮口径由结构校验兜底(生涯段) # 营地语境:开始层全程隐藏(笔记本入口在营地场景根——关生涯回营地不回开始层,卡④ T3 语境分支) main._camp_pres.open_camp() # 重开营地场景根(笔记本入口语境) _settle() ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"notebook").get_center()) _settle() if not main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[career] 生涯页未打开或开始层未隐藏(笔记本改挂路径断裂)〔营地图批 V5〕") await _audit("career", [main.ui.modals.menu.career_overlay]) _career_readonly_contract(main) _career_zero_motion_contract(main) _career_nojump_contract(main) main._on_career_closed() _settle() if main.ui.modals.menu.career_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[career] 关闭后页未隐藏或开始层回弹(营地语境分支失守)〔营地图批 V5〕") # ---- Esc 浮窗结构段(卡⑤——三钮在位且仅三+零动效契约+登记序在册+debug_badge 不波及)---- # oracle: design/ux/camp.md:45 ESC 优先序:有窗先关最上层窗、无窗才进浮窗;各窗自带关闭钮(Esc 段) main._camp_pres.open_camp() _settle() var gpos_gate: Vector2 = main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center() var gdown := InputEventMouseButton.new() gdown.button_index = MOUSE_BUTTON_LEFT gdown.pressed = true gdown.position = gpos_gate gdown.global_position = gpos_gate main.get_viewport().push_input(gdown, true) # 按下在 gate 命中区 await get_tree().process_frame await get_tree().process_frame var gup := InputEventMouseButton.new() gup.button_index = MOUSE_BUTTON_LEFT gup.pressed = false gup.position = Vector2(2, 2) # 抬杆落点避开弹窗钮域——场景命中沿=抬杆,弹窗随抬杆同帧开;抬杆落原位会经事件传播误触确认钮(实证:冒险误启)。抬杆离场=不经历弹窗开关,段断言(Esc 路由)在净态成立 gup.global_position = Vector2(2, 2) main.get_viewport().push_input(gup, true) _settle() ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._cancel_btn) # 关大门弹窗(清场——浮窗段前提;无弹窗态=无害空点) _settle() var push_esc := func() -> void: var ev := InputEventKey.new() ev.keycode = KEY_ESCAPE ev.physical_keycode = KEY_ESCAPE ev.pressed = true main.get_viewport().push_input(ev, true) push_esc.call() _settle() if not main.ui.modals.camp.esc_overlay.visible: problems.append("[esc] 浮窗未唤起(结构段前提——营地语境 Esc 路由)〔ESC批 V1〕") var esc_btns := _buttons(main.ui.modals.camp.esc_overlay).size() if esc_btns != 3: problems.append("[esc] 浮窗三钮在位且仅三(实得 %d——禁第四钮)〔ESC批 V2〕" % esc_btns) var esc_static := FileAccess.get_file_as_string("res://presentation/views/camp_esc_overlay.gd") + FileAccess.get_file_as_string("res://presentation/main_esc_flow.gd") if esc_static.contains("create_tween") or esc_static.contains("UiKit.motion"): problems.append("[esc] 浮窗双件源码存在动效令牌(契约 6 零动效——直开直关)〔ESC批 V7〕") var esc_anim := _count_animplayers(main.ui.modals.camp.esc_overlay) if esc_anim != 0: problems.append("[esc] 浮窗子树 AnimationPlayer 非零(契约 6 零动效)〔ESC批 V7〕") var esc_src := FileAccess.get_file_as_string("res://presentation/views/camp_esc_overlay.gd") + FileAccess.get_file_as_string("res://presentation/main_esc_flow.gd") if esc_src.is_empty(): problems.append("[esc] 零动效扫描面为空(源文件读失败——非空前置失守)〔ESC批 V7〕") # 退出项接线断言(卡⑤ V5——绑信号连接与调用链;headless 不可真退=静态判据+观测面局限申报) var quit_wired := false for c in main.ui.modals.camp.esc_overlay.quit_requested.get_connections(): if c.callable.get_method() == "_on_esc_quit": quit_wired = true if not quit_wired: problems.append("[esc] 退出钮接线断链(quit_requested 连接目标≠_on_esc_quit)〔ESC批 V5〕") var quit_chain := FileAccess.get_file_as_string("res://presentation/main.gd") + FileAccess.get_file_as_string("res://presentation/main_esc_flow.gd") if not quit_chain.contains("MainEscFlow.on_quit(self)") or not quit_chain.contains("root.quit_game()"): problems.append("[esc] 退出调用链断裂(_on_esc_quit→_on_quit→_quit_game)〔ESC批 V5〕") if main.ui.screens.debug_badge.visible: problems.append("[esc] 浮窗开态 debug_badge 可见(发布态恒隐——float 类不注册 ModalStack 应零波及)〔ESC批 V8〕") push_esc.call() _settle() if main.ui.modals.camp.esc_overlay.visible: problems.append("[esc] 再按 Esc 未关浮窗(结构段收尾——Q2 单义切换)〔ESC批 V1〕") main._on_camp_closed() # 关营地复归菜单语境(后续选档段前提) _settle() # ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接), # sl1 三槽呈现/sl2 摘要三项逐槽读数/sl3 摘要禁项/sl4 操作集二分/sl9 重启链不预选。 # 隔离:attach_slot_template 注入命名空间(卡① 注入口);夹具按槽直写——槽1 有档异值、 # 槽2 空槽、槽3 坏档(Q2 甲=按空槽呈现→操作集与空槽同形)。---- var ssel_tpl := "user://smoke_ui_struct_probe_savesel_slot_%d.json" for i in range(1, 4): for suf in ["", ".tmp", ".corrupt.bak"]: var spp: String = ssel_tpl % i + suf if FileAccess.file_exists(spp): DirAccess.remove_absolute(spp) ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_savesel", true, false, true) main.save_manager.slots.attach_slot_template(ssel_tpl) # 注入口(激活态回零——sl9 重启链同构态) var f_s1 := FileAccess.open(ssel_tpl % 1, FileAccess.WRITE) f_s1.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 555, "adventure_total": 7, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}})) f_s1.close() # 槽1 有档异值 var f_s3 := FileAccess.open(ssel_tpl % 3, FileAccess.WRITE) f_s3.store_string("broken json not a save{{{") # 槽3 坏档(窥视 readable=false→按空呈现) f_s3.close() if main.save_manager.slots.active_slot() != 0: problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)") main.open_save_select() _settle() if not main.ui.modals.menu.save_select_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)") await _audit("savesel", [main.ui.modals.menu.save_select_overlay]) _save_select_contract(main, ssel_tpl) main._on_save_select_closed() _settle() if main.ui.modals.menu.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") main.ui.modals.menu.confirm_dialog.open("结构探针采样", "确认") _settle() await _audit("confirm", [main.ui.modals.menu.confirm_dialog]) main.ui.modals.menu.confirm_dialog.close() _settle() main._show_debug_overlay() _settle() await _audit("debug", [main.ui.modals.menu.debug_overlay]) main._close_debug_overlay() _settle() # ---- 冒险簇(真流推进:开冒险→地图选路态→选战斗节点→战斗 HUD)---- # oracle: design/ux/level-map.md:3 图上点击选路·候选按钮退役(冒险簇选路真点组) ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_adv", true, false, true) main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,结构采样确定性禁依赖默认分支) main._start_adventure_from_menu() _settle(4) await _audit("level_map", [main.ui.modals.run.level_screen]) var battle_id := _pick_battle_node(main) if battle_id >= 0: main.ui.modals.run.level_screen.route_chosen.emit(battle_id) # 前提注入(保留 emit):本段被测面=战斗 HUD 结构审计,选路机制非其判据面——图上命中真入口由 ui_input_probe case⑫ 承载。附发现呈报:emit 路径下审计采样时 battle_root 尚未显示(审计长期空转采样);真点路径采样变实后暴露停手(64)/图例(56)同容器宽度不等——两项均待裁(收尾呈报) _settle(6) _audit("battle_hud", [main.ui.battle.board_zone, main.ui.battle.info_panel, main.ui.battle.stage]) _standeefit_contract(main) # ---- 状态面板:组合根真实路径开层→结构断言→关闭。附只读性契约 #(R4——子树信号连接白名单巡检)与零动效契约(R5/P4 甲——零 AnimationPlayer/零 tween)。 # 巡检信号名清单=子树全控件的 get_signal_list() 全集(不放行任何未列名信号——新连接 # 一律红,须先入册审查);白名单={视图自身 _emit_closed, 组合根 _on_status_closed}。 main._open_status_panel() _settle() await _audit("status", [main.ui.modals.run.status_panel]) _status_readonly_contract(main) _status_zero_motion_contract(main) main._on_status_closed() _settle() if main.ui.modals.run.status_panel.visible: problems.append("[status] 关闭后层未隐藏(模态栈单口失守)") # ---- 图例层(旧模态、首过结构族):ap_bar legend_toggled # 信号真路径开(board_zone 信号→组合根 _on_legend_toggled→_open_modal)→结构断言族→ # closed 信号真路径关(close()→组合根 _on_modal_closed)。 # oracle: design/gdd/systems/表现与支撑/HUD框架.md:88 图例面板=模态族:统一登记序栈、入栈出栈、关闭逐层恢复(图例组) main.ui.battle.board_zone.ap_bar._btn_legend.pressed.emit() # 前提注入(保留 emit):emit 选路路径下 battle_root 未及显示且输入锁在(实证在案),真点不可达——图例开启的真输入承载随「route 信号路径屏幕切换时序」发现一并呈报待裁 _settle() if not main.ui.modals.run.legend_overlay.visible: problems.append("[legend] 图例层未打开(legend_toggled 路径中断)") await _audit("legend", [main.ui.modals.run.legend_overlay]) main.ui.modals.run.legend_overlay.close() _settle() if main.ui.modals.run.legend_overlay.visible: problems.append("[legend] 关闭后层未隐藏(模态栈单口失守)") else: problems.append("[battle_hud] 候选中无战斗节点(驱动中断——候选=%s)" % str(main.ui.modals.run.level_screen._cand_ids)) # ---- 兑付屏(真实 open 面 + 合成最小数据——结构断言只看矩形/宽度/可见性)---- main.ui.modals.run.end_overlay.open_victory("结构探针·胜利演算", "返回", Rect2(200, 200, 400, 300), {"gp_summary": "GP 0"}) _settle() await _audit("battle_end", [main.ui.modals.run.end_overlay]) main.ui.modals.run.end_overlay.close() _settle() # ---- 节点结算层(旧模态、首过结构族):rest 兑付真路径开 #(组合根 _open_modal 登记册+show_rest 合成喂入)→结构断言族→continue_pressed 信号真路径关 #(press_continue→组合根 _on_node_continue→_close_modal)。 main.ui.flow.open(main.ui.modals.run.node_overlay) main.ui.modals.run.node_overlay.show_rest(3, 15, 20) _settle() if not main.ui.modals.run.node_overlay.visible: problems.append("[node] 节点结算层未打开(rest 兑付路径中断)") await _audit("node_overlay", [main.ui.modals.run.node_overlay]) ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn) _settle() if main.ui.modals.run.node_overlay.visible: problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)") main.ui.modals.run.curio_overlay.open_pick("结构探针·三选一", [ {"id": "probe_heal", "text": "回血\n恢复 3 点"}, {"id": "probe_magic", "text": "爆裂轰击\n下一击 +5"}, {"id": "probe_pierce", "text": "穿甲\n无视护甲"}, ]) _settle() await _audit("curio_pick", [main.ui.modals.run.curio_overlay]) main.ui.modals.run.curio_overlay.visible = false _settle() main.ui.modals.menu.shop_overlay.open_shop([ {"product_id": "probe_p1", "display_name": "探针商品甲", "attr": "atk", "flat_value": 1, "price": 10, "purchased": 0}, {"product_id": "probe_p2", "display_name": "探针商品乙", "attr": "hp", "flat_value": 2, "price": 20, "purchased": 0}, ], 100) _settle() await _audit("shop", [main.ui.modals.menu.shop_overlay]) # ---- 术语传导:货架卡与三选一卡片的玩家面文案(物品名=专长获取、分类=专长凭证) main.ui.modals.menu.shop_overlay.open_shelf([ {"kind_id": "feat_ticket", "display_name": "专长获取", "value": 0, "price": 5}, {"kind_id": "heal", "display_name": "应急回复", "value": 6, "price": 5}, ], 20) _settle() var ticket_row: Dictionary = main.ui.modals.menu.shop_overlay._shelf_rows[0] if String(main.db.run.shop.pool_row("feat_ticket").get("display_name", "")) != "专长获取": problems.append("[shop_term] shop.tres 专长凭证件名未换代(现=%s)" % String(main.db.run.shop.pool_row("feat_ticket").get("display_name", ""))) if ticket_row.card.name_l.text != "专长获取": problems.append("[shop_term] 专长凭证件名未用物品名「专长获取」(现=%s)" % ticket_row.card.name_l.text) if ticket_row.btn.get_parent() != ticket_row.card.body or ticket_row.btn.get_index() >= ticket_row.card.foot_l.get_index(): problems.append("[shop_term] 购买钮未落在底注之前(add_footer 落位失败)") if ticket_row.card.desc_l.text != "开一次专长三选一": problems.append("[shop_term] 专长凭证件描述未用规范流程句(现=%s)" % ticket_row.card.desc_l.text) if ticket_row.card.foot_l.text != "专长凭证": problems.append("[shop_term] 专长凭证件分类标注未用「专长凭证」(现=%s)" % ticket_row.card.foot_l.text) var heal_row: Dictionary = main.ui.modals.menu.shop_overlay._shelf_rows[1] if heal_row.card.desc_l.text != "回复 +6(本冒险)": problems.append("[shop_term] 回复件描述漏了 heal 词表(现=%s)" % heal_row.card.desc_l.text) if heal_row.card.foot_l.text != "回复": problems.append("[shop_term] 回复件分类标注落到英文 kind(现=%s)" % heal_row.card.foot_l.text) var ticket_texts: Array = main._bonus._curio_card_texts([{"kind": "feat_ticket", "tier": "excellent"}]) var ticket_text := String((ticket_texts[0] as Dictionary).get("text", "")) var ticket_lines := ticket_text.replace("\r\n", "\n").split("\n") if ticket_lines.size() < 2 or ticket_lines[0] != "专长获取" or not ticket_lines[1].contains("专长三选一"): problems.append("[curio_term] 专长凭证三选一卡未用物品名/规范流程句(现=%s)" % ticket_text) if ticket_text.contains("门票") or ticket_text.contains("T5") or ticket_text.contains("持有集") or ticket_text.contains("奇物三选一"): problems.append("[curio_term] 专长凭证三选一卡残留退役词/内部任务号(现=%s)" % ticket_text) main.ui.modals.menu.shop_overlay.visible = false _settle() # ---- ⑦resume 专项:续玩停点恢复(main.gd:717 报错回归闸)---- # 三选一停点续玩分支在 level_started 之前同步 curio_choice_presented.emit(adventure_runner # resume 提前 return)——GP 总结 lambda 求值时 runner 尚未赋值,修复前 717 炸 null。 ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_resume", true) main.save_manager.set_breakpoint(_resume_bp_fixture()) main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) main._resume_from_menu() _settle(8) if not main.ui.modals.run.end_overlay.visible: problems.append("[resume] 续玩恢复后胜利总结面未开(三选一停点分支中断)") else: var found := {"ok": false} _find_label_text(main.ui.modals.run.end_overlay, "合计 24", found) if not found.ok: problems.append("[resume] 胜利总结面 GP 行未见快照合计「合计 24」(runner 缺位守卫缺位——717 回归)") var s: String = main.gp_summary_text({"gp_dropped": 0}) if not s.contains("合计 24"): problems.append("[resume] _gp_summary_text runner=null 守卫未取快照 gp(%s)" % s) # 恢复现场(resume 改写了 main 的 adventure/save 态——后续无屏依赖,仅为整洁) main.run_scope.level_active = false # ---- 判定 ---- if problems.is_empty(): print("[ui_struct_probe] PASS — 全屏结构断言零红") get_tree().quit(0) else: for p in problems: print("[ui_struct_probe] FAIL — %s" % p) get_tree().quit(1) # ⑦resume 夹具:最小合法断点快照(含生成图代差标记键)——bonus_pending 非空=三选一停点分支(合计 gp=24 供守卫兜底断言) func _resume_bp_fixture() -> Dictionary: return { "master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0, "level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [], "graph_gen_v2": 1, # 生成图换代代差标记键(缺键=换代前旧断点,resume_adventure 显式拒绝) "keystone_drops": [], "bonus_taken": {}, "bonus_pending": {"candidates": [{"kind": "heal", "tier": "common"}], "payload": {"gp_dropped": 0}}, } # 递归找含目标子串的 Label(⑦resume 断言读数面) func _find_label_text(node: Node, needle: String, found: Dictionary) -> void: for ch in node.get_children(): if ch is Label and String(ch.text).contains(needle): found.ok = true return _find_label_text(ch, needle, found) func _pick_battle_node(main: Node) -> int: # 入口/兑付停点走 level_screen.open_choices 直传(不写 _pending_candidates)—— # 候选从视图态取:_cand_ids × runner.nodes() 的 type(编号解析=node_id_of 单一出口) var ls: Node = main.ui.modals.run.level_screen if ls._map.runner == null: return -1 for nd in ls._map.runner.nodes(): if String(nd.get("type", "")) in ["battle", "boss"]: var nid: int = ls._map.runner.node_id_of(nd) if nid in ls._cand_ids: return nid return -1 # ⑧status 只读性契约(R4):状态面板子树全控件 × 游戏语义信号连接巡检——清单外零连接。 # 巡检信号名清单(需求卡片 v1.1「巡检信号名清单落码时列全进探针」)= 玩家交互可携带写意图的通道 # 全集(按钮按下/拨杆/文本/滑值/选项/键盘焦点)+ 本层外发信号 closed;引擎内部布局生命线信号 # 不计(child_order_changed/item_rect_changed/visibility_changed/size_flags_changed/ # minimum/maximum_size_changed 等系 Godot 容器系统对每控件自动建立的脏标记连接,与写通路无关 #——首跑实测踩中:全集巡检把布局生命线误判为非法连接)。清单外任何连接(尤其指向 # 购买/解锁/写入路径的)一律红,须先入册审查再改清单。 const _PAUSE_READONLY_SIGNALS: Array[String] = [ "pressed", "button_down", "button_up", "toggled", "text_changed", "text_submitted", "value_changed", "item_selected", "gui_input", "focus_entered", "focus_exited", "closed", ] func _status_readonly_contract(main: Node) -> void: # oracle: design/engineering/code-conventions.md:21 视图零核心写调用、依赖单向(status 只读巡检组) var overlay: Control = main.ui.modals.run.status_panel var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): var n: Node = stack.pop_back() for sig_name in _PAUSE_READONLY_SIGNALS: for conn in n.get_signal_connection_list(sig_name): conn_count += 1 var cb: Callable = conn["callable"] var host: Object = cb.get_object() var m := String(cb.get_method()) if host == overlay and m == "_emit_closed": continue if host == main and m == "_on_status_closed": continue problems.append("[status][readonly] 非法信号连接 %s.%s → %s.%s(零写通路巡检——白名单=视图 _emit_closed/组合根 _on_status_closed)" % [n.name, sig_name, host, m]) for ch in n.get_children(): stack.append(ch) if conn_count != 2: problems.append("[status][readonly] 信号连接数=%d 应=2(关闭钮 pressed+closed 外发——多连接=写通路嫌疑)" % conn_count) # ⑨status 零动效契约(R5/P4 甲——零动效直开直关路径存在断言): # 子树零 AnimationPlayer + 视图源码零 tween/动效包装调用(源码 token 扫描=ui_contrast 静态段同手法) func _status_zero_motion_contract(main: Node) -> void: var overlay: Control = main.ui.modals.run.status_panel var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() if n is AnimationPlayer: problems.append("[status][motion] 子树含 AnimationPlayer(R5 零动效破)") for ch in n.get_children(): stack.append(ch) var src := FileAccess.get_file_as_string("res://presentation/views/status_panel.gd") for token in ["create_tween", "UiKit.motion"]: if src.contains(token): problems.append("[status][motion] 视图源码含「%s」(R5 零动效直开直关破——P4 甲)" % token) # ---- 生涯页契约族(需求卡片契约 3/4+提纲 P3/P4 甲)---- # ⑩career 只读性契约(契约 3——状态面板 ⑧ 同形制):生涯页子树全控件 × 游戏语义信号连接巡检—— # 清单外零连接。白名单={视图自身 _emit_closed〔返回钮〕, 组合根 _on_career_closed};页内任何 # 购买/导航/写入方向的新连接一律红,须先入册审查。 func _career_readonly_contract(main: Node) -> void: # oracle: design/engineering/code-conventions.md:21 视图零核心写调用、依赖单向(career 只读巡检组) var overlay: Control = main.ui.modals.menu.career_overlay var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): var n: Node = stack.pop_back() for sig_name in _PAUSE_READONLY_SIGNALS: for conn in n.get_signal_connection_list(sig_name): conn_count += 1 var cb: Callable = conn["callable"] var host: Object = cb.get_object() var m := String(cb.get_method()) if host == overlay and m == "_emit_closed": continue if host == main and m == "_on_career_closed": continue problems.append("[career][readonly] 非法信号连接 %s.%s → %s.%s(零写通路巡检——白名单=视图 _emit_closed/组合根 _on_career_closed)" % [n.name, sig_name, host, m]) for ch in n.get_children(): stack.append(ch) if conn_count != 2: problems.append("[career][readonly] 信号连接数=%d 应=2(返回钮 pressed+closed 外发——多连接=写通路嫌疑)" % conn_count) # ⑪career 零动效契约(状态面板 ⑨ 同形制——P4 甲直开直关) func _career_zero_motion_contract(main: Node) -> void: var overlay: Control = main.ui.modals.menu.career_overlay var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() if n is AnimationPlayer: problems.append("[career][motion] 子树含 AnimationPlayer(零动效破)") for ch in n.get_children(): stack.append(ch) var src := FileAccess.get_file_as_string("res://presentation/views/career_overlay.gd") for token in ["create_tween", "UiKit.motion"]: if src.contains(token): problems.append("[career][motion] 视图源码含「%s」(零动效直开直关破)" % token) # ⑫模态登记/树序全量合同(V1——career 专属登记序契约推广到全部模态+屏壳; # 判据=main.ui.LAYER_CONTRACT 单一事实源:登记册序==合同模态序、直接子层树序==合同序逐对相等。 # 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈 # screen_state 登记)、camp_scene 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。 # _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖) # ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判) func _save_select_contract(main: Node, tpl: String) -> void: # oracle: design/gdd/systems/表现与支撑/存档序列化.md:61 三存档位独立文件、激活态由选档界面建立(选档契约组) var sso: Node = main.ui.modals.menu.save_select_overlay # sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1) var rows: VBoxContainer = sso._rows_box var live_rows := 0 for c in rows.get_children(): if c is Control and not c.is_queued_for_deletion(): live_rows += 1 if live_rows != 3: problems.append("[savesel] 槽卡行数=%d 应=3(sl1 三槽呈现)" % live_rows) var scroll_count := _count_scroll(sso) if scroll_count > 0: problems.append("[savesel] 子树出现滚动容器 ×%d(sl1 禁滚动长列表)" % scroll_count) # sl4 操作集二分(渲染面实测——扫真实按钮文本,不走簿记缝):槽1 有档=进档+删档零新档; # 槽2 空=新档零进档删档;槽3 坏档=按空槽呈现(Q2 甲) var occ: Dictionary = sso._occupied_flags if not (bool(occ.get(1, false)) and not bool(occ.get(2, false)) and not bool(occ.get(3, false))): problems.append("[savesel] 槽态判定失守(occ=%s 应=1有/2空/3坏按空)〔sl4〕" % str(occ)) var row_idx := 0 for row_c in rows.get_children(): if not (row_c is Control) or row_c.is_queued_for_deletion(): continue row_idx += 1 var btn_texts: Array[String] = [] _collect_button_texts(row_c, btn_texts) var has_load := btn_texts.has("进档") var has_del := btn_texts.has("删档") var has_new := btn_texts.has("新档") if row_idx == 1: if not (has_load and has_del and not has_new): problems.append("[savesel] 行1 操作集失守(有档槽应=进档+删档、零新档——渲染面实测)〔sl4〕") else: if not (has_new and not has_load and not has_del): problems.append("[savesel] 行%d 操作集失守(空/坏槽应=仅新档、零进档删档含置灰形态)〔sl4〕" % row_idx) # sl2 摘要三项逐槽读数:槽1 喂入值逐字(钥石 555/冒险成功 2 局/冒险 7 局) var sum1: Label = sso._summary_labels.get(1) if sum1 == null or sum1.text != "冒险成功 2 局 · 冒险 7 局": problems.append("[savesel] 槽1 摘要读数失守(got=%s 应=冒险成功 2 局 · 冒险 7 局)〔sl2〕" % (sum1.text if sum1 else "null")) var all_texts: Array[String] = [] _collect_labels(sso, all_texts) var has_keystone := false for t in all_texts: if t == "钥石 555": has_keystone = true if not has_keystone: problems.append("[savesel] 槽1 钥石徽读数缺失(应=钥石 555)〔sl2〕") # sl3 摘要禁项:断点/种子/「继续」/停点/「 关」(关数形——摘要「冒险成功 X 局 · 冒险 Y 局」不含「关」字)零命中 #(裁决 4——零未完冒险读数;词形覆盖=对照表 sl3 缺陷形态「断点关数拼进槽卡」的拦截面) for needle in ["断点", "种子", "继续", "停点", " 关"]: for t in all_texts: if t.contains(needle): problems.append("[savesel] 摘要禁项命中「%s」(label=「%s」)〔sl3〕" % [needle, t]) # sl9 重启链不预选:视图零选中态字段+喂入数据重建(无「已选」标记文本) for t in all_texts: if t.contains("已选"): problems.append("[savesel] 出现预选标记文本「%s」(sl9 Q1 甲不预选)" % t) func _count_scroll(node: Node) -> int: var count := 1 if node is ScrollContainer else 0 for c in node.get_children(): count += _count_scroll(c) return count func _collect_button_texts(node: Node, out: Array[String]) -> void: if node is Button and node.visible: out.append(String(node.text)) for c in node.get_children(): _collect_button_texts(c, out) func _collect_labels(node: Node, out: Array[String]) -> void: if node is Label: out.append(String(node.text)) for c in node.get_children(): _collect_labels(c, out) func _modal_registry_contract(main: Node) -> void: # oracle: design/engineering/code-conventions.md:54 模态覆盖层层级规则——新增覆盖层必须登记 z 序位置(登记序契约组) var list: Array = main.ui.flow.stack._list # ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源) var contract_modals: Array = [] for entry: Dictionary in main.ui.LAYER_CONTRACT: if String(entry["kind"]) != "modal": continue var n: Node = main.ui.node(String(entry["key"])) if n == null: problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"])) continue contract_modals.append(n) if list.size() != contract_modals.size(): problems.append("[registry] 登记册层数=%d 应=%d(合同模态数)〔装配生命批 V1〕" % [list.size(), contract_modals.size()]) for i in contract_modals.size(): if i >= list.size(): break if list[i] != contract_modals[i]: problems.append("[registry] 登记序第 %d 位与合同不一致(登记=%s 合同=%s)〔装配生命批 V1〕" % [ i, list[i].name, contract_modals[i].name]) # ② 直接子层树序==合同序逐对相等(屏壳/模态/全屏态/非视觉节点全链;宿住层随宿主不参与) var prev: Node = null for entry: Dictionary in main.ui.LAYER_CONTRACT: if entry.has("host"): continue var node: Node = main.ui.node(String(entry["shell"]) if entry.has("shell") else String(entry["key"])) if node == null: problems.append("[registry] 合同层缺失: %s〔装配生命批 V1〕" % String(entry["key"])) continue if prev != null and node.get_index() <= prev.get_index(): problems.append("[registry] 树序违约(合同邻接对 %s→%s:%d≤%d)〔装配生命批 V1〕" % [ prev.name, node.name, node.get_index(), prev.get_index()]) prev = node # ③ 宿住层特例:level_screen 宿主=冒险屏壳且在全直接子模态之下(遮挡语义归模态栈 screen_state) var lv: Node = main.ui.modals.run.level_screen if lv.get_parent() != main.ui.screens.adventure_screen: problems.append("[registry] level_screen 宿主应=冒险屏壳(实测 %s)〔装配生命批 V1〕" % lv.get_parent().name) elif lv.get_parent().get_index() >= main.ui.modals.run.legend_overlay.get_index(): problems.append("[registry] 冒险屏壳应在全模态之下(宿主 idx=%d legend idx=%d)〔装配生命批 V1〕" % [ lv.get_parent().get_index(), main.ui.modals.run.legend_overlay.get_index()]) # ④ 营地同槽特例:camp_screen 屏壳树序夹商店层与设置层之间(成员 camp_scene 登记位代言) var cs: Node = main.ui.screens.camp_screen if cs.get_index() <= main.ui.modals.menu.shop_overlay.get_index() or cs.get_index() >= main.ui.modals.menu.settings_overlay.get_index(): problems.append("[registry] camp_screen 屏壳树序应夹商店与设置层之间(idx=%d shop=%d settings=%d)〔装配生命批 V1〕" % [ cs.get_index(), main.ui.modals.menu.shop_overlay.get_index(), main.ui.modals.menu.settings_overlay.get_index()]) # ⑤ 分域结构(句柄分域批 T1——两新子容器在座/平铺面清零/编排口独立) # V1a 分域在座:run/menu 各六句柄经新二段式寻址逐件非 null(12 检) var run_names: Array[String] = ["legend_overlay", "node_overlay", "level_screen", "curio_overlay", "end_overlay", "status_panel"] var menu_names: Array[String] = ["shop_overlay", "settings_overlay", "career_overlay", "save_select_overlay", "confirm_dialog", "debug_overlay"] for n in run_names: if main.ui.modals.run.get(n) == null: problems.append("[registry] run 域句柄缺失: %s〔句柄分域批 V1〕" % n) for n in menu_names: if main.ui.modals.menu.get(n) == null: problems.append("[registry] menu 域句柄缺失: %s〔句柄分域批 V1〕" % n) # V1b 平铺面清零:12 旧平铺名经 get() 逐件为 null(旧寻址成员不存在——纯搬家零别名) for n in run_names + menu_names: if main.ui.modals.get(n) != null: problems.append("[registry] 平铺残留: modals.%s 应已迁域(get 非 null)〔句柄分域批 V1〕" % n) # V1c 编排口独立:ModalFlow 直驻注册面(ui.flow 非 null)且 modals 无 flow 成员 if main.ui.flow == null: problems.append("[registry] ui.flow 直驻缺失〔句柄分域批 V1〕") if main.ui.modals.get("flow") != null: problems.append("[registry] modals.flow 残留(应升格独立持有)〔句柄分域批 V1〕") # ⑬career 零跳转契约(提纲 P4 甲——页内零「前往他处」入口:除「返回」外任何导航钮=红) func _career_nojump_contract(main: Node) -> void: # oracle: design/ux/camp.md:19 生涯记录入口与返回钮口径由结构校验兜底——页内零导航入口(零跳转契约组) for nav in ["营地", "商店", "设置", "冒险", "继续上局", "退出", "前往"]: if ProbeUiUtil.find_button(main.ui.modals.menu.career_overlay, nav, "contains") != null: problems.append("[career][nojump] 页内出现导航入口「%s」(零跳转违约——生涯纯只读)" % nav) # ⑥standee 专项:B 区站桩内容矩形 ⊆ 舞台矩形(高度读布局表;内缩 8px)且不被裁切 # (名行基线离盒底 1px 同型高危写法返修回归闸; # 内容矩形=StandeeBody.content_rect(),最低绘制元素=底座下缘) func _standeefit_contract(main: Node) -> void: # oracle: design/ux/battle.md:154 主角站桩立绘(无框立绘+落影)——站桩内容矩形⊆舞台(站桩适配组) var st: Node = main.ui.battle.stage if st == null or st.hero_standee == null: problems.append("[standee] 舞台/主角站桩缺失(断言中断)") return var stage_gr: Rect2 = st.get_global_rect() var bodies: Array = [st.hero_standee] for c in st._cards: bodies.append(c) for body in bodies: if body == null or not is_instance_valid(body): continue var content := Rect2(st.world_to_shell(body.position), body.content_rect().size) if not stage_gr.encloses(content): problems.append("[standee] %s 内容矩形 %s 越出舞台矩形 %s(N2 站桩出框回归)" % [body.name if body.name != "" else "standee", content, stage_gr]) elif content.end.y > stage_gr.end.y - 8.0: problems.append("[standee] %s 内容底缘 %s 距舞台底 <8px(N2 纵向余量回归,舞台底=%s)" % [body.name if body.name != "" else "standee", content.end.y, stage_gr.end.y]) func _settle(frames := 4) -> void: for i in range(frames): await get_tree().process_frame await get_tree().process_frame # 布局排序系帧尾 call_deferred——末帧后再保一帧 # ---- 采样与断言 ---- func _visible_children(root: Control) -> Array: var out: Array = [] var stack: Array = [root] while not stack.is_empty(): var c: Control = stack.pop_back() if c == null: continue if c.is_visible_in_tree(): out.append(c) for ch in c.get_children(): if ch is Control: stack.append(ch) return out func _buttons(root: Control) -> Array: var out: Array = [] for c in _visible_children(root): if c is Button and not c.disabled: out.append(c) return out func _wl(screen: String, rule: String, ctl: Control) -> bool: var s: Dictionary = WL.get(screen, {}) if s.is_empty(): return false var names: Array = s.get(rule, []) if names.is_empty(): return false return String(ctl.name) in names func _force_layout(root: Control) -> void: # modal 屏首次 visible 后容器排序不自动传播(size 已算而 position 停 0——4.7 行为)—— # 采样前对子树全部 Container 强制 queue_sort var stack: Array = [root] while not stack.is_empty(): var c: Control = stack.pop_back() if c is Container: c.queue_sort() for ch in c.get_children(): if ch is Control: stack.append(ch) func _in_scroll(c: Control) -> bool: var p := c.get_parent() while p is Control: if p is ScrollContainer: return true p = p.get_parent() return false func _audit(screen: String, roots: Array) -> void: # oracle: design/engineering/architecture/architecture.md:168 组合根交接面与呈现装配——屏结构审计宿主面(结构审计组) for r in roots: if r != null and is_instance_valid(r): _force_layout(r) await _settle(1) if OS.get_environment("STRUCT_PROBE_DEBUG") == "1": for r in roots: if r != null and is_instance_valid(r): _dump(r, 0) var all_ctls: Array = [] var all_buttons: Array = [] for r in roots: if r == null or not is_instance_valid(r): problems.append("[%s] 采样根缺失" % screen) return all_ctls += _visible_children(r) all_buttons += _buttons(r) var vp := get_viewport().get_visible_rect() # ①overlap:操作钮两两不相交 for i in range(all_buttons.size()): for j in range(i + 1, all_buttons.size()): var a: Control = all_buttons[i] var b: Control = all_buttons[j] if _wl(screen, "overlap", a) or _wl(screen, "overlap", b): continue # 滚动区跨界豁免(实证:当局数据区块加高列表后,恰好一枚在 ScrollContainer # 内时,其未剪裁内容矩形与滚区外控件的全局矩形交叠是滚动态样貌非布局缺陷; # 同滚区两钮仍查——同一容器 stacking 真重叠照红) if _in_scroll(a) != _in_scroll(b): continue if a.get_global_rect().intersects(b.get_global_rect()): problems.append("[%s][overlap] %s 与 %s 矩形相交(%s ∩ %s)" % [ screen, a.name, b.name, a.get_global_rect(), b.get_global_rect()]) # ②equal_width + equal_height:同一直接父容器内 ≥2 可见按钮宽度/高度差 >1px #(钮高族判据「对齐好、不要细」——主钮 26px 对常规 38px 不齐回归闸;纪律=ui_kit BUTTON_MIN_H) var groups := {} for b in all_buttons: var k: Node = b.get_parent() if not groups.has(k): groups[k] = [] groups[k].append(b) for k in groups: var g: Array = groups[k] if g.size() < 2: continue var w0: float = (g[0] as Control).size.x var h0: float = (g[0] as Control).size.y var diff := false var diff_h := false for b in g: if _wl(screen, "equal_width", b): diff = false diff_h = false break if absf((b as Control).size.x - w0) > 1.0: diff = true if absf((b as Control).size.y - h0) > 1.0: diff_h = true if diff: var names: Array = [] var widths: Array = [] for x in g: names.append(String((x as Control).name)) widths.append((x as Control).size.x) problems.append("[%s][equal_width] 同容器按钮宽度不一(%s——%s)" % [screen, str(names), str(widths)]) if diff_h: var names_h: Array = [] var heights: Array = [] for x in g: names_h.append(String((x as Control).name)) heights.append((x as Control).size.y) problems.append("[%s][equal_height] 同容器按钮高度不一(%s——%s)" % [screen, str(names_h), str(heights)]) # ③offscreen + ④zero_size(ScrollContainer 内容坐标系超出=可滚动设计内——豁免) for c in all_ctls: if _in_scroll(c): continue var gr: Rect2 = c.get_global_rect() if not _wl(screen, "offscreen", c) and not gr.intersects(vp): problems.append("[%s][offscreen] %s 完全出视口(%s)" % [screen, c.name, gr]) if not _wl(screen, "zero_size", c) and (c.size.x <= 0.0 or c.size.y <= 0.0): problems.append("[%s][zero_size] %s 零尺寸(%s)" % [screen, c.name, c.size]) func _dump(c: Control, depth: int) -> void: var ind := " ".repeat(depth) print("[dump] %s%s(%s) pos=%s gpos=%s size=%s vis=%s" % [ind, c.name, c.get_class(), c.position, c.get_global_position(), c.size, c.is_visible_in_tree()]) for ch in c.get_children(): if ch is Control: _dump(ch, depth + 1) # ⑤start 专项:左列/右列矩形不相交(M2 双列契约——装配件命名 start_box/start_right 为探针锚点) func _start_columns_contract(main: Node) -> void: var box: Control = main.ui.screens.start_overlay.find_child("start_box", true, false) var right: Control = main.ui.screens.start_overlay.find_child("start_right", true, false) if box == null or right == null: problems.append("[start][columns] 左列/右列节点缺失(start_box/start_right——装配命名失守)") return if box.get_global_rect().intersects(right.get_global_rect()): problems.append("[start][columns] 左列 %s 与右列 %s 相交(双列叠放回归——M2 缺陷闸)" % [ box.get_global_rect(), right.get_global_rect()]) # AnimationPlayer 全子树收集(零动效契约扫描面——_buttons 同款递归形态) func _count_animplayers(node: Node) -> int: var out := 0 if node is AnimationPlayer: out += 1 for child in node.get_children(): out += _count_animplayers(child) return out