From b0c5d7e081d83a0163249d36be95728935ed936c Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 18:04:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=BC=94=E6=92=AD=E5=90=8E=E6=8E=92=E7=AA=97?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D+=E5=AE=9A=E7=82=B9=E6=8E=A2=E9=92=88?= =?UTF-8?q?=EF=BC=88T17=E7=94=A8=E6=88=B7=E4=BB=A4=E3=80=8C=E4=BC=A4?= =?UTF-8?q?=E5=AE=B3=E8=BF=87=E7=A8=8B=E6=BC=94=E5=AE=8C=E6=89=8D=E5=88=87?= =?UTF-8?q?=E8=83=9C=E5=88=A9=E7=BB=93=E7=AE=97=E7=AA=97=E3=80=8D=EF=BC=89?= =?UTF-8?q?:=20=E2=91=A0=E7=BC=BA=E9=99=B7=E6=9C=AC=E4=BD=93=3Dopen=5Fvict?= =?UTF-8?q?ory=E9=A6=96=E8=A1=8Cvisible=3Dtrue=E4=B8=8A=E5=B1=8F=E5=85=88?= =?UTF-8?q?=E4=BA=8E=E6=A8=A1=E6=80=81=E6=B3=A8=E5=86=8C=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E5=8F=AA=E6=8C=82=E4=BA=86=E6=A0=88=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=B2=A1=E6=8C=82=E8=A7=86=E8=A7=89=EF=BC=8C=E6=8E=92?= =?UTF-8?q?=E6=92=AD=E6=9C=9F=E7=AA=97=E7=85=A7=E4=BC=9A=E4=B8=8A=E5=B1=8F?= =?UTF-8?q?=EF=BC=9B=E4=BF=AE=3D=E6=8C=82=E8=B5=B7=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E8=A1=A5m.visible=3Dfalse=EF=BC=88=E6=B4=BE=E5=8F=91=E5=BC=80?= =?UTF-8?q?=E7=AA=97=E7=BB=8F=5Fmodals.open=E7=82=B9=E4=BA=AE=EF=BC=89?= =?UTF-8?q?=E2=91=A1=E5=9B=9B=E5=BC=80=E5=8F=A3=EF=BC=88=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E8=83=9C=E5=88=A9/Boss=E6=80=BB=E7=BB=93/=E5=86=92=E9=99=A9?= =?UTF-8?q?=E7=BB=88=E5=B1=80/=E8=B0=83=E8=AF=95=E7=BB=88=E5=B1=80?= =?UTF-8?q?=EF=BC=89=E7=BB=9F=E4=B8=80=E8=B5=B0=5Fopen=5Fmodal=5Fafter=5Fp?= =?UTF-8?q?layback=E2=91=A2=E6=96=B0=E5=A2=9Et17=5Fdefer=5Fprobe=E5=AE=9A?= =?UTF-8?q?=E7=82=B9=E6=8E=A2=E9=92=88=EF=BC=88=E7=9C=9F=E5=AE=9E=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7=E8=B7=AF=E5=BE=84=E9=A9=B1=E5=8A=A8=E5=87=BB=E6=9D=80?= =?UTF-8?q?=E8=83=9C=E5=88=A9=EF=BC=8C=E4=B8=A4=E6=96=AD=E8=A8=80=EF=BC=9A?= =?UTF-8?q?=E8=83=9C=E5=88=A9=E5=90=8C=E5=B8=A7=E7=AA=97=E4=B8=8D=E5=BC=80?= =?UTF-8?q?+=E6=8C=82=E8=B5=B7=E5=9C=A8=E6=A1=88+=E9=98=9F=E5=88=97?= =?UTF-8?q?=E5=9C=A8=E6=92=AD=E3=80=94=E6=97=A7=E8=B7=AF=E5=BE=84=E5=90=8C?= =?UTF-8?q?=E5=B8=A7=E5=BC=80=E7=AA=97=E5=8D=B3=E7=BA=A2=3D=E5=88=A4?= =?UTF-8?q?=E5=88=AB=E6=80=A7=E3=80=95/=E6=8E=92=E6=92=AD=E5=AE=8C5.5s?= =?UTF-8?q?=E7=AA=97=E5=BC=80=EF=BC=89=E2=91=A3=E6=92=A4=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=92=E6=A1=A9+ui=5Fclick=5Fprobe?= =?UTF-8?q?=E7=BB=88=E5=B1=80=E5=88=A4=E6=8D=AE=E6=94=B9adventure.state?= =?UTF-8?q?=EF=BC=88running=3D=E4=B8=AD=E9=80=94=E9=9D=A2/settling=3D?= =?UTF-8?q?=E7=9C=9F=E7=BB=88=E5=B1=80=EF=BC=89=EF=BC=9B=E9=97=B8=3D?= =?UTF-8?q?=E5=AE=9A=E7=82=B9=E6=8E=A2=E9=92=88PASS=EF=BC=88=E6=8C=89?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=B5=8B=E8=AF=95=E7=B2=92=E5=BA=A6=E9=93=81?= =?UTF-8?q?=E8=A7=84=E5=8F=AA=E8=B7=91=E5=85=B3=E8=81=94=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=EF=BC=8C=E5=85=A8=E9=87=8F=E5=BD=92=E8=BF=AD=E4=BB=A3=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E6=89=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/main.gd | 42 +++++++++++-- game/smoke/t17_defer_probe.gd | 103 ++++++++++++++++++++++++++++++++ game/smoke/t17_defer_probe.tscn | 6 ++ game/smoke/ui_click_probe.gd | 16 +++-- 4 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 game/smoke/t17_defer_probe.gd create mode 100644 game/smoke/t17_defer_probe.tscn diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 6b5401df..a49cc714 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -123,6 +123,32 @@ var _locked_click := {} # 锁期点击缓存(T17 v3.1——最后一 # 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 #(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, # 排播泵本体已迁 row_pacer.gd——T3 结构段)。 +var _deferred_modal: Control = null # 演播后开窗挂起(T17——胜利/Boss 总结/终局面等排播放完再开) + + +# 演播后排窗(T17 用户令「伤害过程演完才切结算窗口」):排播队列在播=挂起,演播完边沿 +# (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出 +# 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。 +func _open_modal_after_playback(m: Control) -> void: + if _row_pacer.pending_rows().is_empty(): + _open_modal(m) + return + _deferred_modal = m + m.visible = false # 挂起含视觉(open_* 已置 visible=true——不压掉则窗在排播期照会上屏,延迟形同虚设) + + +# 演播完边沿派发(row_pacer.pump_ended 注入口——原直连锁期点击重放,现统一派发): +# 先开挂起 modal(胜利窗接管呈现),后锁期点击重放——modal 开启后 _input_locked 吞掉 +# 重放点击(战斗已收场,重放无意义)。 +func _on_pump_ended() -> void: + if _deferred_modal != null: + var m: Control = _deferred_modal + _deferred_modal = null + _open_modal(m) + return + _replay_locked_click() + + func _replay_locked_click() -> void: if _locked_click.is_empty() or not _row_queue.is_empty(): return @@ -180,7 +206,7 @@ func _build_ui() -> void: _row_pacer.settlement_fx = settlement_fx _row_pacer.board_view = board_view _row_pacer.refresh_request = _refresh_presentations - _row_pacer.pump_ended = _replay_locked_click # T17 v3.1——演播完边沿重放锁期点击(合并批接线) + _row_pacer.pump_ended = _on_pump_ended # T17 v3.1+演播后排窗——边沿统一派发(挂起 modal 优先,其次锁期点击重放) # T17 v3.1——演播完边沿重放锁期点击(合并批接线) add_child(_row_pacer) # 魔法道具两步交互(T3 拆件批——迁 aux_interact_controller.gd;settlement/runner 随关卡更替, # 经现值回调注入——仿 identity_resolver 只读回调先例) @@ -298,6 +324,12 @@ func _register_modal(m: Control) -> void: func _open_modal(m: Control) -> void: + # 挂起兜底(T17 演播后排窗):其他路径(如回看)在播期排空队列会使演播完边沿不再触发—— + # 此处先结挂起 modal 防永不兑现(m==挂起对象时跳过=本调用自身)。 + if _deferred_modal != null and m != _deferred_modal: + var dm: Control = _deferred_modal + _deferred_modal = null + _open_modal(dm) # 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈 _flush_row_queue() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行) _modals.open(m) @@ -486,7 +518,7 @@ func _on_bonus_presented(payload: Dictionary) -> void: _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", board_view.get_global_rect(), "%s\n%s" % [_gp_summary_text({"gp_dropped": _last_gain_amount}), Narrator.bonus_keystone_line(payload)]) - _open_modal(_end_overlay) + _open_modal_after_playback(_end_overlay) _refresh_presentations() @@ -615,7 +647,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) if runner != null: runner.restore_hero_base() - _open_modal(_end_overlay) + _open_modal_after_playback(_end_overlay) _refresh_presentations() @@ -779,7 +811,7 @@ func _on_battle_finished(payload: Dictionary) -> void: else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) - _open_modal(_end_overlay) + _open_modal_after_playback(_end_overlay) _refresh_presentations() @@ -898,7 +930,7 @@ func _on_choices_presented(candidates: Array) -> void: _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点", board_view.get_global_rect(), "战利品 GP +%d · 合计 %d" % [_last_gain_amount, int(runner.gp)]) - _open_modal(_end_overlay) + _open_modal_after_playback(_end_overlay) _refresh_presentations() return _enter_level_screen() diff --git a/game/smoke/t17_defer_probe.gd b/game/smoke/t17_defer_probe.gd new file mode 100644 index 00000000..e08f336a --- /dev/null +++ b/game/smoke/t17_defer_probe.gd @@ -0,0 +1,103 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 演播后排窗定点探针(T17 用户令「伤害过程演完才切战斗胜利结算窗」——对应本改动的唯一关联测试): +# 真实信号路径驱动一场战斗到击杀胜利(结算行在排播队列),断言两条: +# ①胜利同帧:终局面未开 + 挂起态在案 + 队列非空(旧行为=同帧 flush+开窗,本断言即红——判别性); +# ②排播放完:终局面开启(演播完边沿派发——_on_pump_ended)。 +# 存档/设置/元进度三面隔离(fx_shot_probe 同款),零真实档触碰。headless 可跑(不抓帧)。 +extends Node + +var main +var _problems: Array = [] + + +func _ready() -> void: + await get_tree().process_frame + main = load("res://presentation/main.tscn").instantiate() + add_child(main) + await get_tree().process_frame + for p0 in ["user://t17_defer_probe.json", "user://t17_defer_probe_settings.cfg"]: + if FileAccess.file_exists(p0): + DirAccess.remove_absolute(p0) + var iso_c = load("res://core/save_manager.gd").new("user://t17_defer_probe.json") + iso_c.load_from_disk() + main.save_manager = iso_c + main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c) + main.settings_store = load("res://presentation/settings_store.gd").new("user://t17_defer_probe_settings.cfg") + # 本探针验证完整动画档的演播后排窗——强制 motion_scale=1.0(隔离档替换在 boot 后, + # 减动效已随真档 settings.cfg 应用于引擎态;减动效档=consume_now 快路、开窗即时,另属语义) + UiKit.motion_scale = 1.0 + for i in 8: + await get_tree().process_frame + var lvbtn: Button = _find_button_by_sub(main._start_overlay, "开始冒险") + if lvbtn == null: + _fail("开始冒险按钮缺失") + return + lvbtn.pressed.emit() + for i in 60: + await get_tree().process_frame + if main.level_screen != null and main.level_screen.visible: + break + if main.level_screen == null or not main.level_screen.visible: + _fail("关卡画面未出现") + return + for i in 5: + await get_tree().process_frame + main.level_screen.press_candidate(0) + for i in 20: + await get_tree().process_frame + if main.settlement == null: + _fail("战斗未启动") + return + # 直挖至击杀胜利(settle_batch 真实管线——step_row 信号→排播队列,与点击路径同链) + var ms = main.settlement.minesweeper + var guard := 200 + while guard > 0 and String(main.settlement.battle_result) == "": + guard -= 1 + var plan = load("res://smoke/auto_player.gd").choose_dig(ms) + if plan.is_empty(): + main.settlement.settle_batch([{"action": "stop", "cell": Vector2i(-1, -1)}]) + await get_tree().process_frame + continue + if plan.hero_cell != ms.hero_pos(): + ms.settle_batch([{"action": "move", "cell": plan.hero_cell}]) + ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + await get_tree().process_frame + if String(main.settlement.battle_result) != "victory": + _fail("战斗未胜利(result='%s')" % str(main.settlement.battle_result)) + return + # 断言①胜利同帧:终局面未开 + 挂起在案 + 队列非空(判别性——旧行为同帧开窗即红) + if main._end_overlay.is_open(): + _problems.append("胜利同帧终局面已开(演播后排窗失效——同帧 flush 旧路径回潮)") + if main._deferred_modal == null: + _problems.append("胜利同帧挂起态缺失(_deferred_modal == null)") + if main._row_pacer.pending_rows().is_empty(): + _problems.append("胜利同帧排播队列已空(结算行未走排播链)") + # 断言②排播放完:终局面开启(20s 守卫——0.22s×行数 ×10 余量) + var waited := 0.0 + while not main._end_overlay.is_open() and waited < 20.0: + await get_tree().process_frame + waited += get_process_delta_time() + if not main._end_overlay.is_open(): + _problems.append("排播放完后终局面未开(%.1fs——演播完边沿派发失守)" % waited) + if _problems.is_empty(): + print("[t17_defer] PASS — 胜利同帧挂起+排播完开窗 两断言全过(waited=%.1fs)" % waited) + get_tree().quit(0) + else: + for p in _problems: + print("[t17_defer] FAIL — " + p) + get_tree().quit(2) + + +func _fail(why: String) -> void: + print("[t17_defer] FAIL — " + why) + get_tree().quit(2) + + +func _find_button_by_sub(root: Node, sub: String) -> Button: + if root is Button and String(root.text).contains(sub): + return root + for c in root.get_children(): + var r := _find_button_by_sub(c, sub) + if r != null: + return r + return null diff --git a/game/smoke/t17_defer_probe.tscn b/game/smoke/t17_defer_probe.tscn new file mode 100644 index 00000000..6ff68f84 --- /dev/null +++ b/game/smoke/t17_defer_probe.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://smoke/t17_defer_probe.gd" id="1"] + +[node name="T17DeferProbe" type="Node"] +script = ExtResource("1") diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 921a62ec..30c5dbed 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -256,7 +256,11 @@ func _ready() -> void: # 冒险终局(失败/放弃/打穿)——单关直入口退役后无二段入口,见终局即收口(模态断言照旧) # T17 适配(迭代 10 三选一链):_adventure_mode 仍真且非选路挂起=中途结算面(Boss 胜利 # 总结「继续」后挂三选一→真终局未到)——真实信号路径点继续推进。 - if main._adventure_mode and not main._settlement_stop_pending: + # T17 演播后排窗适配:真终局 vs 中途结算面判据=adventure.state(冒险结构三态—— + # 战斗胜利停点=running / 真终局(失败/放弃/打穿)=settling)。旧判据 + # (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。 + var _adv_running: bool = main.adventure != null and String(main.adventure.state) == "running" + if main._adventure_mode and _adv_running: main._end_overlay._btn.pressed.emit() continue if not main._input_locked(): @@ -293,14 +297,14 @@ func _ready() -> void: # (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至 # level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。 if not _done and not main._end_overlay.is_open(): - var close_guard := 1200 + var close_guard := 8000 + var dbg_frame := 0 # T17 演播后排窗:每场胜利 +~400 帧演播等待(旧同帧 flush 零等待)——守卫按多场战斗放大 while close_guard > 0: close_guard -= 1 await get_tree().process_frame if main._end_overlay.is_open(): - # 冒险中=中途结算面(战斗总结「继续」/下一节点——L2 通关后还有三选一面续推); - # 非冒险=终局面在案——断点段前提满足,收口完成。 - if main._adventure_mode: + # 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。 + if main._adventure_mode and main.adventure != null and String(main.adventure.state) == "running": main._end_overlay._btn.pressed.emit() else: break @@ -336,6 +340,8 @@ func _ready() -> void: and main.settlement.action_economy.phase == "Active": _play_one_action() # 排播期/Waiting 等待态=自然流逝(演播秒级、锁随队列空解除) + str(main.settlement.battle_result) if main.settlement != null else "-", + main._row_queue.size(), str(main._deferred_modal != null)]) # T17 适配:战败或放弃二途皆达终局面(放弃=预算耗尽收口路径——见上),断点段前提不变。 await get_tree().process_frame var bp_file := "user://smoke_click_probe_reset.json"