From a6614a3b8ec373e8609c8f4043bda132282730d3 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 05:07:51 +0800 Subject: [PATCH] =?UTF-8?q?iter33=20=E5=8D=A1=E2=91=A2=20T1+T2+T3:=20?= =?UTF-8?q?=E9=80=89=E6=A1=A3=E7=95=8C=E9=9D=A2=E6=96=B0=E5=BB=BA=EF=BC=88?= =?UTF-8?q?3=20=E5=AD=98=E6=A1=A3=E4=BD=8D=20=E6=96=B0=E6=A1=A3/=E8=BF=9B?= =?UTF-8?q?=E6=A1=A3/=E5=88=A0=E6=A1=A3=EF=BC=89=EF=BC=8B=E7=BB=84?= =?UTF-8?q?=E5=90=88=E6=A0=B9=E6=8E=A5=E7=BA=BF=EF=BC=8B=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E6=89=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 视图 save_select_overlay.gd:页壳先例(career 同款)纵列三行槽卡;摘要三项(钥石徽/通关局/总局—— 裁决 4 零断点读数);操作集二分(空/坏槽=仅新档,有档=进档+删档——Q2 甲坏槽按空呈现);删档只发 意图信号(二次确认归组合根);零预选(Q1 甲零选中态字段);零新素材零新色值(契约 7)。 组合根:LAYER_CONTRACT 加行(生涯同档)+_open_save_select 开层+_save_select_data 窥视取数 (occupied=has_file∧readable)+三回调;暂行落点 CampOverlay(Q3 乙,动线序=先落点开层不关选档); ConfirmDialog 消费方编排(静态常驻接线→open 前设消费方按量分派——sl8,_start_adventure_from_menu 同步设消费方)。窥视读器补 has_file 位(T0 对齐结论:空/有档二分判定来处,非摘要第四项)。 断言批:struct 段(sl1 计数/禁滚动+sl2 逐槽读数+sl3 禁词+sl4 二分+sl9 不预选);click 段 (sl5 新档链+sl6 进档链+sl7 确认双路径+sl8 分派单口+sl12 动线序逐帧采样);flow 步(sl10 返回路径+ESC 不响应)。开层探针=函数直调中间态(「进入存档」钮归卡② mn3)。 --- game/core/save_manager.gd | 10 +- game/presentation/main.gd | 96 +++++++++++++-- .../presentation/views/save_select_overlay.gd | 111 ++++++++++++++++++ .../views/save_select_overlay.gd.uid | 1 + game/smoke/screen_flow_probe.gd | 45 +++++++ game/smoke/ui_click_probe.gd | 94 +++++++++++++++ game/smoke/ui_struct_probe.gd | 97 +++++++++++++++ 7 files changed, 441 insertions(+), 13 deletions(-) create mode 100644 game/presentation/views/save_select_overlay.gd create mode 100644 game/presentation/views/save_select_overlay.gd.uid diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 5a8a72d1..01e0dc26 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -156,13 +156,14 @@ func delete_slot(slot: int) -> bool: # 摘要三项按槽窥视读器(sv7/sv8,契约 5/9):钥石余额/通关局数/冒险总局数。 # 只读零副作用:不激活、不写盘、不触发 _corrupt_fallback 改名(.corrupt.bak 只在正式进档路径产生)。 -# 返回 {"readable", "keystone", "cleared", "total"}——readable=true=可读槽(空槽=零值合法态, +# 返回 {"readable", "has_file", "keystone", "cleared", "total"}——readable=true=可读槽(空槽=零值合法态, # .tmp 残留按空槽——主文件不在即空);readable=false=不可读槽(损坏/高版本/缺版本字段/打开失败, -# 三值无意义——卡③ Q2 裁甲:UI 按空槽呈现,进档时走正式 load 兜底/拒读链)。 +# 三值无意义——卡③ Q2 裁甲:UI 按空槽呈现,进档时走正式 load 兜底/拒读链); +# has_file=主文件在盘与否(空/有档操作集二分判定来处——卡③ T0 对齐结论;非摘要第四项,零 UI 语义)。 # 迁移链在此只读推演(生产迁移 Callable 为纯函数,零盘面副作用)。 func peek_slot_summary(slot: int) -> Dictionary: if slot < 1 or slot > SLOT_COUNT: - return {"readable": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽 + return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽 return _peek_summary_at(slot_path(slot)) @@ -172,10 +173,11 @@ func peek_summary_at(path: String) -> Dictionary: func _peek_summary_at(path: String) -> Dictionary: - var out := {"readable": false, "keystone": 0, "cleared": 0, "total": 0} + var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} if not FileAccess.file_exists(path): out["readable"] = true # 空槽=可读零值合法态 return out + out["has_file"] = true # 主文件在盘(可读性与占档判定分离——白板档与空槽自此可区分) var f := FileAccess.open(path, FileAccess.READ) if f == null: return out diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 42bcc882..a5f3d6a2 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -45,6 +45,7 @@ const LAYER_CONTRACT: Array[Dictionary] = [ {"key": "settings_overlay", "kind": "modal", "tags": []}, {"key": "status_panel", "kind": "modal", "tags": []}, {"key": "career_overlay", "kind": "modal", "tags": []}, + {"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档) {"key": "inter_overlay", "kind": "state", "tags": []}, {"key": "confirm_dialog", "kind": "modal", "tags": []}, {"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]}, @@ -68,6 +69,8 @@ var _debug_entry := false # 调试会话门控(写死=true——恒 var _seed_edit: LineEdit var _resume_btn: Button # 继续上局按钮句柄(有断点才亮) var confirm_dialog: ConfirmDialog # 通用确认弹窗(新冒险互斥确认弹窗化) +var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清) +var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用) var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(level-clear.md v2 选择面承载;奖励节点共用) var _start_overlay: Control var _end_overlay: BattleEndOverlay @@ -114,6 +117,7 @@ var settings_store # 系统级设置独立存储(音量 var settings_overlay: SettingsOverlay # 设置覆盖层(最小设置面) var status_panel: StatusPanel # 局内状态面板·只读(钥石余额+营地三柜台进度查看;登记设置层同档) var career_overlay: CareerOverlay # 生涯页·只读全屏页(跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕) +var save_select_overlay: SaveSelectOverlay # 选档界面·3 存档位(新档/进档/删档——营地交互重做卡③;登记生涯同档) var debug_overlay: DebugOverlay # 调试浮窗(种子/数值调整/批量模拟/两层调试入口收一) var debug_badge: Button # 调试浮层入口角标(UI 最顶层浮层——LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡) @@ -288,6 +292,14 @@ func _build_ui() -> void: career_overlay = CareerOverlay.new() add_child(career_overlay) career_overlay.closed.connect(_on_career_closed) + # 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号 + #(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。 + save_select_overlay = SaveSelectOverlay.new() + add_child(save_select_overlay) + save_select_overlay.closed.connect(_on_save_select_closed) + save_select_overlay.new_slot_requested.connect(_on_save_select_new) + save_select_overlay.load_slot_requested.connect(_on_save_select_load) + save_select_overlay.delete_slot_requested.connect(_on_save_select_delete) # 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进; # 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child) curio_overlay = CurioPickOverlay.new() @@ -344,8 +356,10 @@ func _build_ui() -> void: # 诊断转储入口在调试浮窗) confirm_dialog = ConfirmDialog.new() add_child(confirm_dialog) - confirm_dialog.confirmed.connect(_on_confirm_new_adventure) - confirm_dialog.cancelled.connect(_on_confirm_cancelled) + # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, + # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) + confirm_dialog.confirmed.connect(_on_confirm_accepted) + confirm_dialog.cancelled.connect(_on_confirm_declined) # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; # 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块; @@ -417,6 +431,7 @@ func _layer_node(key: String) -> Node: "settings_overlay": return settings_overlay "status_panel": return status_panel "career_overlay": return career_overlay + "save_select_overlay": return save_select_overlay "inter_overlay": return _inter_overlay "confirm_dialog": return confirm_dialog "debug_overlay": return debug_overlay @@ -477,6 +492,7 @@ func _start_adventure_from_menu() -> void: # 「开新冒险」提交(弃旧断点+写盘)/「取消」回菜单 if save_manager.has_breakpoint(): var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() + _confirm_consumer = "new_adventure" # 消费方编排(契约 3——open 前设消费方) confirm_dialog.open("已有未完冒险(第 %d 关)——开新冒险将覆盖旧断点" % maxi(1, int(bp.get("level_index", 1))), "开新冒险", "取消") return @@ -484,16 +500,26 @@ func _start_adventure_from_menu() -> void: _start_adventure() -func _on_confirm_new_adventure() -> void: +# 确认弹窗分派(卡③ 契约 3):按 _confirm_consumer 路由——"new_adventure"=现役新冒险确认 +#(行为与原 _on_confirm_new_adventure 逐句等价)、"delete_slot"=卡③ 删档确认(确认才删、 +# 取消只关窗——裁决 3);分派面禁写死单一目标(卡④ 到位接入第三消费方不返工)。 +func _on_confirm_accepted() -> void: confirm_dialog.close() - save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) - save_manager.write_to_disk() - _start_overlay.visible = false - _start_adventure() + match _confirm_consumer: + "delete_slot": + save_manager.delete_slot(_confirm_consumer_slot) + save_select_overlay.open_save_select(_save_select_data()) # 就地刷新(该槽回空态) + "new_adventure": + save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) + save_manager.write_to_disk() + _start_overlay.visible = false + _start_adventure() + _confirm_consumer = "" -func _on_confirm_cancelled() -> void: - confirm_dialog.close() # 回开始菜单(无状态变更) +func _on_confirm_declined() -> void: + confirm_dialog.close() # 只关窗(取消无状态变更——裁决 3:取消不删) + _confirm_consumer = "" # 退出游戏(一级菜单入口;设置即时落盘后退出) @@ -1125,6 +1151,58 @@ func _career_data() -> Dictionary: } +# ==================== 选档界面(营地交互重做卡③)==================== + +# 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为 +# 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。 +func _open_save_select() -> void: + _start_overlay.visible = false + _open_modal(save_select_overlay) # 模态栈单口 + save_select_overlay.open_save_select(_save_select_data()) + + +func _on_save_select_closed() -> void: + _close_modal(save_select_overlay) # 模态栈单口 + _start_overlay.visible = true + _refresh_resume_button() + + +# 槽数据快照(只读同源——每次开层实时经卡① 窥视 API 逐槽取数拼装;零写调用)。 +# occupied = has_file ∧ readable:坏槽/高版本槽按空槽呈现(Q2 裁甲——进档/新档均经卡① 兜底链接管); +# 摘要仅三项(裁决 4),has_file 只是空/有档操作集二分判定来处(非摘要项)。 +func _save_select_data() -> Dictionary: + var slots: Array[Dictionary] = [] + for i in range(1, save_manager.SLOT_COUNT + 1): + var p: Dictionary = save_manager.peek_slot_summary(i) + slots.append({"slot": i, + "occupied": bool(p.get("has_file", false)) and bool(p.get("readable", false)), + "keystone": int(p.get("keystone", 0)), + "cleared": int(p.get("cleared", 0)), + "total": int(p.get("total", 0))}) + return {"slots": slots} + + +# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘, +# 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。 +func _on_save_select_new(slot: int) -> void: + save_manager.create_slot(slot) + _camp_pres.open_camp() + + +# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。 +func _on_save_select_load(slot: int) -> void: + save_manager.activate_slot(slot) + _camp_pres.open_camp() + + +# 删档链(契约 3):第一次点击只出确认窗(消费方编排——open 前设消费方=delete_slot), +# 确认才删、取消不删;禁长按删/一次点击即删/撤销式软删。 +func _on_save_select_delete(slot: int) -> void: + _confirm_consumer = "delete_slot" + _confirm_consumer_slot = slot + confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") + + # 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰, # 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding) func _on_status_badge_pressed() -> void: diff --git a/game/presentation/views/save_select_overlay.gd b/game/presentation/views/save_select_overlay.gd new file mode 100644 index 00000000..de3f4e1f --- /dev/null +++ b/game/presentation/views/save_select_overlay.gd @@ -0,0 +1,111 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 选档界面·全屏页:3 存档位(新档/进档/删档)+ 槽摘要三项(钥石余额/通关局数/冒险总局数)。 +## 结构树层级:选档 → 进档 → 营地(本面零存档内功能入口——禁营地/冒险/生涯跳转与购买/查看入口); +## 删档必经二次确认(确认弹窗与分派归组合根消费方编排——本面只发意图信号)。 +## 槽数与槽态(空/有档)唯一来自组合根喂入数据(open_save_select 每开重建——组合根经卡① 窥视 API +## 取数;视图禁自数文件/禁自拼槽名/禁硬编码槽判定);Q1 甲=零预选(视图零选中态字段、零持久化)。 +## 摘要面仅三项,零断点/未完冒险读数(裁决 4——槽卡只消费喂入三键)。 +## 构成件全部经 UiKit 工厂产出——零新素材/新色值(契约 7)。 +class_name SaveSelectOverlay +extends Control + +signal closed +signal new_slot_requested(slot: int) +signal load_slot_requested(slot: int) +signal delete_slot_requested(slot: int) + +var _rows_box: VBoxContainer +var _summary_labels: Dictionary[int, Label] = {} # 槽号→摘要读数 label(通关/冒险两值——探针取值缝) +var _occupied_flags: Dictionary[int, bool] = {} # 槽号→有档态(操作集二分断言缝;判定源=喂入数据) +var _new_btns: Dictionary[int, Button] = {} # 槽号→新档钮(空/坏槽——操作集断言与点击链驱动缝) +var _load_btns: Dictionary[int, Button] = {} # 槽号→进档钮(有档槽) +var _delete_btns: Dictionary[int, Button] = {} # 槽号→删档钮(有档槽——确认链驱动缝) + + +func _init() -> void: + # 全屏页壳(career_overlay 同款先例——暖亮底+石纹+暗角+56px 通栏顶条+内容列) + var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, true) + var top: HBoxContainer = page.top_row + top.add_child(UiKit.label("存 档",UiKit.FS_TITLE, true, UiKit.WARM)) + var top_sp := Control.new() + top_sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL + top.add_child(top_sp) + var close_btn := UiKit.button("返回", 90.0) # 紧凑返回钮(生涯/设置先例形制) + close_btn.tooltip_text = "关闭选档界面,返回主菜单(进入存档位前不发生任何进度读写)" + close_btn.pressed.connect(_emit_closed) + top.add_child(close_btn) + + var box: VBoxContainer = page.box + # 对比度口径:亮底页用亮墨族 token(career_overlay 同款——深墨族禁用) + box.add_child(UiKit.label("选择一个存档位进入。空存档位可新开档;删档需二次确认。", + UiKit.FS_HINT, false, UiKit.WARM_SOFT)) + _rows_box = VBoxContainer.new() + _rows_box.add_theme_constant_override("separation", 10) + box.add_child(_rows_box) + + visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律) + + +# ---- 开层(组合根唯一调用口——槽数据随开重建;删档确认后就地重喂刷新)---- +# data = {"slots": [{"slot": int, "occupied": bool, "keystone": int, "cleared": int, "total": int}, ...]} +# 槽数=数组长度(位数唯一在卡① 承载面 SLOT_COUNT);occupied=组合根经窥视 API 判定 +# (has_file∧readable——坏槽/高版本槽按空槽呈现,Q2 裁甲);摘要仅消费三键(裁决 4)。 +func open_save_select(data: Dictionary) -> void: + for c in _rows_box.get_children(): + _rows_box.remove_child(c) + c.queue_free() # 先摘树后释放——重建无同帧叠影 + _slot_labels_clear() + var slots: Array = data.get("slots", []) as Array + for sd: Dictionary in slots: + _build_slot_row(sd) + visible = true + + +func _slot_labels_clear() -> void: + _summary_labels.clear() + _occupied_flags.clear() + _new_btns.clear() + _load_btns.clear() + _delete_btns.clear() + + +func _build_slot_row(sd: Dictionary) -> void: + var slot := int(sd.get("slot", 0)) + var occupied := bool(sd.get("occupied", false)) + _occupied_flags[slot] = occupied + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 14) + _rows_box.add_child(row) + var name_l := UiKit.label("存档位 %d" % slot,UiKit.FS_SUB, true, UiKit.WARM) + name_l.custom_minimum_size = Vector2(110, 0) + row.add_child(name_l) + var sp := Control.new() + sp.size_flags_horizontal = Control.SIZE_EXPAND_FILL + if occupied: + row.add_child(UiKit.keystone_badge("钥石 %d" % int(sd.get("keystone", 0)))) + var summary_l := UiKit.label("通关 %d 局 · 冒险 %d 局" % [int(sd.get("cleared", 0)), int(sd.get("total", 0))], + UiKit.FS_BODY, false, UiKit.CANDLE) + _summary_labels[slot] = summary_l # 摘要读数缝(禁词断言枚举 Label 全集时含此件) + row.add_child(summary_l) + row.add_child(sp) + var load_btn := UiKit.primary_button("进档") + load_btn.tooltip_text = "激活存档位 %d 并进入营地" % slot + load_btn.pressed.connect(func() -> void: load_slot_requested.emit(slot)) + row.add_child(load_btn) + _load_btns[slot] = load_btn + var del_btn := UiKit.danger_button("删档", "删除存档位 %d(需二次确认——删除后该档不可恢复)" % slot) + del_btn.pressed.connect(func() -> void: delete_slot_requested.emit(slot)) + row.add_child(del_btn) + _delete_btns[slot] = del_btn + else: + row.add_child(UiKit.label("空存档位",UiKit.FS_BODY, false, UiKit.WARM_FAINT)) + row.add_child(sp) + var new_btn := UiKit.primary_button("新档") + new_btn.tooltip_text = "在存档位 %d 新开一档(初始白板)" % slot + new_btn.pressed.connect(func() -> void: new_slot_requested.emit(slot)) + row.add_child(new_btn) + _new_btns[slot] = new_btn + + +func _emit_closed() -> void: + closed.emit() diff --git a/game/presentation/views/save_select_overlay.gd.uid b/game/presentation/views/save_select_overlay.gd.uid new file mode 100644 index 00000000..69ccf6b1 --- /dev/null +++ b/game/presentation/views/save_select_overlay.gd.uid @@ -0,0 +1 @@ +uid://cd3ayrxyco3ju diff --git a/game/smoke/screen_flow_probe.gd b/game/smoke/screen_flow_probe.gd index ef4119d0..1712f59d 100644 --- a/game/smoke/screen_flow_probe.gd +++ b/game/smoke/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_save_select_open_back() # 迭代 33 卡③ sl10——选档开合+返回路径+ESC 语境不响应 await _step_menu_to_adventure() await _step_level_to_battle_and_back() await _step_abandon_to_end() @@ -126,6 +127,50 @@ func _step_camp_back_to_menu() -> void: # ---- ④ 菜单 → 冒险(开新冒险 → 入口停点落关卡地图屏)---- +# ---- 选档界面开合(卡③ sl10):函数直调开层(「进入存档」钮归卡② mn3)→屏级互斥采样→ +# 返回钮真实信号路径关闭→ESC 语境不响应(卡⑤ 契约 2 范围外——零实现即断言)。---- +func _step_save_select_open_back() -> void: + ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, false, true) + main.save_manager.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json") + main._open_save_select() + await get_tree().process_frame + var seals := _screen_seals() + if not main.save_select_overlay.visible or not seals_check_single(seals): + _problems.append("[savesel] sl10 开层后屏级互斥失守(选档=%s)" % str(main.save_select_overlay.visible)) + var back_btn: Button = ProbeUiUtil.find_button(main.save_select_overlay, "返回", "prefix") + if back_btn == null: + _problems.append("[savesel] sl10 返回钮缺失(页壳顶条)") + else: + back_btn.pressed.emit() + await get_tree().process_frame + if main.save_select_overlay.visible or not main._start_overlay.visible: + _problems.append("[savesel] sl10 返回后选档未隐藏或开始层未恢复(关闭链失守)") + # ESC 语境:再开选档→合成 ESC 键→层不关、流程零扰动(ESC 不属选档语境——零实现断言) + main._open_save_select() + await get_tree().process_frame + var ev := InputEventKey.new() + ev.keycode = KEY_ESCAPE + ev.physical_keycode = KEY_ESCAPE + ev.pressed = true + main.get_viewport().push_input(ev, true) + await get_tree().process_frame + if not main.save_select_overlay.visible: + _problems.append("[savesel] sl10 ESC 关闭了选档层(选档语境 ESC 应不响应——卡⑤ 范围外)") + main._on_save_select_closed() + await get_tree().process_frame + + +# 互斥判据(本步局部——四屏 seal 之外新增选档位的同形判据) +func seals_check_single(f: Dictionary) -> bool: + var on := 0 + for k in f: + if bool(f[k]): + on += 1 + if main.save_select_overlay.visible: + on += 1 # 选档自身计入(调用点已开层) + return on == 1 + + func _step_menu_to_adventure() -> void: var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains") if lvbtn == null: diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 90938a1a..a0c17e6b 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -125,6 +125,100 @@ func _ready() -> void: await get_tree().process_frame if main.camp_overlay.visible or not main._start_overlay.visible: _problems.append("营地关闭后未恢复开始层") + # ---- 选档界面行为走查(迭代 33 卡③):sl5 空槽新档链/sl6 有档槽进档链/sl7 删档二次确认双路径 + # /sl8 消费方分派单口/sl12 动线序不回闪。开层=函数直调(「进入存档」钮归卡② mn3——中间态在案); + # 隔离=attach_slot_template 注入命名空间,夹具按槽直写(卡① 注入口)。---- + var csel_tpl := "user://smoke_click_probe_savesel_slot_%d.json" + for i in range(1, 4): + for suf in ["", ".tmp", ".corrupt.bak"]: + var cp: String = csel_tpl % i + suf + if FileAccess.file_exists(cp): + DirAccess.remove_absolute(cp) + ProbeUiUtil.isolate_save(main, "smoke_click_probe_savesel", true, false, false, true) + main.save_manager.attach_slot_template(csel_tpl) + var f_c2 := FileAccess.open(csel_tpl % 2, FileAccess.WRITE) + f_c2.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 100, + "adventure_total": 3, "adventure_cleared": 1, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}})) + f_c2.close() # 槽2 有档异值(进档链夹具);槽1/3 空 + main._open_save_select() + await get_tree().process_frame + if not main.save_select_overlay.visible: + _problems.append("[savesel] 选档层未打开(click 段前置)") + # sl12 动线序(新档链同拍采样):逐帧可见性次序——首帧选档隐藏时营地层必须已可见、开始层保持隐藏 + #(禁先关后开——_close_modal 会恢复开始层=回闪一帧) + var new_btn: Button = main.save_select_overlay._new_btns.get(1) + if new_btn == null: + _problems.append("[savesel] 槽1 新档钮缺失(sl5/sl12 驱动中断)") + else: + var samples: Array = [] + new_btn.pressed.emit() # 真实按钮信号路径 → _on_save_select_new → create_slot+open_camp + for fi in 3: + await get_tree().process_frame + samples.append([main.save_select_overlay.visible, main.camp_overlay.visible, main._start_overlay.visible]) + if samples[0][0]: + _problems.append("[savesel] sl12 新档后选档层仍可见(落点开层未发生)") + elif not samples[0][1]: + _problems.append("[savesel] sl12 动线序违约:选档隐藏帧营地层未可见(先关后开=回闪形态)") + elif samples[0][2]: + _problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)") + if main.save_manager.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1): + _problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.active_slot()) + if int((JSON.parse_string(FileAccess.get_file_as_string(csel_tpl % 1)) as Dictionary).get("meta", {}).get("keystone_balance", -1)) != 0: + _problems.append("[savesel] sl5 新档非白板(槽1 落盘应全零)") + main._on_camp_closed() + await get_tree().process_frame + # sl6 进档链:槽2 进档 → 激活=2(camp 数据随激活槽) + main._open_save_select() + await get_tree().process_frame + var load_btn: Button = main.save_select_overlay._load_btns.get(2) + if load_btn == null: + _problems.append("[savesel] 槽2 进档钮缺失(sl6 驱动中断——夹具应有档)") + else: + load_btn.pressed.emit() + await get_tree().process_frame + if main.save_manager.active_slot() != 2: + _problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.active_slot()) + if not main.camp_overlay.visible or main.save_select_overlay.visible: + _problems.append("[savesel] sl6 落点未开营地层或选档未隐藏") + main._on_camp_closed() + await get_tree().process_frame + # sl7 删档二次确认双路径:一击只开窗(文件仍在)→取消只关窗(文件仍在)→再开窗确认才删(槽回空) + main._open_save_select() + await get_tree().process_frame + var del_btn: Button = main.save_select_overlay._delete_btns.get(2) + if del_btn == null: + _problems.append("[savesel] 槽2 删档钮缺失(sl7 驱动中断)") + else: + del_btn.pressed.emit() + await get_tree().process_frame + if not main.confirm_dialog.is_open(): + _problems.append("[savesel] sl7 一击未只开确认窗(直删形态)") + elif not FileAccess.file_exists(csel_tpl % 2): + _problems.append("[savesel] sl7 确认前文件已被删(一次点击即删违契)") + else: + main.confirm_dialog.cancelled.emit() # 取消路径(真实信号——cancelled 钮同源) + await get_tree().process_frame + if main.confirm_dialog.is_open() or not FileAccess.file_exists(csel_tpl % 2): + _problems.append("[savesel] sl7 取消路径失守(应只关窗、文件仍在)") + elif main._confirm_consumer != "": + _problems.append("[savesel] sl8 取消后消费方状态量未清(分派面残留)") + del_btn.pressed.emit() # 再开窗走确认路径 + await get_tree().process_frame + main.confirm_dialog.confirmed.emit() # 确认路径(真实信号——confirmed 钮同源) + await get_tree().process_frame + if FileAccess.file_exists(csel_tpl % 2) or main.confirm_dialog.is_open(): + _problems.append("[savesel] sl7 确认路径失守(应删档+关窗)") + elif main.run_scope.is_adventure or main._start_overlay.visible: + _problems.append("[savesel] sl8 删档确认误触发开局(双常驻接线形态——分派单口失守)") + elif bool(main.save_select_overlay._occupied_flags.get(2, true)): + _problems.append("[savesel] sl7 删后选档面未就地刷新(槽2 应回空态)") + elif main._confirm_consumer != "": + _problems.append("[savesel] sl8 确认后消费方状态量未清(分派面残留)") + main._on_save_select_closed() + await get_tree().process_frame + if main.save_select_overlay.visible or not main._start_overlay.visible: + _problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") + # ---- 设置面段(隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)---- for p0 in ["user://smoke_click_probe_settings.cfg"]: if FileAccess.file_exists(p0): diff --git a/game/smoke/ui_struct_probe.gd b/game/smoke/ui_struct_probe.gd index 16ff114b..67e6f580 100644 --- a/game/smoke/ui_struct_probe.gd +++ b/game/smoke/ui_struct_probe.gd @@ -68,6 +68,38 @@ func _ready() -> void: else: problems.append("[career] 生涯入口按钮缺失(开始层 careerbtn)") + # ---- 选档界面(迭代 33 卡③):函数直调开层(「进入存档」钮归卡② mn3——点击链在卡② 承接), + # sl1 三槽呈现/sl2 摘要三项逐槽读数/sl3 摘要禁项/sl4 操作集二分/sl9 重启链不预选。 + # 隔离:attach_slot_template 注入命名空间(卡① 注入口);夹具按槽直写——槽1 有档异值、 + # 槽2 空槽、槽3 坏档(Q2 甲=按空槽呈现→操作集与空槽同形)。---- + var ssel_tpl := "user://smoke_ui_struct_probe_savesel_slot_%d.json" + for i in range(1, 4): + for suf in ["", ".tmp", ".corrupt.bak"]: + var spp: String = ssel_tpl % i + suf + if FileAccess.file_exists(spp): + DirAccess.remove_absolute(spp) + ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_savesel", true, false, true) + main.save_manager.attach_slot_template(ssel_tpl) # 注入口(激活态回零——sl9 重启链同构态) + var f_s1 := FileAccess.open(ssel_tpl % 1, FileAccess.WRITE) + f_s1.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 555, + "adventure_total": 7, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}})) + f_s1.close() # 槽1 有档异值 + var f_s3 := FileAccess.open(ssel_tpl % 3, FileAccess.WRITE) + f_s3.store_string("broken json not a save{{{") # 槽3 坏档(窥视 readable=false→按空呈现) + f_s3.close() + if main.save_manager.active_slot() != 0: + problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)") + main._open_save_select() + _settle() + if not main.save_select_overlay.visible or main._start_overlay.visible: + problems.append("[savesel] 选档层未打开或开始层未隐藏(开层函数断裂)") + await _audit("savesel", [main.save_select_overlay]) + _save_select_contract(main, ssel_tpl) + main._on_save_select_closed() + _settle() + if main.save_select_overlay.visible or not main._start_overlay.visible: + problems.append("[savesel] 关闭后层未隐藏或开始层未恢复(模态栈单口失守)") + main.confirm_dialog.open("结构探针采样", "确认") _settle() await _audit("confirm", [main.confirm_dialog]) @@ -330,6 +362,71 @@ func _career_zero_motion_contract(main: Node) -> void: # 两特例按合同数据表达:level_screen 宿住冒险屏壳(树序随宿主=全模态之下,遮挡语义归模态栈 # screen_state 登记)、camp_overlay 成员与 camp_screen 屏壳同槽(屏壳树序代言成员登记位)。 # _career_registry_contract 的断言(career=状态面板后/确认弹窗前、树序同步)由本全量合同覆盖) +# ⑭选档界面契约(卡③ sl1/sl2/sl3/sl4/sl9——呈现面全部来自喂入数据,视图零自判) +func _save_select_contract(main: Node, tpl: String) -> void: + var sso: Node = main.save_select_overlay + # sl1 三槽呈现:槽卡计数==3+零滚动容器(禁滚动长列表形态——契约 1) + var rows: VBoxContainer = sso._rows_box + var live_rows := 0 + for c in rows.get_children(): + if c is Control and not c.is_queued_for_deletion(): + live_rows += 1 + if live_rows != 3: + problems.append("[savesel] 槽卡行数=%d 应=3(sl1 三槽呈现)" % live_rows) + var scroll_count := _count_scroll(sso) + if scroll_count > 0: + problems.append("[savesel] 子树出现滚动容器 ×%d(sl1 禁滚动长列表)" % scroll_count) + # sl4 操作集二分:槽1 有档=进档+删档零新档;槽2 空=新档零进档删档;槽3 坏档=按空槽呈现(Q2 甲) + var occ: Dictionary = sso._occupied_flags + if not (bool(occ.get(1, false)) and not bool(occ.get(2, false)) and not bool(occ.get(3, false))): + problems.append("[savesel] 槽态判定失守(occ=%s 应=1有/2空/3坏按空)〔sl4〕" % str(occ)) + for slot in [1, 2, 3]: + var has_load: bool = sso._load_btns.has(slot) + var has_del: bool = sso._delete_btns.has(slot) + var has_new: bool = sso._new_btns.has(slot) + if slot == 1: + if not (has_load and has_del and not has_new): + problems.append("[savesel] 槽1 操作集失守(有档槽应=进档+删档、零新档)〔sl4〕") + else: + if not (has_new and not has_load and not has_del): + problems.append("[savesel] 槽%d 操作集失守(空/坏槽应=仅新档、零进档删档含置灰形态)〔sl4〕" % slot) + # sl2 摘要三项逐槽读数:槽1 喂入值逐字(钥石 555/通关 2 局/冒险 7 局) + var sum1: Label = sso._summary_labels.get(1) + if sum1 == null or sum1.text != "通关 2 局 · 冒险 7 局": + problems.append("[savesel] 槽1 摘要读数失守(got=%s 应=通关 2 局 · 冒险 7 局)〔sl2〕" % (sum1.text if sum1 else "null")) + var all_texts: Array[String] = [] + _collect_labels(sso, all_texts) + var has_keystone := false + for t in all_texts: + if t == "钥石 555": + has_keystone = true + if not has_keystone: + problems.append("[savesel] 槽1 钥石徽读数缺失(应=钥石 555)〔sl2〕") + # sl3 摘要禁项:断点/种子/「继续」字样零命中(裁决 4——零未完冒险读数) + for needle in ["断点", "种子", "继续"]: + for t in all_texts: + if t.contains(needle): + problems.append("[savesel] 摘要禁项命中「%s」(label=「%s」)〔sl3〕" % [needle, t]) + # sl9 重启链不预选:视图零选中态字段+喂入数据重建(无「已选」标记文本) + for t in all_texts: + if t.contains("已选"): + problems.append("[savesel] 出现预选标记文本「%s」(sl9 Q1 甲不预选)" % t) + + +func _count_scroll(node: Node) -> int: + var count := 1 if node is ScrollContainer else 0 + for c in node.get_children(): + count += _count_scroll(c) + return count + + +func _collect_labels(node: Node, out: Array[String]) -> void: + if node is Label: + out.append(String(node.text)) + for c in node.get_children(): + _collect_labels(c, out) + + func _modal_registry_contract(main: Node) -> void: var list: Array = main._modals._list # ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源)