From 399ed3ad5377fe97732d30e6cf8d563bcfc1323c Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 1 Nov 2026 18:59:16 +0800 Subject: [PATCH] =?UTF-8?q?iter47-=E5=8D=A13=EF=BC=9A=E5=8D=A1=E2=91=A4(?= =?UTF-8?q?=E8=BF=AD=E4=BB=A347=C2=B7Esc=E6=B5=AE=E7=AA=97=E4=B8=8E?= =?UTF-8?q?=E8=90=A5=E5=9C=B0=E9=A1=B6=E6=A0=8F=E6=89=B9):=20Esc=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E5=9B=9B=E6=80=81=E8=B7=AF=E7=94=B1+CampHudBand?= =?UTF-8?q?=E9=A1=B6=E6=A0=8F=E6=A0=8F+=E8=AE=BE=E7=BD=AE=E8=AF=AD?= =?UTF-8?q?=E5=A2=83=E5=88=86=E6=94=AF+=E6=96=AD=E8=A8=80=E6=89=B9?= =?UTF-8?q?=E2=80=94=E2=80=94=E7=BB=88=E6=80=81=E5=85=AD=E6=9E=9A=E8=90=BD?= =?UTF-8?q?=E6=BB=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/camp_shop_presenter.gd | 1 + game/presentation/main.gd | 19 +++++ game/presentation/main_esc_flow.gd | 50 +++++++++++++ game/presentation/main_esc_flow.gd.uid | 1 + game/presentation/main_overlay_flow.gd | 6 +- game/presentation/main_ui_build.gd | 16 ++++- game/presentation/ui_camp_modals.gd | 1 + game/presentation/ui_registry.gd | 2 + game/presentation/views/camp_esc_overlay.gd | 32 +++++++++ .../views/camp_esc_overlay.gd.uid | 1 + game/presentation/views/camp_hud_band.gd | 27 +++++++ game/presentation/views/camp_hud_band.gd.uid | 1 + game/presentation/views/camp_scene_view.gd | 2 + game/probe/screen_flow_probe.gd | 70 +++++++++++++++++++ game/probe/ui_struct_probe.gd | 42 +++++++++++ 15 files changed, 269 insertions(+), 2 deletions(-) create mode 100644 game/presentation/main_esc_flow.gd create mode 100644 game/presentation/main_esc_flow.gd.uid create mode 100644 game/presentation/views/camp_esc_overlay.gd create mode 100644 game/presentation/views/camp_esc_overlay.gd.uid create mode 100644 game/presentation/views/camp_hud_band.gd create mode 100644 game/presentation/views/camp_hud_band.gd.uid diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index 07094d3b..f57a8a31 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -42,6 +42,7 @@ func open_camp() -> void: meta.refresh_treasure_unlocks() # 条件解锁刷新 start_overlay.visible = false _open_modal.call(views.camp_scene) # 模态栈单口(场景根开——选档层被栈遮挡挂起) + views.refresh_bus.refresh(camp_data()) # 进档动线尾显式刷新一闸(卡⑤ 契约 5 时机②——HUD 读数重读) _refresh_presentations.call() diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 8d10c7ae..8e277baf 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -528,6 +528,25 @@ func _on_camp_object_pressed(key: StringName) -> void: MainOverlayFlow._on_camp_object_pressed(self, key) # 场景点选路由(卡④ T3——薄委托,实现归覆盖层流程承接件) +# Esc 输入入口(产品码首个键盘入口——卡⑤ T1 契约 9 接线定稿:事件驱动 _unhandled_input、 +# KEY_ESCAPE 键值判定配 physical_keycode、pressed 且非 echo、禁 Input 单例轮询、不挂 ui_cancel) +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ESCAPE or event.physical_keycode == KEY_ESCAPE): + MainEscFlow.on_esc_input(self) + + +func _on_esc_return() -> void: + MainEscFlow._on_return_to_menu(self) + + +func _on_esc_settings() -> void: + MainEscFlow._on_settings(self) + + +func _on_esc_quit() -> void: + MainEscFlow._on_quit(self) + + func _camp_data() -> Dictionary: return MainOverlayFlow._camp_data(self) diff --git a/game/presentation/main_esc_flow.gd b/game/presentation/main_esc_flow.gd new file mode 100644 index 00000000..85ef1cb0 --- /dev/null +++ b/game/presentation/main_esc_flow.gd @@ -0,0 +1,50 @@ +## 组合根 Esc 流程承接件(产品码首个键盘输入入口——main._unhandled_input 同名薄委托落地): +## Esc 四态路由(契约 9 判据单点=ModalStack 登记册现开集合,禁手枚名单)+三出口动线。 +## 静态承接件零成员态(仿 main_overlay_flow 形态——main 侧留同名薄委托,探针 main.X 路径零改动)。 +class_name MainEscFlow +extends RefCounted + + +## 营地语境判据(Q1 甲——场景根 visible;子界面开态抑制由契约 9① 现开集合承载,不并入本谓词) +static func is_camp_context(root: Variant) -> bool: + return root.ui.modals.camp.scene != null and root.ui.modals.camp.scene.visible + + +## Esc 四态优先序(契约 9——①先于②:设置开在浮窗之上态必须先命中「无反馈」,浮窗不隔层关闭): +## ①除浮窗外任一登记模态开→无反馈(子界面开态/设置开在浮窗之上)→②浮窗开→close(同键单义切换, +## Q2 甲)→③营地语境→open→④非营地→无反馈(选档/冒险/菜单语境 Esc 零响应——契约 2) +static func on_esc_input(root: Variant) -> void: + var esc: Control = root.ui.modals.camp.esc_overlay + if _other_modal_open(root, esc): + return # ① 除浮窗外任一登记模态开 → 无反馈 + if esc.visible: + root.ui.modals.flow.close(esc) # ② 再按 Esc 关窗 + return + if is_camp_context(root): + root.ui.modals.flow.open(esc) # ③ 营地语境唤起 + # ④ 非营地语境 → 无反馈 + + +# 登记册现开集合查询(除浮窗与营地场景根本身——场景根=语境层非抑制对象; +# 迭代登记册遍历=契约 9「判据单点」 sanctioned 读法,禁按层名手枚名单) +static func _other_modal_open(root: Variant, esc: Control) -> bool: + for m in root.ui.modals.flow.stack._list: + if m != esc and m != root.ui.modals.camp.scene and is_instance_valid(m) and m.visible: + return true + return false + + +## 返回主菜单动线(契约 4):首步 close 浮窗(缺此步浮窗残留主菜单上方)→营地与选档层同收+开始层恢复 +static func _on_return_to_menu(root: Variant) -> void: + root.ui.modals.flow.close(root.ui.modals.camp.esc_overlay) + root._camp_pres.on_camp_closed() # 关层收闭单口(卡④ T3——营地层与选档层同收+开始层恢复) + + +## 设置出口(浮窗登记在设置层之下——open 只隐上方层,浮窗由树序遮挡非属性隐藏;关后重现=栈语义) +static func _on_settings(root: Variant) -> void: + root.ui.modals.flow.open(root.ui.modals.settings_overlay) + + +## 退出出口(_quit_game 原样复用——settings_store.save_settings+quit,零语义副作用) +static func _on_quit(root: Variant) -> void: + root._quit_game() diff --git a/game/presentation/main_esc_flow.gd.uid b/game/presentation/main_esc_flow.gd.uid new file mode 100644 index 00000000..261aac57 --- /dev/null +++ b/game/presentation/main_esc_flow.gd.uid @@ -0,0 +1 @@ +uid://caa868un7dnt4 diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd index bf8d5a4d..7df4e973 100644 --- a/game/presentation/main_overlay_flow.gd +++ b/game/presentation/main_overlay_flow.gd @@ -10,7 +10,11 @@ static func _open_settings(root: Variant) -> void: static func _on_settings_closed(root: Variant) -> void: root.ui.modals.flow.close(root.ui.modals.settings_overlay) # 模态栈单口 - root.ui.screens.start_overlay.visible = true + # 语境分支(卡⑤ 契约 3——谓词=camp_esc_overlay.visible,谁开的设置谁收): + # 营地浮窗路径关设置回浮窗不回主菜单(栈语义:浮窗登记在下、由树序遮挡非属性隐藏); + # 菜单路径保留开始层恢复硬副作用。 + if not root.ui.modals.camp.esc_overlay.visible: + root.ui.screens.start_overlay.visible = true # ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd index 15172e8d..09164e42 100644 --- a/game/presentation/main_ui_build.gd +++ b/game/presentation/main_ui_build.gd @@ -110,6 +110,15 @@ static func _build_overlays(root: Variant) -> void: var codex := CodexView.new() root.add_child(codex) root.ui.modals.camp.codex = codex + # Esc 浮窗(卡⑤——登记于设置层之前的模态;三钮意图经 MainEscFlow 路由) + var esc_overlay := CampEscOverlay.new() + esc_overlay.setup_buttons() + esc_overlay.bind_signals() + root.add_child(esc_overlay) + root.ui.modals.camp.esc_overlay = esc_overlay + # 营地顶栏 HUD 栏(卡⑤——钥石余额常驻;挂卡④场景根顶栏预留位+入刷新单口目标册) + var hud_band := CampHudBand.new() + root.ui.modals.camp.scene.set_hud_band(hud_band) # 屏壳/子界面插位由 LAYER_CONTRACT 槽统一落位——禁散落 move_child root.ui.modals.legend_overlay.closed.connect(root.ui.modals.flow.close.bind(root.ui.modals.legend_overlay)) root.ui.modals.node_overlay.continue_pressed.connect(root._on_node_continue) @@ -178,7 +187,8 @@ static func _build_menu_wiring(root: Variant) -> void: # 营地刷新单口(各写口尾→现开界面+场景根 HUD 一闸完成——卡④ T3;卡⑤ HUD band 随卡⑤挂册) var camp_bus := CampRefreshBus.new() camp_bus.bind_targets([root.ui.modals.camp.forge, root.ui.modals.camp.treasure, - root.ui.modals.camp.tree, root.ui.modals.camp.codex] as Array[Control]) + root.ui.modals.camp.tree, root.ui.modals.camp.codex, + root.ui.screens.camp_screen.camp_scene.hud_band] as Array[Control]) root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, {"camp_scene": root.ui.modals.camp.scene, "forge_view": root.ui.modals.camp.forge, "treasure_view": root.ui.modals.camp.treasure, "tree_view": root.ui.modals.camp.tree, @@ -226,6 +236,10 @@ static func _build_debug_and_resolvers(root: Variant) -> void: root.ui.modals.confirm_dialog.confirmed.connect(root._on_confirm_accepted) root.ui.modals.confirm_dialog.secondary_confirmed.connect(root._on_confirm_secondary) # 三钮次确认(卡④ 大门「开始新冒险」出口) root.ui.modals.confirm_dialog.cancelled.connect(root._on_confirm_declined) + # Esc 浮窗三出口接线(卡⑤——意图信号经薄委托归 MainEscFlow 动线;输入入口=main._unhandled_input) + root.ui.modals.camp.esc_overlay.return_requested.connect(root._on_esc_return) + root.ui.modals.camp.esc_overlay.settings_requested.connect(root._on_esc_settings) + root.ui.modals.camp.esc_overlay.quit_requested.connect(root._on_esc_quit) # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; # 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块; diff --git a/game/presentation/ui_camp_modals.gd b/game/presentation/ui_camp_modals.gd index 12297b74..69cbd2a3 100644 --- a/game/presentation/ui_camp_modals.gd +++ b/game/presentation/ui_camp_modals.gd @@ -9,3 +9,4 @@ var forge: ForgeView # 锻造工坊子界面 var treasure: TreasureView # 秘宝匣子界面 var tree: TreeView # 天赋树子界面 var codex: CodexView # 奖励格图鉴子界面(占位挂起态) +var esc_overlay: CampEscOverlay # Esc 浮窗(卡⑤——终态六枚落满) diff --git a/game/presentation/ui_registry.gd b/game/presentation/ui_registry.gd index 67358424..ad4db46b 100644 --- a/game/presentation/ui_registry.gd +++ b/game/presentation/ui_registry.gd @@ -27,6 +27,7 @@ const LAYER_CONTRACT: Array[Dictionary] = [ {"key": "treasure_view", "kind": "modal", "tags": []}, {"key": "tree_view", "kind": "modal", "tags": []}, {"key": "codex_view", "kind": "modal", "tags": []}, + {"key": "camp_esc_overlay", "kind": "modal", "tags": []}, # Esc 浮窗(卡⑤——登记于营地层与设置层之间,契约 3 栈语义正解) {"key": "settings_overlay", "kind": "modal", "tags": []}, {"key": "status_panel", "kind": "modal", "tags": []}, {"key": "career_overlay", "kind": "modal", "tags": []}, @@ -67,6 +68,7 @@ func node(key: String) -> Node: "treasure_view": return modals.camp.treasure "tree_view": return modals.camp.tree "codex_view": return modals.camp.codex + "camp_esc_overlay": return modals.camp.esc_overlay "settings_overlay": return modals.settings_overlay "status_panel": return modals.status_panel "career_overlay": return modals.career_overlay diff --git a/game/presentation/views/camp_esc_overlay.gd b/game/presentation/views/camp_esc_overlay.gd new file mode 100644 index 00000000..44e4ac2e --- /dev/null +++ b/game/presentation/views/camp_esc_overlay.gd @@ -0,0 +1,32 @@ +## Esc 浮窗视图(营地语境 Esc 唤起):返回主菜单/设置/退出游戏三钮(裁决 9——三项且仅三项)。 +## 壳=UiKit.modal_shell(居中模态壳,ui_kit 工厂单点);退出钮=UiKit.danger_button;零新素材零新色值。 +## Q2 再按 Esc 关窗=组合根 Esc 路由直呼 flow.close(本视图无自关通路);三钮 pressed 外发意图不内含动线。 +## 零动效(契约 6——直开直关,子树零 AnimationPlayer、源码零动效令牌)。 +class_name CampEscOverlay +extends Control + +signal return_requested ## 返回主菜单意图(动线归 MainEscFlow._on_return_to_menu——首步 close 浮窗) +signal settings_requested ## 设置意图(开设置层——浮窗登记在其下,栈语义关后重现) +signal quit_requested ## 退出游戏意图(_quit_game 原样复用) + +var _return_btn: Button +var _settings_btn: Button +var _quit_btn: Button + + +func setup_buttons() -> void: + var box: VBoxContainer = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(360, 0), 12, + UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box + _return_btn = UiKit.button("返回主菜单") + box.add_child(_return_btn) + _settings_btn = UiKit.button("设置") + box.add_child(_settings_btn) + _quit_btn = UiKit.danger_button("退出游戏") + box.add_child(_quit_btn) + visible = false # 登记模态初始隐(开合走栈——模态纪律) + + +func bind_signals() -> void: + _return_btn.pressed.connect(func() -> void: return_requested.emit()) + _settings_btn.pressed.connect(func() -> void: settings_requested.emit()) + _quit_btn.pressed.connect(func() -> void: quit_requested.emit()) diff --git a/game/presentation/views/camp_esc_overlay.gd.uid b/game/presentation/views/camp_esc_overlay.gd.uid new file mode 100644 index 00000000..17d1a1a1 --- /dev/null +++ b/game/presentation/views/camp_esc_overlay.gd.uid @@ -0,0 +1 @@ +uid://col25elh1utw5 diff --git a/game/presentation/views/camp_hud_band.gd b/game/presentation/views/camp_hud_band.gd new file mode 100644 index 00000000..a228626a --- /dev/null +++ b/game/presentation/views/camp_hud_band.gd @@ -0,0 +1,27 @@ +## 营地顶栏小 HUD 栏(裁决 10——钥石余额常驻,最小集形态):承载 UiKit.keystone_badge(工厂单点)。 +## 读数只经 camp_data() 投影单口(禁第二账本、禁视图自算);刷新挂卡④ CampRefreshBus 单口 +## (bus 目标册成员——显式触发才变,禁 _ready 自刷/自持第二入口,契约 5);载体=PRESET_TOP_WIDE 48px 浮层。 +class_name CampHudBand +extends Control + +var _badge: PanelContainer + + +func _init() -> void: + set_anchors_preset(Control.PRESET_TOP_WIDE) + custom_minimum_size = Vector2(0, 48) # 高 48px 浮层覆盖、不挤压画面(camp.md §顶栏) + mouse_filter = Control.MOUSE_FILTER_IGNORE # HUD 栏不挡场景命中区(浮层覆盖语义) + _badge = UiKit.keystone_badge("钥石 0") + _badge.set_anchors_preset(Control.PRESET_CENTER_RIGHT) + _badge.position = Vector2(-160, 8) # 顶栏右位次(UiKit 现行件域内——代拍板 3) + add_child(_badge) + + +## 余额写入(格式串=既有消费面同款「钥石 %d」——badge 工厂约定单点,禁第二格式串) +func set_balance(v: int) -> void: + _badge.value_label.text = "钥石 %d" % v + + +## 刷新单口落点(CampRefreshBus 目标册成员——显式触发才变,契约 5 刷新时机①②③同一闸) +func refresh_camp(camp_data: Dictionary) -> void: + set_balance(int(camp_data.get("balance", 0))) diff --git a/game/presentation/views/camp_hud_band.gd.uid b/game/presentation/views/camp_hud_band.gd.uid new file mode 100644 index 00000000..6153c82b --- /dev/null +++ b/game/presentation/views/camp_hud_band.gd.uid @@ -0,0 +1 @@ +uid://by6ugjfdicgud diff --git a/game/presentation/views/camp_scene_view.gd b/game/presentation/views/camp_scene_view.gd index eb0f563b..921a7a1c 100644 --- a/game/presentation/views/camp_scene_view.gd +++ b/game/presentation/views/camp_scene_view.gd @@ -46,6 +46,7 @@ var _press_key := &"" # 当前按住命中键 var _float_tween: Tween # 悬停浮动补间(逐级整像素吸附,禁 _process 逐帧移 UI) var _desat_mat: ShaderMaterial # 禁用态共享材质(strength 恒 1,材质挂/摘承载开关) var _hud_slot: Control # 顶栏预留位(卡⑤ set_hud_band 填值) +var hud_band: Control # 顶栏 HUD 栏句柄(卡⑤ CampHudBand——装配后 bus 目标册取用面) func _init() -> void: @@ -94,6 +95,7 @@ func set_feedback_state(key: StringName, state: FeedbackState) -> void: func set_hud_band(band: Control) -> void: if _hud_slot != null: _hud_slot.add_child(band) + hud_band = band func _gui_input(event: InputEvent) -> void: diff --git a/game/probe/screen_flow_probe.gd b/game/probe/screen_flow_probe.gd index 1f939d5e..df28d919 100644 --- a/game/probe/screen_flow_probe.gd +++ b/game/probe/screen_flow_probe.gd @@ -27,6 +27,7 @@ func _ready() -> void: await _step_boot_menu() await _step_menu_to_camp() await _step_camp_back_to_menu() + await _step_camp_esc_and_hud() # 卡⑤——Esc 四态路由/三出口/设置语境分支/HUD 读数 await _step_save_select_open_back() # 迭代 33 卡③ sl10——选档开合+返回路径+ESC 语境不响应 await _step_menu_to_adventure() await _step_level_to_battle_and_back() @@ -229,3 +230,72 @@ func _step_end_back_to_menu() -> void: _expect_exclusive("⑦终局→菜单", "菜单") if main.run_scope != null and main.run_scope.is_adventure: _problems.append("⑦终局→菜单:冒险态未收束(run_scope 仍标 is_adventure)") + + +# ---- ③' 营地 Esc 浮窗+HUD(卡⑤——真实键盘合成路径 push_input;四态路由+三出口+语境分支+余额单口)---- +func _step_camp_esc_and_hud() -> void: + ProbeUiUtil.isolate_save(main, "smoke_screen_flow_esc", false, false, true) + if not ProbeUiUtil.open_camp_via_savesel(main, "smoke_screen_flow_esc"): + _problems.append("[esc] 营地入口缺失(选档层新档钮驱动中断)〔ESC批 V1〕") + return + await _frames() + _push_esc() + await _frames() + if not main.ui.modals.camp.esc_overlay.visible: + _problems.append("[esc] 营地根态 Esc 未唤起浮窗(契约 9③)〔ESC批 V1〕") + _push_esc() + await _frames() + if main.ui.modals.camp.esc_overlay.visible: + _problems.append("[esc] 再按 Esc 未关浮窗(Q2 同键单义切换失守)〔ESC批 V1〕") + _push_esc(true) # echo 连发(真实键盘长按形态) + await _frames() + if main.ui.modals.camp.esc_overlay.visible: + _problems.append("[esc] echo 事件唤起浮窗(触发沿 pressed 且非 echo 失守)〔ESC批 V1〕") + main.ui.modals.camp.scene.object_pressed.emit(&"dummy") # 子界面开态 + await _frames() + _push_esc() + await _frames() + if main.ui.modals.camp.esc_overlay.visible or not main.ui.modals.camp.tree.visible: + _problems.append("[esc] 子界面开态 Esc 反馈(契约 9① 无反馈失守)〔ESC批 V1〕") + main.ui.modals.camp.tree.closed.emit() + await _frames() + # HUD 余额(契约 5 时机③——夹具直写后显式触发才变;禁 band 自刷假设) + main.meta.meta_state._add_keystone(9999) + main._camp_pres.views.refresh_bus.refresh(main._camp_data()) # 显式触发刷新单口 + var band: Control = main.ui.modals.camp.scene.get_node("HudBandSlot").get_child(0) + if not String(band._badge.value_label.text).begins_with("钥石 9999"): + _problems.append("[esc] HUD 余额读数≠存档单口现值(直写+显式触发未变)〔ESC批 V6〕") + # 设置开在浮窗之上→Esc 无反馈且浮窗不误关(契约 9①);关设置回浮窗不回主菜单(契约 3 语境分支) + _push_esc() + await _frames() + main._on_esc_settings() + await _frames() + if not main.ui.modals.settings_overlay.visible: + _problems.append("[esc] 浮窗设置出口断裂〔ESC批 V4〕") + _push_esc() + await _frames() + if not main.ui.modals.camp.esc_overlay.visible: + _problems.append("[esc] 设置开态 Esc 误关浮窗(契约 9①——浮窗不隔层关闭)〔ESC批 V1〕") + main._on_settings_closed() + await _frames() + if not main.ui.modals.camp.esc_overlay.visible or main.ui.screens.start_overlay.visible: + _problems.append("[esc] 关设置后未回浮窗或回弹主菜单(契约 3 语境分支失守)〔ESC批 V4〕") + # 返回主菜单动线(契约 4——浮窗自收+营地隐+选档收+菜单显) + main.ui.modals.camp.esc_overlay.return_requested.emit() + await _frames() + if main.ui.modals.camp.esc_overlay.visible or main.ui.modals.camp.scene.visible or main.ui.modals.save_select_overlay.visible or not main.ui.screens.start_overlay.visible: + _problems.append("[esc] 返回主菜单动线失守(浮窗自收/营地隐/选档收/菜单显)〔ESC批 V3〕") + # 非营地语境(boot 菜单)Esc→无响应(契约 9④ 回归面) + _push_esc() + await _frames() + if main.ui.modals.camp.esc_overlay.visible: + _problems.append("[esc] 非营地语境 Esc 唤起浮窗(契约 9④ 失守)〔ESC批 V1〕") + + +func _push_esc(echo := false) -> void: + var ev := InputEventKey.new() + ev.keycode = KEY_ESCAPE + ev.physical_keycode = KEY_ESCAPE + ev.pressed = true + ev.echo = echo + main.get_viewport().push_input(ev, true) # 真实键盘合成路径(:153 先例——push_input 不更新 Input 单例动作态) diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index bf485e30..6a47b530 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -104,6 +104,38 @@ func _ready() -> void: _settle() if main.ui.modals.career_overlay.visible or main.ui.screens.start_overlay.visible: problems.append("[career] 关闭后页未隐藏或开始层回弹(营地语境分支失守)〔营地图批 V5〕") + # ---- Esc 浮窗结构段(卡⑤——三钮在位且仅三+零动效契约+登记序在册+debug_badge 不波及)---- + main._camp_pres.open_camp() + _settle() + main.ui.modals.camp.scene.object_pressed.emit(&"gate") + _settle() + main.ui.modals.confirm_dialog.cancelled.emit() # 关大门弹窗(清场——浮窗段前提) + _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〕") + 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() @@ -756,3 +788,13 @@ func _start_columns_contract(main: Node) -> void: 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