iter47-卡3:卡⑤(迭代47·Esc浮窗与营地顶栏批): Esc浮窗四态路由+CampHudBand顶栏栏+设置语境分支+断言批+审计补差+master取用+camp.md v2.3定稿吸收(半透明遮罩0.6场景根留底/Esc先关窗Q1甲换代)——终态六枚落满

This commit is contained in:
DKChan committed 2026-11-01 23:44:29 +08:00
1 parent bbeeea77e0
commit 6208a9cd1a
37 files changed
+1117 -107

No files matched your search

+1
View File
@@ -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()
+19
View File
@@ -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)
+56
View File
@@ -0,0 +1,56 @@
## 组合根 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 优先序(camp.md v2.3 定稿 2026-11-01 用户裁——有功能窗开着→先关最上层窗,覆盖卡⑤ 契约 9①
## 的 Q1 甲无反馈;设置开在浮窗之上仍无反馈=契约 3 保留):
## ①功能窗开着→close 最上层窗→①'设置开在浮窗之上→无反馈→②浮窗开→close(Q2 同键单义)→
## ③营地语境→open→④非营地→无反馈
static func on_esc_input(root: Variant) -> void:
var esc: Control = root.ui.modals.camp.esc_overlay
var window := _topmost_camp_window(root)
if window != null:
root.ui.modals.flow.close(window) # ① 先关最上层功能窗(v2.3 定稿)
return
if root.ui.modals.settings_overlay.visible:
return # ①' 设置开在浮窗之上 → 无反馈(浮窗不隔层关闭——契约 3)
if esc.visible:
root.ui.modals.flow.close(esc) # ② 再按 Esc 关窗
return
if is_camp_context(root):
root.ui.modals.flow.open(esc) # ③ 营地语境唤起
# ④ 非营地语境 → 无反馈
# 五功能窗现开查询(camp.md v2.3 五窗互斥单开——模态栈深恒 1,至多一窗在开;生涯页入窗集)
static func _topmost_camp_window(root: Variant) -> Control:
for w: Control in [root.ui.modals.camp.forge, root.ui.modals.camp.treasure, root.ui.modals.camp.tree,
root.ui.modals.camp.codex, root.ui.modals.career_overlay]:
if w != null and w.visible:
return w
return null
## 返回主菜单动线(契约 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()
+1
View File
@@ -0,0 +1 @@
uid://caa868un7dnt4
+5 -1
View File
@@ -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
# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)----
+15 -1
View File
@@ -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〕+两屏角标再开+当局数据只读区块;
+1
View File
@@ -9,3 +9,4 @@ var forge: ForgeView # 锻造工坊子界面
var treasure: TreasureView # 秘宝匣子界面
var tree: TreeView # 天赋树子界面
var codex: CodexView # 奖励格图鉴子界面(占位挂起态)
var esc_overlay: CampEscOverlay # Esc 浮窗(卡⑤——终态六枚落满)
+2
View File
@@ -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
@@ -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())
@@ -0,0 +1 @@
uid://col25elh1utw5
+27
View File
@@ -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)))
@@ -0,0 +1 @@
uid://by6ugjfdicgud
@@ -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:
@@ -12,12 +12,8 @@ var _balance_label: Label # 钥石徽数值行(探针直读面——ui_probe
## 壳构建(子类 _init 调用一次):content 由子类先建好传入(列容器/滚动容器皆可)
func _build_shell(title: String, icon_path: String, content: Control) -> void:
# 实底暖亮档(半透 dim 会透出底下场景图——CampOverlay 同案;遮蔽语义由组合根输入锁兜底)
var dim := UiKit._modal_dim(self, UiKit.BG_BRIGHT)
var bd := UiKit.backdrop_tile()
if bd != null:
dim.add_child(bd)
dim.add_child(UiKit.vignette_rect())
# 半透明压暗遮罩 0.6(camp.md v2.3 定稿——场景根留底可见,窗口之下垫遮罩;交互聚焦由遮罩 STOP 兜底)
var dim := UiKit._modal_dim(self, Color(UiKit.DIM, 0.6))
var margin := MarginContainer.new()
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
margin.add_theme_constant_override("margin_left", 16)
@@ -18,6 +18,7 @@ var _keystone_badge: UiKit.KeystoneBadge # 钥石余额(badge 工厂件—
func _init() -> void:
# 全屏页壳(T6/T7 对齐批先例——暖亮底+石纹弱质感+暗角+56px 通栏顶条+内容列)
var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, true)
page.base.color = Color(UiKit.DIM, 0.6) # 功能窗口化(camp.md v2.3 定稿——场景根留底+半透明压暗遮罩 0.6;观感随样张目检校准)
var top: HBoxContainer = page.top_row
top.add_child(UiKit.label("生 涯",UiKit.FS_TITLE, true, UiKit.WARM))
var top_sp := Control.new()
+74 -1
View File
@@ -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()
@@ -122,7 +123,9 @@ func _step_camp_back_to_menu() -> void:
return
main.ui.modals.camp.scene.object_pressed.emit(&"campfire")
await _frames()
main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(回主菜单——卡④ T4)
if not main.ui.modals.confirm_dialog.visible:
_problems.append("③篝火:二次确认弹窗未现(camp.md v2.2 必须先确认)〔营地图批 V7〕")
main.ui.modals.confirm_dialog.confirmed.emit() # 二次确认离场(confirmed 先于关层——卡④ T4)
await _frames()
_expect_exclusive("③营地→菜单(回退)", "菜单")
@@ -229,3 +232,73 @@ 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〕〔ESC批 B1〕")
_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.tree.visible:
_problems.append("[esc] 子界面开态 Esc 未关窗(camp.md v2.3 定稿——先关最上层窗)〔ESC批 V1〕")
if main.ui.modals.camp.esc_overlay.visible:
_problems.append("[esc] 关窗同拍唤起浮窗(先关窗后浮窗——两拍应分离)〔ESC批 V1〕")
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批 B2〕")
# 设置开在浮窗之上→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〕〔ESC批 B3〕")
# 返回主菜单动线(契约 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〕〔ESC批 B3〕")
# 非营地语境(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 单例动作态)
+5 -5
View File
@@ -88,11 +88,11 @@ func _ready() -> void:
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
await get_tree().process_frame
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
_problems.append("营地树购买未出账(%d→%d 应 −%d)〔句柄子容器批 V1〕〔营地图批 V4〕" % [bal_before, iso_c.meta_state.keystone_balance(), price])
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
iso_chk.load_from_disk()
if int(iso_chk.meta_state.tree_state().nodes.get("sp_hp", 0)) != 1:
_problems.append("营地树购买未持久(reload 后级 != 1)〔句柄子容器批 V1〕")
_problems.append("营地树购买未持久(reload 后级 != 1)〔句柄子容器批 V1〕〔营地图批 V4〕")
# Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
var socket_btn: Button = null
for card in main.ui.modals.camp.tree._tree_buy_btns:
@@ -177,10 +177,10 @@ func _ready() -> void:
_collect_labels_by_text(main.ui.modals.camp.codex, codex_labels)
var atlas_locks := _labels_with_text(codex_labels, "结构位预留")
if atlas_locks.is_empty():
_problems.append("营地灰字:图鉴锁文 label 未找到(wc4 定位锚失守——锚随形态订正去『柜台』)〔句柄子容器批 V1〕")
_problems.append("营地灰字:图鉴锁文 label 未找到(wc4 定位锚失守——锚随形态订正去『柜台』)〔句柄子容器批 V1〕〔营地图批 V6〕")
for l in atlas_locks:
if l.get_theme_color("font_color") != UiKit.STONE_DEEP:
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕")
_problems.append("营地灰字:图鉴锁文未用 STONE_DEEP(wc4 :167)〔句柄子容器批 V1〕〔营地图批 V6〕")
main.ui.modals.camp.codex.closed.emit()
await get_tree().process_frame
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
@@ -702,7 +702,7 @@ func _ready() -> void:
main.ui.modals.confirm_dialog.confirmed.emit() # 「是」→开始新冒险
await get_tree().process_frame
if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible:
_problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕")
_problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕")
# 有断点三出口(选路停点形注入——同开场行③ set_breakpoint 先例)
main.save_manager.set_breakpoint({
"master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0,
+55
View File
@@ -104,6 +104,51 @@ 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〕")
var esc_src := 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_src.is_empty():
problems.append("[esc] 零动效扫描面为空(源文件读失败——非空前置失守)〔ESC批 V7〕")
# 退出项接线断言(卡⑤ V5——绑信号连接与调用链;headless 不可真退=静态判据+观测面局限申报)
var quit_wired := false
for c in main.ui.modals.camp.esc_overlay.quit_requested.get_connections():
if c.callable.get_method() == "_on_esc_quit":
quit_wired = true
if not quit_wired:
problems.append("[esc] 退出钮接线断链(quit_requested 连接目标≠_on_esc_quit)〔ESC批 V5〕")
var quit_chain := FileAccess.get_file_as_string("res://presentation/main.gd") + FileAccess.get_file_as_string("res://presentation/main_esc_flow.gd")
if not quit_chain.contains("MainEscFlow._on_quit(self)") or not quit_chain.contains("root._quit_game()"):
problems.append("[esc] 退出调用链断裂(_on_esc_quit→_on_quit→_quit_game)〔ESC批 V5〕")
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 +801,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
+4 -2
View File
@@ -134,6 +134,7 @@ tests/
| 地形瓦片套与格面数字可读 | HUD框架 | `TilePalette.digit_ink_mode()`/`TilePalette.normalize_floor_luma()` | `terrain_tile_sets_test.gd:_test_set_completeness`;`terrain_tile_sets_test.gd:_test_hue_domains`;`terrain_tile_sets_test.gd:_test_digit_ink_modes` |
| 站桩敌人呈现与结算演出落点 | 战斗表现 | `BattleStageView.enemy_standee_center()`/`BattleEndOverlay.open_victory()` | `standee_fx_sweep_test.gd:_init`;`pacing_queue_test.gd:_init` |
| 站桩呈现定值与素材绑定 | 战斗表现 | `UiKit.load_tex()`/`StandeeBody.name_line_probe()` | `standee_present_values_test.gd:_init` |
| 站桩显示档与素材分辨率绑定 | 战斗表现 | `StandeeBody.portrait_probe()`/`StandeeMotion.PORTRAIT_DISPLAY_H` | `standee_present_values_test.gd:_check_display_tier` |
| B 区舞台背景三关变体与连续地面层(按 board_type 选图/地面层高度读布局表/四柱光柱退役) | HUD框架 | `BattleStageView.set_board_type()` | `stage_background_test.gd:_test_background_selection`;`stage_background_test.gd:_test_ground_layer_geometry`;`stage_background_test.gd:_test_beams_retired` |
| 战斗屏三区几何与棋盘格边长(分辨率两档×四档板型) | HUD框架 | `BoardView.set_cell_area()`(D 区可用面积 → 格边长);读面 `BoardView.cell_size` | `layout_probe.gd:_ready`;`board_gen_reward_test.gd:_smoke_layout_cell_formula` |
| 调参测速批量模拟(逐帧步进与统计读数) | 调参测量 | `TuneSimController.run()`(调试浮窗批量模拟入口) | `tune_sim_batch_parity_test.gd:_init`(配对对拍套件,随测速循环批建) |
@@ -162,6 +163,7 @@ tests/
| 难度修饰套乘:空表零行为/套乘生效/越域 clamp | 难度曲线与Boss | `UnitContainer.spawn()`/`ConfigDbResolver.resolve_difficulty_modifiers()` | `table_structure_guard_test.gd:_smoke_difficulty_table`;`config_db_resolver_test.gd:_test_difficulty_clamp` |
| 锻造表 schema 正反与几何价格梯 | 锻造工坊 | `ForgeTable.level_price()`/`ForgeTable.route_rows()` | `forge_table_test.gd:_test_production_rows`;`forge_table_test.gd:_test_price_ladder`;`forge_table_test.gd:_test_negative_cases` |
| 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理 | 战斗结算×数据配置×增益状态 | `DamageController.open_ticket()`/`StatusContainer.apply_effect()` | `system_contracts_test.gd:_test_damage_controller_vocabularies`;`system_contracts_test.gd:_test_rng_tag_closure`;`system_contracts_test.gd:_test_status_boolean_acceptance` |
| 同名状态合并策略(叠加/累加/并存)与剩余次数语义 | 战斗循环×增益状态 | `StatusContainer.apply_effect()`(编排宿主 begin_battle 链) | `roll_multiplier_test.gd:_smoke_armor_roll` |
| 敌人技能池跨表链接校验 | 数据配置×敌人系统 | `ConfigDbCrossValidator.validate_skill_pool_links()` | `config_db_cross_validator_test.gd:_test_all_links_valid`;`config_db_cross_validator_test.gd:_test_dangling_skill_id`;`config_db_cross_validator_test.gd:_test_cast_side_mismatch` |
**覆盖域表**(`design/gdd/systems/` 逐篇对照——状态三档:已覆盖/部分覆盖/未覆盖;部分与未覆盖行的缺口指待办清单条目,覆盖面提升时同批改本表):
@@ -174,9 +176,9 @@ tests/
| 冒险循环 | 奇物.md | 专长(奇物)授予与凭证保底;奖励三选一候选/刷新付费/呈现 | 已覆盖 | — |
| 冒险循环 | 难度曲线与Boss.md | Boss 半血解锁技门控;行集寻址、行序保序与难度乘数 clamp;难度修饰套乘:空表零行为/套乘生效/越域 clamp | 部分覆盖 | 难度曲线整链与 Boss 出场门控无专项断言;见待办清单「能力清单覆盖缺口补齐」条 |
| 战斗循环 | 地图战斗资源.md | 清图奖励双份与零路径;GP 钱包/escrow/商店收口/共享池;封印与极性路由/不可逆 | 已覆盖 | — |
| 战斗循环 | 增益状态.md | 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理;封印与极性路由/不可逆 | 部分覆盖 | 增益叠层/衰减/清除结算链无断言;见待办清单「能力清单覆盖缺口补齐」条 |
| 战斗循环 | 增益状态.md | 系统间契约词表与受理口径:五配方互异/掷点标签成员性/增益布尔受理;同名状态合并策略(叠加/累加/并存)与剩余次数语义;封印与极性路由/不可逆 | 部分覆盖 | 增益叠层/衰减/清除结算链无断言;见待办清单「能力清单覆盖缺口补齐」条 |
| 战斗循环 | 战斗结算.md | 伤害公式与掷点乘区;伤害单据编排与回放;轮内流与结算五段全序 | 已覆盖 | — |
| 战斗循环 | 战斗表现.md | 站桩敌人呈现与结算演出落点;站桩呈现定值与素材绑定 | 部分覆盖 | 死亡灰化与减动效演出档仅探针与人工目检;见待办清单「能力清单覆盖缺口补齐」条 |
| 战斗循环 | 战斗表现.md | 站桩敌人呈现与结算演出落点;站桩呈现定值与素材绑定;站桩显示档与素材分辨率绑定 | 部分覆盖 | 死亡灰化与减动效演出档仅探针与人工目检;见待办清单「能力清单覆盖缺口补齐」条 |
| 战斗循环 | 敌人系统.md | 技能池数据面与回放独立;Boss 半血解锁技门控;敌人技能池跨表链接校验 | 已覆盖 | — |
| 排雷循环 | 地形变异.md | 变异布置门控;表结构守卫:域校验/行序键/跨字段互锁/配额锚 | 已覆盖 | — |
| 排雷循环 | 排雷玩法.md | 装板与开局免费展开(封锁格无资源、计入清图);挖/封印合法性与可辨拒绝(拒绝零副作用);封印不可逆与挖开封印格放行(兑付改判四情况) | 已覆盖 | — |