# VERTICAL SLICE - NOT FOR PRODUCTION # 加成应用契约 = 组合根单一应用路径 # 调试浮窗场景探针(原数值调整板收进 debug_overlay; # 角标挂宿主 / 启动即开 / 当局数据只读区块 / 输入锁防护配套): # 驱动真实 UI 路径——①全局浮层角标开窗(UI 最顶层「调试」钮点击路径)+悬浮菜单之上+登记册末位树序 # ②滑条联动 ③应用 ④重置 ⑤批量模拟 N=10 ⑥关闭 ⑦hero 表运行期不可变 # ⑧单次加成生效(经 _start_adventure_from_menu 真实路径+显式种子钉死)+开冒险路径显式关浮窗 # ⑨对局中可开(战斗屏角标真实信号路径)+棋盘输入不锁+状态角标不置灰(停机点回归闸) # +对局中两钮禁用+当局数据六项值对(与只读访问器逐项一致)+只读巡检(白名单外零连接)+零动效契约 # ⑩种子三分单点断言(显式输入优先/首局恒 42/非首局空框连抽 4 次不全等 42) # 运行:Godot_console.exe --headless --path . res://probe/ui_tune_probe.tscn extends Node const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # T6 收口:隔离样板单点 const SIM_GUARD_FRAMES := 1800 # ⑤ N=10 headless 模拟防挂死帧预算上限(探针内停滞检测第一道——坏态不等满预算才报,超时即断言失败) # 只读巡检信号名清单(17卡③「白名单外零连接」形制沿用——玩家交互可携带写意图的通道全集 # +本层外发 closed;引擎内部布局生命线信号不计) const _OVERLAY_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", ] # 视图内白名单方法(fx 切换/应用/重置/模拟/两层调试/诊断转储/关闭/滑条联动——全部视图自身信号外发前置, # 零跨对象写;清单外任何新连接一律红须入册) const _OVERLAY_VIEW_METHODS: Array[String] = ["_on_fx_toggle", "_apply_btn_pressed", "_reset_btn_pressed", "_emit_simulate_requested", "_emit_debug_levels", "_emit_diagnose_requested", "_emit_closed", "_on_slider_value_changed"] func _ready() -> void: await get_tree().process_frame var main: Node = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame var problems: Array = [] # 基线读表(表值驱动——不写字面量) var base_atk := int(main.db.hero.atk) var base_hp := int(main.db.hero.max_hp) var boosted_atk := base_atk * 2 # 攻击滑条 100 → ×2 var boosted_hp := int(round(base_hp * 1.5)) # 生命滑条 50 → +50% # ① 全局浮层角标开窗(调试入口=UI 最顶层浮层,任何 UI 不遮挡—— # main 直接子层、LAYER_CONTRACT 末位 float 类;boot 菜单优先不开浮窗,角标真实点击路径开窗; # 悬浮于菜单之上——开始层不隐藏;树序新基准=登记册末位(确认弹窗之后=绘制序最上)) main.debug_badge.pressed.emit() await get_tree().process_frame if not main.debug_overlay.visible: problems.append("调试浮层角标点击后浮窗未开(全局浮层入口失守)") if not main._start_overlay.visible: problems.append("开始覆盖层被隐藏——浮窗应悬浮于菜单之上(14.5 形态失守)") if main.debug_overlay.get_index() < main.confirm_dialog.get_index(): problems.append("调试浮窗树序不在确认弹窗之后——登记册末位失守") # ② 滑条 → 生效预览联动 main.debug_overlay.set_sliders(100, 50) if str(boosted_atk) not in main.debug_overlay.effect_text() or str(boosted_hp) not in main.debug_overlay.effect_text(): problems.append("生效预览未联动(%s)" % main.debug_overlay.effect_text()) # ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照 main.debug_overlay.press_apply_for_probe() # ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线) main._show_debug_overlay() main.debug_overlay.press_reset_for_probe() # ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位; # 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕) main.debug_overlay.set_sliders(100, 0) main.debug_overlay.set_sim_count(10) main.debug_overlay.press_simulate_for_probe() var guard := SIM_GUARD_FRAMES while main.debug_overlay.is_running() and guard > 0: guard -= 1 await get_tree().process_frame if main.debug_overlay.is_running(): problems.append("模拟未在预算帧内完成(疑似挂死)") var out: String = main.debug_overlay.output_text() if not out.contains("通关率"): problems.append("模拟输出缺通关率") if not out.contains("败北节点"): problems.append("模拟输出缺败北节点") if "N=10" not in out: problems.append("模拟输出 N 标注错误(%s)" % out.substr(0, 40)) # ⑥ 关闭 → 浮窗收起、开始层全程在屏 main._close_debug_overlay() if main.debug_overlay.visible or not main._start_overlay.visible: problems.append("关闭后层级状态错误") # ⑦ hero 表运行期不可变(E4 收口回归断言:任何代码 reintroduce 表写在此被抓获——原"表值残留"断言语义升级) if int(main.db.hero.atk) != base_atk or int(main.db.hero.max_hp) != base_hp: problems.append("hero 表被运行时改写(E4 违例:atk=%s hp=%s,期望恒基线 %d/%d)" % [str(main.db.hero.atk), str(main.db.hero.max_hp), base_atk, base_hp]) # ⑧ 单次加成生效回归断言:应用加成 + 启动单场战斗 → 呈现快照 hero 必须是"基线 × 单次加成" # (攻击 ×2 / 生命 +50%)——旧缺陷(残留值被当基线再乘)在 encounter 参数链上同样被抓获 # 存档隔离(停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律; # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档) main.debug_overlay.set_sliders(100, 50) main.debug_overlay.press_apply_for_probe() main.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed) main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言) await get_tree().process_frame await get_tree().process_frame # ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——调试会话默认开态下开局不带窗,对局中经角标再开) if main.debug_overlay.visible: problems.append("开冒险路径未显式关浮窗(T2 防护失守)") if int(main.run_scope.adventure.master_seed) != 42: problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed)) # 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 if main.level_screen.visible: main.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero if int(hero.get("atk", -1)) != boosted_atk or int(hero.get("max_hp", -1)) != boosted_hp: problems.append("战斗加成单次生效违例(atk=%s max_hp=%s,期望 %d/%d)" % [ str(hero.get("atk", "?")), str(hero.get("max_hp", "?")), boosted_atk, boosted_hp]) # ⑨ 对局中可开(18卡⑦ P2 甲/T2 防护——S1+ 停机点回归闸):战斗屏角标真实信号路径开窗; # 棋盘输入不锁、状态角标不置灰(「默认开×遮挡复活」下必锁死的破约回归面); # 对局中批量模拟/两层调试两钮禁用;当局数据六项与只读访问器逐项一致;关闭钮真实路径收窗恢复 var dbg_badge: Button = main.debug_badge if dbg_badge == null or not dbg_badge.visible: problems.append("对局中调试浮层角标未点亮(全局浮层最顶层失守)") else: if dbg_badge.disabled: problems.append("战斗屏调试角标被置灰(T2 排除式判据失守)") dbg_badge.pressed.emit() # 真实按钮信号路径 → 组合根 _on_debug_badge_pressed → C6 单口 if not main.debug_overlay.visible: problems.append("对局中角标开浮窗失败(P2 甲对局中可达失守)") if main._input_locked(): problems.append("对局中开浮窗后棋盘输入锁死(T2 防护失守——S1+ 停机点)") if main.board_zone.status_badge.disabled: problems.append("对局中开浮窗后状态角标置灰(T2 防护失守——S1+ 停机点)") if main.debug_overlay.is_running(): problems.append("对局中上下文锁定误判为模拟在跑(is_running 语义互扰)") if not main.debug_overlay._sim_btn.disabled or not main.debug_overlay._dbg_btn.disabled: problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)") # 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算) var rd: Dictionary = main.debug_overlay.run_data_probe() var r = main.run_scope.adventure.runner() if String(rd.get("seed", "")) != "42": problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed"))) if int(rd.get("level", -1)) != int(main.run_scope.adventure.level_index): problems.append("当局数据关卡失对(%s ≠ %s)" % [str(rd.get("level")), str(main.run_scope.adventure.level_index)]) if int(rd.get("node", -999)) != int(r.node_index): problems.append("当局数据节点失对(%s ≠ %s)" % [str(rd.get("node")), str(r.node_index)]) if String(rd.get("hp", "")) != "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())]: problems.append("当局数据 HP 失对(%s)" % str(rd.get("hp"))) if int(rd.get("gp", -1)) != int(r.gp): problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)]) if int(rd.get("keystone", -1)) != int(main.run_scope.adventure.keystone): problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.keystone)]) var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix") if close_btn == null: problems.append("浮窗关闭钮缺失(真实收窗路径中断)") else: close_btn.pressed.emit() if main.debug_overlay.visible or main._input_locked(): problems.append("对局中关闭浮窗后态失守(visible=%s locked=%s)" % [str(main.debug_overlay.visible), str(main._input_locked())]) # ⑨b 只读巡检(17卡③形制)+零动效契约(17卡③ R5——零 AnimationPlayer+源码零 tween) _overlay_readonly_contract(main, problems) _overlay_zero_motion_contract(main, problems) # ⑩ 种子三分单点断言(18卡⑦ C1——判定只落 _seed_value() 单点;对局态不影响本判定面): # 显式合法整数全场景优先(含首局)→ 首局(存档布尔 false)恒 42 → 非首局空框引擎熵连抽不全等 42 main.debug_overlay.seed_edit.text = "777" if main._seed_value() != 777: problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)") # SaveManager 是 autoload 单例——换注走原地 attach(首局态挂载+恢复隔离档); # --script 模式无裸 autoload 标识符——经 main 节点路径解析 var sm = main.get_node("/root/SaveManager") sm.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面) main.debug_overlay.seed_edit.text = "" if main._seed_value() != 42: problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") sm.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位) var draws: Array[int] = [] for i in range(4): draws.append(main._seed_value()) var all42 := true for d in draws: if d == 42: all42 = all42 and true else: all42 = false if all42: problems.append("非首局空框连抽 4 次全等 42(随机化失守——C1 ③)") main.debug_overlay.seed_edit.text = "42" # 钉回显式值(现场整洁) for f0 in ["user://smoke_tune_probe_seed.json", "user://smoke_tune_probe_seed.json.tmp"]: if FileAccess.file_exists(f0): DirAccess.remove_absolute(f0) if problems.is_empty(): print("[tune_probe] PASS — 启动即开/联动/应用/重置/模拟/关闭/基线无残留/单次加成/对局中可开+当局数据+只读巡检/种子三分 全链路通过(模拟 N=10 输出 %.30s…)" % out) get_tree().quit(0) else: for p in problems: push_error("[tune_probe] FAIL — " + p) print("[tune_probe] FAIL — " + "; ".join(problems)) get_tree().quit(1) # 只读巡检(18卡⑦ T3——17卡③「白名单外零连接」形制沿用):浮窗子树全控件 × 游戏语义信号名清单 # 全连接巡检——白名单={视图自身八方法 + 组合根 _on_modal_closed(closed 外发消费)}; # 当局数据区块=纯标签零连接在此被一并约束(新增任何连接=红须入册审查)。 func _overlay_readonly_contract(main: Node, problems: Array) -> void: var overlay: Control = main.debug_overlay var stack: Array = [overlay] var conn_count := 0 while not stack.is_empty(): var n: Node = stack.pop_back() for sig_name in _OVERLAY_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()) var ok := (host == overlay and m in _OVERLAY_VIEW_METHODS) \ or (host == main and m == "_on_modal_closed") \ or (host is VScrollBar or host is HScrollBar) # 引擎滚动机构内部 drag 接线(RichTextLabel 自动滚动条——17卡③布局生命线同类豁免,零游戏写通路) if not ok: problems.append("[readonly] 非法信号连接 %s.%s → %s.%s(白名单外零连接巡检——18卡⑦ T3)" % [n.name, sig_name, host, m]) for ch in n.get_children(): stack.append(ch) if conn_count != 11: problems.append("[readonly] 信号连接数=%d 应=11(fx切换/应用/重置/模拟/两层调试/诊断转储/关闭七钮+滑条2+closed 外发+引擎滚动条 drag 1——多连接=写通路嫌疑)" % conn_count) # 零动效契约(17卡③ R5 形制——子树零 AnimationPlayer + 视图源码零 tween/动效包装调用) func _overlay_zero_motion_contract(main: Node, problems: Array) -> void: var overlay: Control = main.debug_overlay var stack: Array = [overlay] while not stack.is_empty(): var n: Node = stack.pop_back() if n is AnimationPlayer: problems.append("[motion] 浮窗子树含 AnimationPlayer(零动效契约破)") for ch in n.get_children(): stack.append(ch) var src := FileAccess.get_file_as_string("res://presentation/views/debug_overlay.gd") for token in ["create_tween", "UiKit.motion"]: if src.contains(token): problems.append("[motion] 浮窗视图源码含「%s」(零动效直开直关破——17卡③契约)" % token)