diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 4bcabd8c..d7adb7d2 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -1241,7 +1241,8 @@ func _sync_debug_overlay() -> void: # 当局数据六项只读投影(P4 甲——C4 零直读内部状态、零写通路):种子=adventure.master_seed 现值 # (非 _seed_value() 重算);调试两层/非对局无冒险对象=RNG 服务 master_seed 现值(E5 只读出口, -# 未播种 -1 显「—」);其余五项=runner 成文出口;缺位=「—」占位。零动效(纯标签重查——17卡③契约)。 +# 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域); +# 缺位=「—」占位。零动效(纯标签重查——17卡③契约)。 func _debug_run_data() -> Dictionary: var r = adventure.runner() if adventure != null else null var seed_now: int = _current_master_seed() @@ -1249,7 +1250,7 @@ func _debug_run_data() -> Dictionary: if r == null: return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} return {"seed": seed_txt, "level": int(adventure.level_index), "node": int(r.node_index), - "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(r.keystone)} + "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(adventure.keystone)} # 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值; @@ -1283,12 +1284,12 @@ func _seed_value() -> int: # 种子三分判定单点(迭代 18 卡⑦ C1——全工程唯一种子框读取处,禁第二处读框): # ①显式合法整数全场景优先(含首局——测试/调试接口显式指定;第五路 DebugSession Callable # 经本函数=调试对局与玩家同口径,P6 甲); - # ②首局固定 42(P5 甲——存档现役布尔 is_first_adventure_done:新档显式 false=教学局, - # 老档缺省 true=有游玩史;教学重放模板 0 即在此种子下标定); + # ②首局固定 42(P5 甲——存档现役布尔 is_first_adventure_done:新档显式 false=教学局恒 42〔教学 + # 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机); # ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service # 封闭流、禁 randomize()/自建 RNG——组合根建种子属 ADR-0007 豁免面,不进玩法 RNG 消费) if _seed_edit.text.is_valid_int(): return int(_seed_edit.text) - if save_manager != null and not save_manager.is_first_adventure_done(): + if save_manager != null and save_manager.is_first_adventure_done(): return randi() return 42 diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 4913197f..ef0fff8c 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -204,9 +204,7 @@ func _build() -> void: fx_row.add_child(UiKit.label("胜利演出",UiKit.FS_HINT, false, UiKit.WARM)) var fx_btn := UiKit.button("纹章拼合" if UiKit.victory_fx_style == "crest" else "尘金粒子") fx_btn.tooltip_text = "两套演出并存——点击切换(会话级,不落盘)" - fx_btn.pressed.connect(func() -> void: - UiKit.victory_fx_style = "dust" if UiKit.victory_fx_style == "crest" else "crest" - fx_btn.text = "纹章拼合" if UiKit.victory_fx_style == "crest" else "尘金粒子") + fx_btn.pressed.connect(_on_fx_toggle.bind(fx_btn)) # 迭代 18 卡⑦——lambda 转命名方法(只读巡检白名单方法名精确可判) fx_row.add_child(fx_btn) # 区块①随机种子(原开始覆盖层种子行迁入——种子三分判定归组合根 _seed_value 单点): @@ -310,3 +308,9 @@ func _emit_closed() -> void: func _on_slider_value_changed(_value: float) -> void: _sync_labels() + + +# 胜利演出切换(原 _build 内 lambda——迭代 18 卡⑦ 转命名方法,只读巡检白名单承载) +func _on_fx_toggle(btn: Button) -> void: + UiKit.victory_fx_style = "dust" if UiKit.victory_fx_style == "crest" else "crest" + btn.text = "纹章拼合" if UiKit.victory_fx_style == "crest" else "尘金粒子" diff --git a/game/smoke/ui_tune_probe.gd b/game/smoke/ui_tune_probe.gd index a86abfd2..0bfc9cfa 100644 --- a/game/smoke/ui_tune_probe.gd +++ b/game/smoke/ui_tune_probe.gd @@ -1,9 +1,14 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-11(2026-09-12 code-review 修复批重写——加成应用契约改为组合根单一应用路径) -# 调试浮窗场景探针(2026-10-04 迭代 14.5 调试整合批随批改接——原数值调整板整体收进 debug_overlay, -# 浮窗形态悬浮于开始菜单之上不隐藏菜单;playtest 缺陷⑤回归载体沿袭): -# 驱动真实 UI 路径——打开可见性(浮于菜单之上+树序在后)/ 滑条联动 / 应用(apply_requested → 组合根)/ -# 重置 / 批量模拟全链路(N=10)/ E4 回归断言(hero 表运行期不可变 + 单次加成经 encounter 生效)。 +# 调试浮窗场景探针(2026-10-04 迭代 14.5 调试整合批随批改接——原数值调整板整体收进 debug_overlay; +# 2026-10-07 迭代 18 卡⑦ T4 断言面换代——P2 甲角标迁宿主 / P3 甲启动即开 / T3 当局数据只读区块 / +# T2 输入锁防护落码配套): +# 驱动真实 UI 路径——①启动即开(DS_DEBUG=1 默认打开,探针自设环境变量)+悬浮菜单之上+登记册末位树序 +# ②滑条联动 ③应用 ④重置 ⑤批量模拟 N=10 ⑥关闭 ⑦hero 表运行期不可变 +# ⑧单次加成生效(经 _start_adventure_from_menu 真实路径+显式种子钉死)+开冒险路径显式关浮窗 +# ⑨对局中可开(战斗屏角标真实信号路径)+棋盘输入不锁+暂停角标不置灰(T2 防护=S1+ 停机点回归闸) +# +对局中两钮禁用+当局数据六项值对(与只读访问器逐项一致)+只读巡检(白名单外零连接)+零动效契约 +# ⑩种子三分单点断言(显式输入优先/首局恒 42/非首局空框连抽 4 次不全等 42——C1/P5 甲/P6 甲载体) # 运行:Godot_console.exe --headless --path . res://smoke/ui_tune_probe.tscn extends Node @@ -11,8 +16,21 @@ const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔 const SIM_GUARD_FRAMES := 1800 # ⑤ N=10 headless 模拟防挂死帧预算上限(ADR-0004:探针内停滞检测第一道——坏态不等满预算才报,超时即断言失败) +# 只读巡检信号名清单(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_closed", "_on_slider_value_changed"] + func _ready() -> void: + OS.set_environment("DS_DEBUG", "1") # P3 甲断言前提——探针自设调试会话环境(headless 进程内生效) await get_tree().process_frame var main: Node = load("res://presentation/main.tscn").instantiate() add_child(main) @@ -23,15 +41,15 @@ func _ready() -> void: 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% - # ① 打开可见性(14.5 浮窗形态:悬浮于菜单之上——开始层不隐藏,树序必须在开始层之后 = 绘制在上; - # 旧「打开必须隐藏开始层」断言随互斥形态退役) - main._show_debug_overlay() + # ① 启动即开(18卡⑦ P3 甲——DS_DEBUG=1 会话组合根 _ready 即开;悬浮于菜单之上——开始层不隐藏; + # 树序新基准=登记册末位(确认弹窗之后=绘制序最上);旧「_show_debug_overlay 打开+开始层在屏」 + # 断言随入口迁宿主换代——打开动作本身移至 ⑨ 角标真实路径承载) if not main.debug_overlay.visible: - problems.append("调试浮窗未打开") + problems.append("DS_DEBUG=1 启动浮窗未即开(P3 甲失守)") if not main._start_overlay.visible: problems.append("开始覆盖层被隐藏——浮窗应悬浮于菜单之上(14.5 形态失守)") - if main.debug_overlay.get_index() < main._start_overlay.get_index(): - problems.append("调试浮窗树序在开始层之前——遮挡关系失守") + 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(): @@ -41,7 +59,8 @@ func _ready() -> void: # ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线) main._show_debug_overlay() main.debug_overlay.press_reset_for_probe() - # ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位) + # ⑤ 批量模拟全链路(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() @@ -72,9 +91,15 @@ func _ready() -> void: 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 防护——DS_DEBUG 会话默认开态下开局不带窗,对局中经角标再开) + if main.debug_overlay.visible: + problems.append("开冒险路径未显式关浮窗(T2 防护失守)") + if int(main.adventure.master_seed) != 42: + problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.adventure.master_seed)) # 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 if main.level_screen.visible: main.level_screen.press_candidate(0) @@ -84,11 +109,128 @@ func _ready() -> void: 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.board_zone.debug_badge + if dbg_badge == null or not dbg_badge.visible: + problems.append("战斗屏调试角标未点亮(P2 甲 DS_DEBUG 门控失守)") + 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.pause_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.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.adventure.level_index): + problems.append("当局数据关卡失对(%s ≠ %s)" % [str(rd.get("level")), str(main.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.adventure.keystone): + problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.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 ①失守)") + var fresh_sm = load("res://core/save_manager.gd").new("user://smoke_tune_probe_seed.json") + fresh_sm.load_from_disk() # 全新空档——first_adventure_done=false(首局态;新档缺省面) + var keep_sm = main.save_manager + main.save_manager = fresh_sm + main.debug_overlay.seed_edit.text = "" + if main._seed_value() != 42: + problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") + main.save_manager = keep_sm # 恢复隔离档(非首局态) + 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) + 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 != 10: + problems.append("[readonly] 信号连接数=%d 应=10(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)