Files
gplan-2/game/smoke/ui_tune_probe.gd
T
DKChan 6b1e447d27 feat(调试入口): 全局浮层角标——UI 最顶层、任何 UI 不遮挡(2026-10-16 用户令)
用户定稿:调试入口不做置顶钮式按需出现,要的是「UI 最顶层浮层、不被任何 UI 遮挡」。
实现=三枚屏内角标(菜单右下/战斗 HUD 右下/地图顶栏)合并为一枚全局浮层角标:

- LAYER_CONTRACT 末位新增 {"key": "debug_badge", "kind": "float"}——只落树序(最顶绘制序)、
  不注册 ModalStack:open() 遮挡记账只隐藏登记层,故任何模态/覆盖层开都不遮它(用户需求
  的机制根因);_layer_node 补映射;main 直接子层、构建走 ui_assembler.build_debug_badge
- 位置:右下角 16px 惯例位;地图屏底栏 85px 常驻→_sync_status_entry 按 map_on 抬 offset
  至底栏上方 16px;战斗屏状态角标经 MarginContainer 让位(right 16/bottom 52)不再被压
- 屏内角标全退役:menu_screen/board_zone_view/level_map_screen 三处 debug_badge 删除,
  main 两处接线与 _sync_status_entry 四条可见性/置灰行清除(浮层常显不置灰——点它就是开浮窗)
- 浮窗 desc 文案改「UI 最顶层「调试」浮层角标随时可开——任何界面不遮挡」
- 断言/探针换代:smoke V2f 查 main.debug_badge boot 可见;click_probe 遮挡判据移至设置面
  开中段(浮层角标此时仍应可见=直接验「不遮挡」);tune_probe ①点击路径与⑨对局中角标
  全改 main.debug_badge(T2 防线断言不动——浮窗 non_locking×遮挡复活×has_open 组合回归)

冒烟 1127/0 过;全量闸待跑。
2026-10-16 23:53:57 +08:00

238 lines
15 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-11(加成应用契约 = 组合根单一应用路径)
# 调试浮窗场景探针(原数值调整板收进 debug_overlay;
# 角标挂宿主 / 启动即开 / 当局数据只读区块 / 输入锁防护配套):
# 驱动真实 UI 路径——①全局浮层角标开窗(UI 最顶层「调试」钮点击路径)+悬浮菜单之上+登记册末位树序
# ②滑条联动 ③应用 ④重置 ⑤批量模拟 N=10 ⑥关闭 ⑦hero 表运行期不可变
# ⑧单次加成生效(经 _start_adventure_from_menu 真实路径+显式种子钉死)+开冒险路径显式关浮窗
# ⑨对局中可开(战斗屏角标真实信号路径)+棋盘输入不锁+状态角标不置灰(停机点回归闸)
# +对局中两钮禁用+当局数据六项值对(与只读访问器逐项一致)+只读巡检(白名单外零连接)+零动效契约
# ⑩种子三分单点断言(显式输入优先/首局恒 42/非首局空框连抽 4 次不全等 42)
# 运行:Godot_console.exe --headless --path . res://smoke/ui_tune_probe.tscn
extends Node
const ProbeUiUtil := preload("res://smoke/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 = []
# 基线读表(2026-09-18 随数值压缩批改表值驱动——旧字面量 6/50、12/75 已随 hero 表压缩失效)
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%
# ① 全局浮层角标开窗(2026-10-16 用户令:调试入口=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 参数链上同样被抓获
# 存档隔离(迭代 8 卡①——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律;
# isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退)
ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, false)
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))
# 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照
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+ 停机点回归闸):战斗屏角标真实信号路径开窗;
# 棋盘输入不锁、状态角标不置灰(旧 has_open 判据在「默认开×遮挡复活」下必锁死的破约回归面);
# 对局中批量模拟/两层调试两钮禁用;当局数据六项与只读访问器逐项一致;关闭钮真实路径收窗恢复
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 ①失守)")
# 迭代 27 T1——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)