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 1/5] =?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 # ① 登记册序==合同模态序(逐位点名——合同是登记序唯一事实源) From 52dd1ad239b4a6e38672876ed005d6d82103e2a6 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 05:27:56 +0800 Subject: [PATCH 2/5] =?UTF-8?q?iter33=20=E5=8D=A1=E2=91=A2=20T3=20?= =?UTF-8?q?=E5=8F=98=E5=BC=82=E4=BC=B4=E7=94=9F=E6=96=AD=E8=A8=80=E5=BC=BA?= =?UTF-8?q?=E5=8C=96=EF=BC=9Asl4=20=E6=94=B9=E6=B8=B2=E6=9F=93=E9=9D=A2?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E6=89=AB=E6=8F=8F=EF=BC=88=E7=B0=BF=E8=AE=B0?= =?UTF-8?q?=E7=BC=9D=E7=9B=B2=E5=8C=BA=E7=94=B1=E5=8F=98=E5=BC=82m3?= =?UTF-8?q?=E6=9A=B4=E9=9C=B2=EF=BC=89=EF=BC=8Bsl3=20=E8=A1=A5=E5=85=B3?= =?UTF-8?q?=E6=95=B0=E8=AF=8D=E5=BD=A2=E3=80=8C=20=E5=85=B3=E3=80=8D?= =?UTF-8?q?=EF=BC=8Bsl12=20=E8=A1=A5=E6=A0=88=E9=81=AE=E6=8C=A1=E8=AE=B0?= =?UTF-8?q?=E8=B4=A6=E5=88=A4=E5=88=AB=EF=BC=88=E5=85=88=E5=85=B3=E5=90=8E?= =?UTF-8?q?=E5=BC=80=E7=9A=84=E5=8F=AF=E5=88=A4=E5=88=AB=E9=9D=A2=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=9B=9E=E9=97=AA=E5=89=8D=E6=8F=90=E5=9C=A8=E7=8E=B0?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E6=9C=BA=E5=88=B6=E4=B8=8B=E4=B8=8D=E6=88=90?= =?UTF-8?q?=E7=AB=8B=EF=BC=8C=E6=94=B9=E9=92=89=E9=80=89=E6=A1=A3=E8=A2=AB?= =?UTF-8?q?=E6=A0=88=E6=8C=82=E8=B5=B7=E9=9D=9E=E5=85=B3=E9=97=AD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变异组十条全数注入→最窄闸见红→git checkout 精确还原:m1 直连删除/m2 摘要读激活位/m3 空槽删档钮/m4 双常驻接线(忠实无条件形态)/m5 返回不恢复/m6 先关后开/m7 滚动容器/m8 预选标记/m9 拼禁词/m-ESC 加处理。 --- game/smoke/ui_click_probe.gd | 3 +++ game/smoke/ui_struct_probe.gd | 35 +++++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index a0c17e6b..7b6c8343 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -163,6 +163,9 @@ func _ready() -> void: _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()) + var occluded: Array = main._modals._occluded.get(main.camp_overlay, []) + if not occluded.has(main.save_select_overlay): + _problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)") 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() diff --git a/game/smoke/ui_struct_probe.gd b/game/smoke/ui_struct_probe.gd index 67e6f580..a1b829e2 100644 --- a/game/smoke/ui_struct_probe.gd +++ b/game/smoke/ui_struct_probe.gd @@ -376,20 +376,27 @@ func _save_select_contract(main: Node, tpl: String) -> void: var scroll_count := _count_scroll(sso) if scroll_count > 0: problems.append("[savesel] 子树出现滚动容器 ×%d(sl1 禁滚动长列表)" % scroll_count) - # sl4 操作集二分:槽1 有档=进档+删档零新档;槽2 空=新档零进档删档;槽3 坏档=按空槽呈现(Q2 甲) + # 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: + var row_idx := 0 + for row_c in rows.get_children(): + if not (row_c is Control) or row_c.is_queued_for_deletion(): + continue + row_idx += 1 + var btn_texts: Array[String] = [] + _collect_button_texts(row_c, btn_texts) + var has_load := btn_texts.has("进档") + var has_del := btn_texts.has("删档") + var has_new := btn_texts.has("新档") + if row_idx == 1: if not (has_load and has_del and not has_new): - problems.append("[savesel] 槽1 操作集失守(有档槽应=进档+删档、零新档)〔sl4〕") + problems.append("[savesel] 行1 操作集失守(有档槽应=进档+删档、零新档——渲染面实测)〔sl4〕") else: if not (has_new and not has_load and not has_del): - problems.append("[savesel] 槽%d 操作集失守(空/坏槽应=仅新档、零进档删档含置灰形态)〔sl4〕" % slot) + problems.append("[savesel] 行%d 操作集失守(空/坏槽应=仅新档、零进档删档含置灰形态)〔sl4〕" % row_idx) # sl2 摘要三项逐槽读数:槽1 喂入值逐字(钥石 555/通关 2 局/冒险 7 局) var sum1: Label = sso._summary_labels.get(1) if sum1 == null or sum1.text != "通关 2 局 · 冒险 7 局": @@ -402,8 +409,9 @@ func _save_select_contract(main: Node, tpl: String) -> void: has_keystone = true if not has_keystone: problems.append("[savesel] 槽1 钥石徽读数缺失(应=钥石 555)〔sl2〕") - # sl3 摘要禁项:断点/种子/「继续」字样零命中(裁决 4——零未完冒险读数) - for needle in ["断点", "种子", "继续"]: + # sl3 摘要禁项:断点/种子/「继续」/停点/「 关」(关数形——「通关」无空格不误伤)零命中 + #(裁决 4——零未完冒险读数;词形覆盖=对照表 sl3 变异「断点关数拼进槽卡」的拦截面) + for needle in ["断点", "种子", "继续", "停点", " 关"]: for t in all_texts: if t.contains(needle): problems.append("[savesel] 摘要禁项命中「%s」(label=「%s」)〔sl3〕" % [needle, t]) @@ -420,6 +428,13 @@ func _count_scroll(node: Node) -> int: return count +func _collect_button_texts(node: Node, out: Array[String]) -> void: + if node is Button and node.visible: + out.append(String(node.text)) + for c in node.get_children(): + _collect_button_texts(c, out) + + func _collect_labels(node: Node, out: Array[String]) -> void: if node is Label: out.append(String(node.text)) From 54bde3678ab8707872c2a117d75e7a9e8abcb91f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 05:31:13 +0800 Subject: [PATCH 3/5] =?UTF-8?q?iter33=20=E5=8D=A1=E2=91=A2=20T4:=20?= =?UTF-8?q?=E4=BA=A7=E5=87=BA=E7=89=A9=E8=90=BD=E4=BD=8D=EF=BC=8B=E5=8D=A1?= =?UTF-8?q?=E2=91=A2=20F7=20=E8=A1=8C=E5=8F=B7=E8=AE=A2=E6=AD=A3=EF=BC=8B?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=E9=99=84=E6=B3=A8=E8=A1=A5=20has=5Ffile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现报告卡③节(实际对照表 sl1-sl14 双端对账+变异表十条+confirm 断言面清点+已知中间态); 追认单卡③ 7 项(§9 六项+has_file 接口补全待追认);反哺点卡③ 4 条(生涯结构树/08 增行请求/ 存档序列化窥视口径/流程清单)。 --- reports/iter33-代拍板追认单-2026-10-21.md | 12 ++++ reports/iter33-反哺点-2026-10-21.md | 9 +++ reports/iter33-实现报告-2026-10-21.md | 68 +++++++++++++++++++ reports/需求卡片-选档界面新建批-2026-10-18.md | 4 +- 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/reports/iter33-代拍板追认单-2026-10-21.md b/reports/iter33-代拍板追认单-2026-10-21.md index d1d55170..0b4d90cf 100644 --- a/reports/iter33-代拍板追认单-2026-10-21.md +++ b/reports/iter33-代拍板追认单-2026-10-21.md @@ -12,3 +12,15 @@ | 4 | 删档边界(§9-4) | 激活槽可删;删后回合法空态+指针连清除;**`.corrupt.bak` 损坏备份不追删**(用户数据留盘可考,同槽再损坏时兜底流按槽覆盖同名备份) | 禁崩禁误写 ✓;激活态生命周期终点=显式清除/进程结束/该槽被删 ✓;回主菜单不清除 ✓(内存态) | 追删备份=删档语义扩一步,改 delete_slot 一处 | | 5 | 提交切分与 worktree 命名(§9-5) | worktree `iter33card1-save`/分支 `iter33-card1-save`;两笔:T1+T2+T3 实现(6a7c2575)+T4 产出物 | 单 worktree 单分支 ✓;定义面(迭代行/卡片)不入本批提交 ✓ | 仅过程命名,veto 无实质影响 | | 6 | Q1 甲执行取法(实现取法,超出字面处陈明) | 旧档迁移取「字节级原样搬运、零解析零改造」——损坏/高版本旧档字节原样落槽 1(「原样落入槽 1」字面成立),读取时兜底/拒读链接管;未走 §8 L3 呈报(意外形态不再触发承诺失效) | 不静默丢数据 ✓;源改名 `.migrated.bak` 留痕不删 ✓;失败源保留重试 ✓ | 若用户裁「损坏旧档应 L3 停机」——则补迁移前预检解析、损坏即停的分支 | + +## 卡③ 选档界面新建批 + +| # | 代拍项(卡片 §9 条目) | 取法 | 约束核对 | veto 影响 | +|---|---|---|---|---| +| 1 | 槽卡布局(§9-1) | **纵列三行**(页壳内容列内每槽一行:槽名+摘要+动作钮)——career 页行族同构,等宽纪律零豁免 | 三槽全显一屏(契约 1 禁滚动)✓;1440 宽内单行容纳 | 改横列三卡=视图布局重排(L2 域) | +| 2 | 槽位编号呈现(§9-2) | 「存档位 1/2/3」 | 用户原话口径直用 | 改措辞=视图一处 | +| 3 | 三钮文案(§9-3) | 「新档」/「进档」/「删档」(动词钮,对齐现役动词钮先例;进档=primary 主操作钮、删档=danger_button 危险钮 `ui_kit.gd:695`) | 裁决 3/4 语义直用 ✓ | 改文案=视图常量 | +| 4 | 与开始层互斥方式(§9-4) | career 同款(`_start_overlay.visible=false`+模态栈单口;关闭恢复) | 仿先例域内参数选择 ✓ | — | +| 5 | 提交切分与 worktree 命名(§9-5) | worktree `iter33card3-saveselect`/分支 `iter33-card3-saveselect`;三笔:T1+T2+T3 实现+变异伴生强化+T4 产出物 | — | — | +| 6 | ConfirmDialog 消费方编排机制(§9-6,卡片预授权取法照录) | **open 前设消费方状态量(`_confirm_consumer`)、confirmed/cancelled 后按量分派(`_on_confirm_accepted`/`_on_confirm_declined` match 路由)**;双钮签名未动(`confirm_dialog.gd:36-38`);分派面=match 分支可扩展(卡④ 第三消费方直接加分支) | 禁双常驻静态接线 ✓(变异m4 拦截在役);本批消费方=删档确认+现役新冒险确认(随卡②撤出) | 机制重选=分派层重写 | +| 7 | 窥视读器 has_file 位(实现期接口补全——超出 §9 清单单项,按 T0 对齐授权补记待追认) | `peek_slot_summary` 返回结构加 `has_file: bool`(空/有档操作集二分判定来处;卡③ 6.0「槽态唯一来处=卡① 窥视 API 返回值」的接口闭环——交付面缺该位则二分不可实现) | 非摘要第四项(契约 5 三项不变)✓;向后兼容(既有断言零改)✓;卡① 侧 sv7/sv8 断言复跑绿 ✓ | 若裁「二分判定改走他口」(如独立 has_slot API)——save_manager 增一访问器+组合根改一行 | diff --git a/reports/iter33-反哺点-2026-10-21.md b/reports/iter33-反哺点-2026-10-21.md index cea3310f..694414de 100644 --- a/reports/iter33-反哺点-2026-10-21.md +++ b/reports/iter33-反哺点-2026-10-21.md @@ -12,3 +12,12 @@ | 4 | 同上(新增条款建议) | 无旧档迁移记载 | 补一段旧单档一次性迁移口径:`user://save.json` 首次启动字节迁入槽 1、源改名 `.migrated.bak` 留痕、槽 1 已存在不重迁(幂等) | 卡① Q1 裁甲+sv12 实装(契约 4 旧档不静默丢失的机制化) | | 5 | `待办清单.md` 营地交互重做条目 | 状态「…已编迭代 33」 | 卡① 完工后条目状态推进(归流水线登记段/文档批按 §12.1 执行) | doc-conventions §12.1;实现批禁写两份流程清单 | | 6 | `roadmap.md` 迭代 33 行与顶部「当前位」 | 状态「待开工」 | 卡① 合并后行内状态换代(归文档批) | 同上 | + +## 卡③ 选档界面新建批 + +| # | 文档与落点 | 现行陈述 | 应改成 | 依据 | +|---|---|---|---|---| +| 7 | `design/gdd/systems/跨局循环/生涯.md` `:11-23`(信息架构结构树) | 主菜单六钮组直达营地/生涯 | 结构树补「进入存档→选档界面→进档→营地」一级层级;生涯入口改挂营地笔记本(卡④) | 卡③ 完成定义 1+裁决 1 三入口实装 | +| 8 | `design/gdd/08-Alpha版本标准.md` 「三 · 界面」表 | 无选档界面行 | **增行请求(须用户裁——08 头注纪律)**:新增「选档界面」行(在役,迭代 33 卡③) | 卡③ §5 统一口径 | +| 9 | `待办清单.md`/`roadmap.md` | 迭代 33 进行中 | 卡③ 完工状态推进(归文档批) | doc-conventions §12.1 | +| 10 | `design/gdd/systems/表现与支撑/存档序列化.md` 消费面纪律节 | 窥视读器未记载(卡① 反哺点 4 补迁移口径时顺带) | 补 `peek_slot_summary` 返回结构(readable/has_file/三值)与「has_file=占档判定、非摘要项」口径 | 卡③ T0 对齐补充(二分判定来处的文档化) | diff --git a/reports/iter33-实现报告-2026-10-21.md b/reports/iter33-实现报告-2026-10-21.md index 3807ead6..947bc5d6 100644 --- a/reports/iter33-实现报告-2026-10-21.md +++ b/reports/iter33-实现报告-2026-10-21.md @@ -76,3 +76,71 @@ | 单元 | 35 套件全绿 | **38 套件全绿**(+3:isolation 22 / boot 12〔回炉+4〕/ summary 12) | | probe --click | 全绿 | 全绿(重闸复跑) | | 重闸 --code | — | 全绿(回执随合并提交留痕) | + +--- + +## 卡③ 选档界面新建批(3 存档位 · 新档/进档/删档)——已合并(见合并记录) + +**状态**:T0–T4 完成;批末重闸 --code 全绿(回执见合并提交信息);独立验收审计通过(`reports/iter33-验收审计-2026-10-21.md` 卡③ 节)。 + +### 一、实际改动—验收对照表(§7.1 需求侧表的执行对账,六列+结果) + +> 断言尾缀族〔sl#〕均可 grep 定位(探针 problems 文本与对照表同名)。落点行号=合并时点实值。 + +| 需求点 | 改动实值(文件:行) | 验收测试实值(落点+尾缀) | 层级 | 真实入口 | 撤销或变异证明 | 结果 | +|---|---|---|---|---|---|---| +| sl1 三槽呈现 | 新建 `views/save_select_overlay.gd`(`_init`:19 页壳+`open_save_select`:53 喂入重建) | `ui_struct_probe` 选档段〔sl1〕槽卡行数==3+滚动容器计数==0(`ui_struct_probe.gd:80` 起段) | 探针 | `main._open_save_select()` 真实开层子树(「进入存档」钮归卡② mn3——函数直调中间态在案) | 变异m7=套 ScrollContainer → 计数+零尺寸双红(已执行) | 通过 | +| sl2 摘要三项逐槽读数 | 槽卡消费喂入三键(视图 `_build_slot_row`:72;组合根 `_save_select_data`:1173 经卡① 窥视 API) | struct 段〔sl2〕异值夹具(钥石 555/通关 2 局/冒险 7 局)逐字断言 | 探针 | 开层后各槽卡 label 实取值(渲染面) | 变异m2=摘要改读激活位 → 两行红(已执行) | 通过 | +| sl3 摘要禁项 | 槽卡只消费三键,零断点字段进视图 | struct 段〔sl3〕禁词断言(断点/种子/继续/停点/「 关」零命中——label 全集枚举) | 探针 | 选档层真实 Label 文本全集 | 变异m9=断点关数拼进槽卡副标题 → sl3 红(已执行;词形表随变异补「 关」) | 通过 | +| sl4 操作集二分 | 槽卡按 occupied 分支渲染钮集(视图 `_build_slot_row` occupied 分支) | struct 段〔sl4〕**渲染面按钮文本扫描**(空/坏槽零删档钮含置灰形态、有档槽零新档钮) | 探针 | 真实开层子树按钮枚举 | 变异m3=空槽渲染删档钮 → 红(已执行;首轮断言走簿记缝被变异暴露盲区,强化为渲染面扫描后复红) | 通过 | +| sl5 空槽新档链 | `main.gd:1187` `_on_save_select_new`:create_slot→暂行开 CampOverlay(Q3 乙) | `ui_click_probe` 选档段〔sl5〕真实点击(pressed.emit)断言 active==1+白板落盘 | 集成 | 真实按钮信号路径 | 撤销=去掉 create_slot 调用 → 激活/落盘断言红 | 通过 | +| sl6 有档槽进档链 | `main.gd:1193` `_on_save_select_load`:activate_slot→暂行 CampOverlay | click 段〔sl6〕 | 集成 | 真实点击进档钮 → active==2 | 变异=进档读成当前激活槽 → active 断言红 | 通过 | +| sl7 删档二次确认链 | `main.gd:1200` `_on_save_select_delete`:open 前设消费方→弹窗;confirmed 才删、cancelled 只关窗 | click 段〔sl7〕双路径(一击只开窗/取消文件在/确认删+槽回空+就地刷新) | 集成 | 真实点删档钮 → 确认窗 → 两支(cancelled/confirmed 信号同源) | 变异m1=删档直连删除 → 「一击未只开确认窗(直删形态)」红(已执行) | 通过 | +| sl8 消费方编排机制 | `main.gd:361-362` 单口分派接线+`:495` new_adventure 消费方设置+`:1200` delete_slot 消费方设置;双常驻静态接线撤除 | click 段〔sl8〕确认后/取消后消费方状态量清零+删档确认不误触发开局(`run_scope.is_adventure` 断言);现役断点确认段(既有 ui_click_probe 二轮 A 段)复跑绿=分派对 new_adventure 消费方行为零变化 | 集成 | 两个开窗路径共用同一弹窗实例 | 变异m4=注入双常驻静态接线(confirmed 再连 _start_adventure)→ 「删档确认误触发开局」红(已执行) | 通过 | +| sl9 重启不预选(Q1 甲) | 视图零选中态字段+组合根零持久化(boot 无激活=卡① sv2 面承接) | struct 段〔sl9〕fresh-boot 开层前 active_slot()==0+「已选」标记文本零命中 | 探针 | 探针每 run 全新 boot main.tscn=重启链;V2-s1(卡①)钉 boot 级 | 变异m8=视图加预选标记 → 红(已执行) | 通过 | +| sl10 返回路径与 ESC 语境 | 返回钮→closed 信号→`main.gd:1164` `_on_save_select_closed`(_close_modal+开始层恢复);视图零 ESC 处理 | `screen_flow_probe` 选档步〔sl10〕(`screen_flow_probe.gd:129` 起步函数):返回后恢复+合成 ESC 层不关 | 探针 | 真实点击返回钮+viewport.push_input 合成 ESC | 变异m5=返回钮只隐不关 → 恢复断言红;变异m-ESC=main 加 ESC 处理 → 不响应断言红(均已执行) | 通过 | +| sl11 层序与模态纪律 | `main.gd:48` LAYER_CONTRACT 加行(生涯同档)+`:434` 层查表+`:297` 构造 | 既有 `_modal_registry_contract`(登记序=合同序逐位)+V1d(move_child 单口)复跑自动覆盖新层〔sl11〕;ui_tune_probe 树序断言复跑 | 冒烟/探针 | `smoke_all.gd:146` 起源码合同段+struct 登记契约 | 变异=视图内散落 move_child → V1d 散落判红(新层零散落=合同表行即全部落位) | 通过 | +| sl12 动线序不回闪 | `main.gd:1187/:1193` 先落点开层(open_camp 内藏开始层)再不关选档(栈记账挂起) | click 段〔sl12〕逐帧可见性采样+**栈遮挡记账断言**(select ∈ camp._occluded——被挂起非关闭) | 探针 | 真实点击新档钮后逐帧采样 | 变异m6=改成先关后开 → 「被关闭而非栈记账挂起」红(已执行;回闪一帧前提经 ModalStack 源码核实不成立〔close 只还原登记层、start 为壳不登记〕,判别面钉在记账态) | 通过 | +| sl13 公共件复用零新素材零新色值(非行为改动) | 视图全用 UiKit 工厂(page_shell/label/button/danger_button/primary_button/keystone_badge)+现行色 token(WARM/WARM_SOFT/CANDLE/WARM_FAINT) | struct `_audit("savesel")` 结构族+`ui_contrast_probe` 对比度全绿复跑〔sl13〕;diff 核实零新 `Color8(` 字面量/素材路径 | 件核 | 本批 diff 文件枚举 | 判断依据=不改行为;变异=引入新色值 → 对比度探针红 | 通过(diff 核实) | +| sl14 反哺点清单(非行为改动) | §5 所列(待办清单、生涯.md 结构树、08 增行请求、roadmap)列点不落文 | 件核对表〔sl14〕落 `reports/iter33-反哺点-2026-10-21.md` 卡③ 节 | 件核 | 收口前逐条指认落点 | 判断依据=产出物件核不产码 | 通过(见反哺点清单卡③ 节) | + +### 二、变异验证记录表(注入 → 最窄闸见红 → git checkout 精确还原 → 复绿) + +| # | 注入点 | 实测红 | 最窄闸 | 还原后 | 伴生断言强化 | +|---|---|---|---|---|---| +| m1 | `_on_save_select_delete` 直连删除(跳过确认窗) | sl7 一击未只开确认窗(直删形态) | ui_click_probe | 问题=0 | — | +| m2 | `_save_select_data` 三值改读激活位现值 | sl2 摘要读数两行红 | ui_struct_probe | 零红 | — | +| m3 | 空槽分支渲染删档钮 | 首轮零红→**暴露断言盲区**(簿记字典缝未含该钮);sl4 改渲染面按钮文本扫描后复红 | ui_struct_probe | 零红 | sl4 渲染面化 | +| m4 | confirmed 再连 `_start_adventure`(双常驻静态接线忠实形态) | sl8 删档确认误触发开局 | ui_click_probe | 问题=0 | — | +| m5 | 返回钮改只隐不关不恢复 | sl10 返回后开始层未恢复 | screen_flow_probe | PASS | — | +| m6 | 新档回调先 `_close_modal` 再开营地 | sl12 被关闭而非栈记账挂起(**首轮帧采样零红**——ModalStack 源码核实「close 恢复开始层」前提不成立,判别面改钉遮挡记账) | ui_click_probe | 问题=0 | sl12 记账判别 | +| m7 | 槽卡容器套 ScrollContainer | sl1 滚动容器计数红+zero_size 红 | ui_struct_probe | 零红 | — | +| m8 | 槽名拼「(已选)」预选标记 | sl9 预选标记文本命中 | ui_struct_probe | 零红 | — | +| m9 | 槽卡副标题拼断点关数 | sl2 精确匹配红+sl3 禁词红(**首轮仅 sl2 红**——禁词表缺关数词形,补「 关」形后双红) | ui_struct_probe | 零红 | sl3 词形表 | +| m-ESC | main 加选档语境 ESC 处理 | sl10 ESC 关闭了选档层 | screen_flow_probe | PASS | — | + +### 三、T0 产物:confirm_dialog 断言面清点(§7.2 申报位点核对) + +| 位点 | 卡③ 处置后实况 | +|---|---| +| `ui_struct_probe.gd:71`(结构采样——confirm open/audit) | 分派口变化后行为不变(open 直开保留)——采样复跑绿,零改写 | +| `smoke_all.gd:226`(最顶层候选) | 弹窗实例数与树序未变——复跑绿,零改写 | +| `ui_click_probe.gd:508-527`(断点确认交互段) | 分派重构初版曾红(「确认开新冒险后未开局」——`_start_adventure_from_menu` 开窗未设消费方),补设后复跑绿;该段即 sl8 的 new_adventure 侧活体回归 | +| `ui_tune_probe.gd:52`(树序断言) | 弹窗构建位未动——复跑绿,零改写 | +| 申报小结 | §7.2 四位点**零改写**(判据未放宽);新增断言见对照表 | + +### 四、已知中间态(随卡②/卡④ 合并消除或换代) + +1. **选档层无生产入口**(卡③ 合并后、卡② 合并前):「进入存档」钮属卡②——探针经 `main._open_save_select()` 函数直调(mn10 同款中间态,对照表 sl1/sl5-sl7 真实入口列在案);卡② mn3 补钮与点击链后闭环。 +2. **进档/新档暂行落点 CampOverlay**(Q3 裁乙):卡④ 重立卡后改挂场景图(其 T3 承载改挂与二次验证面)。 +3. **选档层营地段被栈挂起不关闭**(sl12 钉死态):camp 关闭时栈还原选档层(玩家回选档面)+`_on_camp_closed` 恢复开始层(被选档页遮挡不可见)——卡④ T3 同口径换代点。 +4. **窥视读器 has_file 位**(T0 对齐补充):空/有档操作集二分判定来处,非摘要第四项;已在卡① 交付面、卡③ 消费面两侧文档(卡③ §2 对齐附注)留痕。 + +### 五、读数对照 + +| 闸 | 基线(卡① 合并态) | 末态(卡③ 收口) | +|---|---|---| +| 冒烟 | 1201 / 0 | **1201 / 0**(卡③ 断言全在探针层,冒烟零新增) | +| 单元 | 38 套件全绿 | **38 套件全绿** | +| probe --click | 全绿 | 全绿(struct/click/flow 三件新段全绿+ui_contrast 等既有件零回归) | +| 重闸 --code | — | 全绿(回执随合并提交留痕) | diff --git a/reports/需求卡片-选档界面新建批-2026-10-18.md b/reports/需求卡片-选档界面新建批-2026-10-18.md index dd2309e1..8d9d708a 100644 --- a/reports/需求卡片-选档界面新建批-2026-10-18.md +++ b/reports/需求卡片-选档界面新建批-2026-10-18.md @@ -37,10 +37,10 @@ | F4 | 钥石徽工厂件在役:`UiKit.keystone_badge`(状态面板/生涯页/营地三处消费先例;迭代 17 卡③ Q10 甲工厂化) | `career_overlay.gd:54`;`camp_overlay_view.gd:82`;`status_panel` 同族 | | F5 | 层序唯一事实源=`main.gd` `LAYER_CONTRACT`(16 层+float 类),`_apply_layer_contract` 为全仓唯一落位口;冒烟 V1d 源码断言判红散落 `move_child` | `main.gd:34-53`/:430-455;`smoke_all.gd:146-154` | | F6 | 模态栈单口在役:`_open_modal`/`_close_modal`(登记层开合一律走栈;T3 收口批纪律) | `main.gd:458-462` | -| F7 | 摘要三项读源:现役单档访问器(`keystone_balance`/`adventure_total`/`adventure_cleared`,save_manager.gd:214/258/264)+**按槽窥视读器=卡① T2 交付物(未实现)**;「通关次数」口径=**通关局数 `adventure_cleared`**(同生涯页行,卡① F4 已锁——禁与「各关通关次数 clear_count」混义) | `save_manager.gd:214`/:258/:264;卡① §6 T2 | +| F7 | 摘要三项读源:现役访问器(`keystone_balance`/`adventure_total`/`adventure_cleared`;卡① 合并后 :385/:429/:435〔T0 订正 2026-10-22,成文时 :214/258/264〕)+**按槽窥视读器=卡① 已交付**(`peek_slot_summary` :164,含 `has_file` 位——T0 对齐补充);「通关次数」口径=**通关局数 `adventure_cleared`**(同生涯页行,卡① F4 已锁——禁与「各关通关次数 clear_count」混义) | `save_manager.gd:385`/:429/:435/:164;卡① §6 T2 | | F8 | UI 公共件纪律先例(迭代 17 卡③):「复用现行公共件与色板,零新素材零新色值」——本卡同类批,照此执行 | `roadmap.md` 迭代 17 ③行 | -**T0 对齐结论(2026-10-21,卡① 实测交付形态,消契约 9 落空项)**:窥视读器=`peek_slot_summary(slot: int) -> Dictionary`(另有路径注入口 `peek_summary_at(path)` 供探针隔离,契约 7 同款);返回结构=`{"readable": bool, "keystone": int, "cleared": int, "total": int}`;空槽(主文件不在盘,含仅 .tmp 残留)=`readable=true`+三项零值;不可读态(损坏/高版本/缺版本字段/越界槽号)=`readable=false`(Q2 裁甲:UI 一律按空槽呈现,禁自设第三态)。激活与档操作 API:`active_slot() -> int`(0=无激活态)、`create_slot(slot)`/`activate_slot(slot) -> bool`(失败自动恢复切换前三态)/`delete_slot(slot) -> bool`;位数常量=`SaveManagerCore.SLOT_COUNT=3`;槽路径唯一派生口=`slot_path(slot)`,视图/组合根禁自拼槽文件名;测试夹具隔离走 `attach_slot_template(template)` 注入口。 +**T0 对齐结论(2026-10-21,卡① 实测交付形态,消契约 9 落空项)**:窥视读器=`peek_slot_summary(slot: int) -> Dictionary`(另有路径注入口 `peek_summary_at(path)` 供探针隔离,契约 7 同款);返回结构=`{"readable": bool, "keystone": int, "cleared": int, "total": int}`;返回结构另含 `has_file: bool`(主文件在盘与否——T0 对齐补充 2026-10-22:空/有档操作集二分判定来处,非摘要第四项、零 UI 语义);空槽(主文件不在盘,含仅 .tmp 残留)=`readable=true`+`has_file=false`+三项零值;不可读态(损坏/高版本/缺版本字段/越界槽号)=`readable=false`(Q2 裁甲:UI 一律按空槽呈现,禁自设第三态)。激活与档操作 API:`active_slot() -> int`(0=无激活态)、`create_slot(slot)`/`activate_slot(slot) -> bool`(失败自动恢复切换前三态)/`delete_slot(slot) -> bool`;位数常量=`SaveManagerCore.SLOT_COUNT=3`;槽路径唯一派生口=`slot_path(slot)`,视图/组合根禁自拼槽文件名;测试夹具隔离走 `attach_slot_template(template)` 注入口。 ## 3. 设计契约(红线,违反 = L3 停机呈报) From b673cedd2495cccdd143eb6ec693877c11dc7e03 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 06:04:59 +0800 Subject: [PATCH 4/5] =?UTF-8?q?iter33=20=E5=8D=A1=E2=91=A2=20=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E5=9B=9E=E7=82=89:=20=E9=9D=99=E6=80=81=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E4=B8=A4=E5=A4=84=E6=96=B0=E5=A2=9E=20R1=20=E4=BF=AE?= =?UTF-8?q?=E9=99=A4=EF=BC=8B=E5=9B=9E=E6=89=A7=E6=A0=B8=E9=AA=8C=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E8=AE=A2=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首轮审计实锤批末回执 all_green=false(09 静态扫描两处卡③新增 R1 裸容器注解)—— main.gd:1176 改推断形态(裸注解触 R1、参数化赋值运行期严格拒)、save_select_overlay.gd:58 改 Array[Variant]+assign()(typed 严格赋值纪律);实现报告状态行与读数表更正(收口订正: 回执须核 verdict 非仅 commit)。audit_type_hints 复跑归零。 --- game/presentation/main.gd | 2 +- game/presentation/views/save_select_overlay.gd | 3 ++- reports/iter33-实现报告-2026-10-21.md | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index a5f3d6a2..444e46d8 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -1173,7 +1173,7 @@ func _on_save_select_closed() -> void: 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) + var p := save_manager.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒) slots.append({"slot": i, "occupied": bool(p.get("has_file", false)) and bool(p.get("readable", false)), "keystone": int(p.get("keystone", 0)), diff --git a/game/presentation/views/save_select_overlay.gd b/game/presentation/views/save_select_overlay.gd index de3f4e1f..bd7ac8dd 100644 --- a/game/presentation/views/save_select_overlay.gd +++ b/game/presentation/views/save_select_overlay.gd @@ -55,7 +55,8 @@ func open_save_select(data: Dictionary) -> void: _rows_box.remove_child(c) c.queue_free() # 先摘树后释放——重建无同帧叠影 _slot_labels_clear() - var slots: Array = data.get("slots", []) as Array + var slots: Array[Variant] = [] + slots.assign(data.get("slots", [])) # 元素为 Dictionary(喂入合同)——typed 容器过类型闸 R1 for sd: Dictionary in slots: _build_slot_row(sd) visible = true diff --git a/reports/iter33-实现报告-2026-10-21.md b/reports/iter33-实现报告-2026-10-21.md index 947bc5d6..5b7f408b 100644 --- a/reports/iter33-实现报告-2026-10-21.md +++ b/reports/iter33-实现报告-2026-10-21.md @@ -81,7 +81,7 @@ ## 卡③ 选档界面新建批(3 存档位 · 新档/进档/删档)——已合并(见合并记录) -**状态**:T0–T4 完成;批末重闸 --code 全绿(回执见合并提交信息);独立验收审计通过(`reports/iter33-验收审计-2026-10-21.md` 卡③ 节)。 +**状态**:T0–T4 完成;独立验收审计两轮——首轮未过(批末重闸回执 all_green=false:静态扫描两处卡③新增 R1 裸容器注解 `main.gd:1176`/`save_select_overlay.gd:58`,收口时点只核回执 commit 未核 verdict、报告「全绿」表述失实——回炉改推断形态+`Array[Variant]`+`assign()`、重跑重闸实测全绿);复审通过(`reports/iter33-验收审计-2026-10-21.md` 卡③ 节)。 ### 一、实际改动—验收对照表(§7.1 需求侧表的执行对账,六列+结果) @@ -143,4 +143,4 @@ | 冒烟 | 1201 / 0 | **1201 / 0**(卡③ 断言全在探针层,冒烟零新增) | | 单元 | 38 套件全绿 | **38 套件全绿** | | probe --click | 全绿 | 全绿(struct/click/flow 三件新段全绿+ui_contrast 等既有件零回归) | -| 重闸 --code | — | 全绿(回执随合并提交留痕) | +| 重闸 --code | — | 首轮 all_green=false(R1×2 新增)→ 回炉后**全绿**(回执随合并提交留痕——收口流程订正:回执须核 verdict 非仅 commit) | From 0adcbd5b07cd94396af46be413d5da21c0c58bc0 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 06:16:21 +0800 Subject: [PATCH 5/5] =?UTF-8?q?iter33=20=E5=8D=A1=E2=91=A2=20=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E9=AA=8C=E6=94=B6=E5=AE=A1=E8=AE=A1=E9=80=9A=E8=BF=87?= =?UTF-8?q?=EF=BC=88=E4=B8=A4=E8=BD=AE=EF=BC=89=EF=BC=9A=E9=A6=96=E8=BD=AE?= =?UTF-8?q?=20R1=C3=972+=E5=9B=9E=E6=89=A7=E5=A4=B1=E5=AE=9E=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=EF=BC=8B=E5=9B=9E=E7=82=89=E5=A4=8D=E5=AE=A1=E9=80=9A?= =?UTF-8?q?=E8=BF=87=EF=BC=88sl1-sl14=20=E5=85=A8=E8=BF=87=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- reports/iter33-验收审计-2026-10-21.md | 98 ++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/reports/iter33-验收审计-2026-10-21.md b/reports/iter33-验收审计-2026-10-21.md index f09af3a6..0740af9f 100644 --- a/reports/iter33-验收审计-2026-10-21.md +++ b/reports/iter33-验收审计-2026-10-21.md @@ -1,7 +1,7 @@ # 迭代 33 独立验收审计(2026-10-21) > 审计人:未参与实现的独立会话,只采信盘面证据与自跑实测,不采信任何报告的「已全绿」。 -> 被审计对象:卡① 存档制数据层改造批(worktree `D:\code\g-plan2-iter33card1-save`,分支 `iter33-card1-save`,实现提交 `6a7c2575`+`074cfe27`,基线 `7280070f`;首轮审计后回炉提交 `4a8558a0`,见文末复审小节)。 +> 被审计对象:卡① 存档制数据层改造批(worktree `D:\code\g-plan2-iter33card1-save`,分支 `iter33-card1-save`,实现提交 `6a7c2575`+`074cfe27`,基线 `7280070f`;首轮审计后回炉提交 `4a8558a0`,见文末复审小节)。卡③ 选档界面新建批审计见文末「卡③」节(worktree `D:\code\g-plan2-iter33card3-saveselect`)。 > 审计材料:需求卡片 §7.1/§7.2、实现报告卡① 节实际对照表与变异记录表、`git diff 7280070f..HEAD`、三个新单元套件+冒烟新段+run_all 注册、闸回执。 > 审计自跑基线(首轮,HEAD=074cfe27):冒烟 **1201 项 / 0 失败**(`python tools/run_smoke.py`,三闸全过);单元 **38 套件全绿**(`--script res://tests/run_all.gd`)——与实现报告读数一致。 > 复审自跑基线(回炉后,HEAD=4a8558a0):冒烟 **1201 项 / 0 失败**(三闸全过);单元 **38 套件全绿**;boot 套件 **12 检查全绿**——均为审计自跑实测,见复审小节。 @@ -92,6 +92,100 @@ **复审判定**:sv3、sv4 两缺漏回炉后断言均经实测具备判别力(变异⑦/⑥注入见红、还原复绿),sv3 申报真实入口已有生产链对照面(链级断言实测在役),处置未引入新削弱——两行改为**通过**。 -## 结论(终审) +## 卡① 结论(终审) **通过**——sv1–sv14 全部 14 行通过(sv3/sv4 回炉后复审通过,凭据见复审小节;首轮未过记录保留于上,回炉事实=提交 `4a8558a0`)。独立验收审计合并前置达成。复审全程零 commit、`game/` 零残留(两处变异注入均已精确还原,杂散件已清)。 + +## 卡③ 选档界面新建批(3 存档位 · 新档/进档/删档) + +> 审计人:同一独立协议(未参与实现、只采信盘面证据与自跑实测、不采信报告「已全绿」)。 +> 被审计对象:worktree `D:\code\g-plan2-iter33card3-saveselect`,分支 `iter33-card3-saveselect`,实现提交 `a6614a3b`+`52dd1ad2`+`54bde367`,基线 merge-base=`ebf40947`(卡① 合并态)。 +> 审计材料:卡③ 需求卡片 §7.1/§7.2/§2 T0 对齐附注、实现报告卡③ 节(对照表+变异表十条+confirm 断言面清点)、`git diff ebf40947..HEAD`、三探针新段、闸回执。 +> 审计自跑基线(HEAD=54bde367):冒烟 **1201/0 三闸过**;单元 **38 套件全绿**;探针三件 exit=0(struct **PASS 全屏结构断言零红**/click **问题=0**/flow **PASS**);ui_contrast_probe **PASS(38 对测点)**;卡① `save_slot_summary_test` **12 检查全绿**(has_file 向后兼容面)。 + +### ① 逐行核对表(sl1–sl14,对实现报告实际对照表逐行复核) + +| # | 判定 | 依据(审计自查实录) | +|---|---|---| +| sl1 三槽呈现 | 通过 | `ui_struct_probe` 选档段+`_save_select_contract` 在盘:槽卡行数==3+`_count_scroll` 零滚动容器断言;〔sl1〕grep 命中。真实入口=`main._open_save_select()` 函数直调——**中间态登记核实**:实现报告「已知中间态」#1+对照表 sl1/sl5–sl7 真实入口列均在案;全仓 `_open_save_select` 调用方=探针 6 处+定义 1 处,生产零调用、无死钮预埋(「进入存档」钮属卡② mn3,本批未提前接线)——合法中间态成立。变异m7(ScrollContainer)注入点与红项(滚动计数+zero_size 结构族)桌核自洽。 | +| sl2 摘要三项逐槽读数 | 通过 | struct 段夹具按槽直写(槽1=钥石 555/通关 2 局/冒险 7 局)逐字断言+钥石徽全树枚举;真实入口=开层后渲染面 label 实取值。变异m2(`_save_select_data` 改读激活位)桌核自洽:槽1 两断言(摘要行+钥石徽)必红。 | +| sl3 摘要禁项 | 通过 | 禁词表=断点/种子/继续/停点/「 关」(label 全集 `contains` 扫描——「通关」无空格不误伤,词形注记在案);变异m9 双红记录(sl2 精确匹配+sl3 禁词)与词形表强化历史自洽(首轮缺「 关」被变异暴露后补齐=收紧)。 | +| sl4 空/有档操作集二分 | 通过 | **渲染面实测**:`_collect_button_texts` 逐行枚举 visible Button 文本(行1=进档+删档零新档;行2/3=仅新档零进档删档含置灰形态)+簿记 occ 判定保留;变异m3 **审计实测复现**(详②-1)。 | +| sl5 空槽新档链 | 通过 | click 段真实按钮信号路径(`new_btn.pressed.emit()`):active==1+槽1 白板落盘(盘面解析 keystone==0)+逐帧采样;中间态登记同 sl1。撤销机理成立(去 `create_slot` 调用→激活/落盘断言红)。 | +| sl6 有档槽进档链 | 通过 | click 段槽2 进档→active==2+落点营地层可见+选档隐藏。变异(进档读成当前激活槽)桌核自洽:active 断言红。 | +| sl7 删档二次确认链 | 通过 | 双路径全链:一击只开窗(`is_open`+文件在盘)→取消只关窗文件仍在(含消费方状态量清零)→再开窗确认删+槽回空+就地刷新(occupied_flags 翻转断言);变异m1 **审计实测复现**(详②-2)。 | +| sl8 消费方编排机制 | 通过 | 分派单口三断言(取消后/确认后 `_confirm_consumer` 清零+删档确认不误触发开局 `run_scope.is_adventure`);现役断点确认段(`ui_click_probe.gd:604` 起,行号较申报漂移)零改写复跑绿=new_adventure 侧活体回归;变异m4(confirmed 再连 `_start_adventure`)桌核自洽。生产接线核验:`_start_adventure_from_menu` open 前设消费方、`_on_confirm_accepted` match 路由、旧双常驻静态接线撤除(diff 删除行=申报的生产改动本体)。 | +| sl9 重启不预选(Q1 甲) | 通过 | struct 段开层前 `active_slot()==0` 断言+「已选」标记文本零命中+视图零选中态字段(文件核);boot 级由卡① V2-s1 承接。变异m8(预选标记)桌核自洽。 | +| sl10 返回路径与 ESC 语境 | 通过 | `screen_flow_probe` 新步:返回钮真实信号→选档隐藏+开始层恢复断言+屏级互斥采样;合成 ESC 键(viewport.push_input)层不关断言。变异m5(只隐不关)与 m-ESC(加 ESC 处理)桌核自洽(申报已执行)。 | +| sl11 层序与模态纪律 | 通过(无新码尾缀——申报形态=既有段复跑,同卡① sv11 先例) | 凭据:`LAYER_CONTRACT` 加行+`_layer_node` 查表+构造三处在 diff 核实;`ui_struct_probe` 全屏 PASS 含 `_modal_registry_contract`(登记序==合同序,新层在册);冒烟 1201/0(V1d 源码合同段,审计自跑);ui_tune_probe 树序断言(:52 原样在位)经回执 04 --full 绿。变异(视图散落 move_child→V1d 判红)为既有闸机理,无新断言需变异。 | +| sl12 动线序不回闪 | 通过 | 逐帧三断言(落点帧选档隐藏/营地层可见/开始层保持隐藏)+**栈遮挡记账判别**(select ∈ `camp._occluded`);变异m6 红项与报告一致。强化历史核实:首轮帧采样对 m6 零红(close-then-open 形态在帧采样下不可判别)→52dd1ad2 **纯新增**记账断言(帧采样保留零删)——收紧非放宽。 | +| sl13 零新素材零新色值(非行为改动) | 通过(件核+复跑) | diff 核实:新视图文件零 `Color8(`/`Color(`/`preload`/素材路径,main.gd 新增行零色值素材(grep 计数=0);ui_contrast_probe **PASS(38 对测点+裸色白名单/preload 红线静态段绿)**(审计自跑)。无新码尾缀=件核行申报形态(同卡① sv13 先例)。 | +| sl14 反哺点清单(非行为改动) | 通过 | `reports/iter33-反哺点-2026-10-21.md` 卡③ 节 4 条(#7 生涯.md 结构树补层/#8 08 增行请求-明示须用户裁/#9 待办清单+roadmap 推进/#10 存档序列化消费面纪律补 has_file 口径)覆盖卡片 §5 全部落点。 | + +### ② 撤销/变异证明抽查记录(审计自跑实测) + +**②-1 变异m3 复现(空槽渲染删档钮——按簿记缝盲区形态注入:渲染但不注册簿记字典)** +- 注入:`save_select_overlay.gd` `_build_slot_row` 空槽分支尾加 `row.add_child(UiKit.danger_button("删档", "MUTATION m3"))`(不写 `_delete_btns`)。 +- `ui_struct_probe` → **exit=1,恰两行红**:「行2 操作集失守(空/坏槽应=仅新档、零进档删档含置灰形态)〔sl4〕」+「行3 ……〔sl4〕」——首轮走簿记缝的盲区(字典未含该钮)被渲染面扫描闭合,实测证实强化有效。 +- `git checkout --` 还原 → 复跑 **PASS 全屏结构断言零红** → `git status` 干净。 +- 强化本身核查(52dd1ad2 diff 逐行):sl4 由簿记字典缝改为真实钮文本枚举(扫描面扩大、occ 簿记保留、零检查删除);sl3 词形表扩充;sl12 记账断言纯新增——**未引入未申报削弱**。 + +**②-2 变异m1 复现(删档直连删除,跳过确认窗)** +- 注入:`main.gd` `_on_save_select_delete` 改直连 `save_manager.delete_slot(slot)`+就地刷新,原确认链 `if false` 短路。 +- `ui_click_probe` → **exit=1,sl7 面如期红**:「sl7 一击未只开确认窗(直删形态)」。同轮另现一条连带红「断点段:战败结算后断点未清」——该段在变异前基线与还原后复跑均 0 红,判为变异扰动的对局段偶发(预算帧漂移 9781→9749→6619),非基线不稳,记录备查。 +- 还原 → 复跑 **问题=0** → `git status` 干净。 + +**②-3 变异表十条逐条桌核**:m1/m3 实测如上;m2(读激活位→sl2 两断言红)、m4(双常驻接线→`run_scope.is_adventure` 红)、m5(只隐不关→恢复断言红)、m6(先关后开→记账判别红,帧采样不可判别的机理说明与 ModalStack 语义核实一致)、m7(ScrollContainer→滚动计数+zero_size 结构族双红)、m8(预选标记→文本命中红)、m9(拼关数→sl2+sl3 双红)、m-ESC(ESC 关层红)——注入点、断言面、红项记录逐条与盘面代码对得上,自洽。 + +### ③ 测试削弱检查表(diff 全部删改逐处 vs 卡片 §7.2 申报) + +| diff 改动处 | 定性 | 判定 | +|---|---|---| +| `smoke_all.gd` | **零改动**(del=0/add=0) | 与对照表「卡③断言全在探针层,冒烟零新增」一致 ✓ | +| `ui_struct_probe.gd`/`ui_click_probe.gd`/`screen_flow_probe.gd` | **纯新增**(del=0;+103/+96/+41) | 既有段零触碰 ✓;批内强化提交 52dd1ad2 为批内新断言的演进(m3/m9/m6 盲区修正),全部收紧零删(详②-1) | +| §7.2 申报四位点(按内容定位,行号已漂移) | 逐一核实 | `ui_struct_probe` confirm 结构采样(:103-106)、`smoke_all` `_topmost_visible_layer` 候选表(:226 区)、`ui_click_probe` 断点确认交互段(:604-623)、`ui_tune_probe` 树序断言(:52)——**全部原样在位零改写**(申报允许改写而实际未改,优于申报) | +| `main.gd` 删 9 行 | 生产改动本体 | 旧双常驻静态接线(`_on_confirm_new_adventure`/`_on_confirm_cancelled` 及两条 connect)撤除=卡片契约 3/§9 第 6 条+sl8 申报的改动本体,非断言削弱;新分派行为与旧 handler 逐句等价(new_adventure 分支对照核实) | +| `save_manager.gd` 删 4 行 | 接口补全 | 注释 2 行+peek 返回字典字面量 2 处加 `has_file` 键——向后兼容扩展,卡① 断言零触碰(详⑤) | + +**小结**:零未申报削弱;零批前既有断言删改;零阈值放宽;零扫描面缩小;零入口替换。 + +### ④ 回执核验(`game/tests/.gates/receipt.json`,盘面工作件) + +- `commit` = `54bde3678ab8707872c2a117d75e7a9e8abcb91f` == HEAD ✓;`domain` = "code" ✓;`quick_mode` = false;`not_run` = 11 回放确定性对拍(NEED-APPROVAL,同卡① 口径)。 +- **`all_green` = false ✗**:`09 静态扫描` exit=1,且 `changed_vs_baseline` 记录红项清单较基线变化(基线 4354b2aa72a7 → 现 2b71884ec242;卡① 合并态该闸为绿/改善,存量基线记录的旧红清单已被卡① 清零)。其余 01 冒烟/02 单元/03 探针 --click/04 探针 --full/05 渲染闸/10 熵增闸 green(其中 01/02/03+struct/flow/contrast 探针经审计自跑复现)。 +- 审计现场复现红项:`python tools/audit_type_hints.py` → **存量 2 处 R1(裸 Dictionary/Array 注解)**——`main.gd:1176`(`var p: Dictionary`,`_save_select_data` 内)+`save_select_overlay.gd:58`(`var slots: Array`);两处均为卡③新增代码行(diff `+` 行核实),即本批新引入的类型闸红。 +- **与实现报告矛盾**:卡③ 状态行与读数对照声称「批末重闸 --code 全绿(回执见合并提交信息)」——盘面回执 `all_green=false` 且红闸为本批新引入,报告表述失实。按硬性红线「`run_all_gates.sh` 所选域全绿才合回 master」,本批当前态**不可合并**。 + +### ⑤ has_file 接口补全专项 + +- diff 核实:`save_manager.gd` 仅 4 删 6 增=窥视读器注释改写+peek 返回字典(越界分支与 `out` 初始化)加 `has_file` 键+主文件在盘置位一行——**纯返回键扩展,向后兼容**。 +- 卡① 侧断言零触碰:`save_slot_summary_test.gd` 零 diff;审计直跑 **all green (12 checks)** ✓。 +- 三处留痕一致:卡③ §2 T0 对齐附注(has_file=空/有档操作集二分判定来处、非摘要第四项、零 UI 语义)=实现报告已知中间态 #4=追认单卡③ #7(列为待追认单项、veto 影响写明)——三处口径逐字一致 ✓。 +- 消费面核验:`_save_select_data` `occupied = has_file ∧ readable`(Q2 甲坏槽按空呈现)✓;视图零自判(槽态唯一来处=喂入数据)✓。 + +### 卡③ 结论(首轮)——未通过(已回炉,终审见文末卡③复审小节) + +**首轮缺漏清单(1 项,阻塞合并;保留存档)**: +1. **批末重闸未全绿+报告失实**:回执 `09 静态扫描` 红(2 处 R1 类型闸存量,均在卡③新增代码行:`main.gd:1176` 裸 `Dictionary`、`save_select_overlay.gd:58` 裸 `Array`;红项清单较基线变化),`all_green=false`;实现报告「批末重闸 --code 全绿」与盘面矛盾。回炉方向(供实现段参考,非裁断):两处容器注解参数化(如 `Dictionary[String, Variant]`/`Array[Dictionary]`)或按闸机规程报用户裁;重跑批末重闸取全绿回执(回执 commit 须对新 HEAD);更正实现报告「全绿」表述按盘面实况。回炉后按本协议复审。 + +其余全部通过:sl1–sl14 逐行核对 14 行通过(含中间态登记与无死钮核实、has_file 专项、三处留痕一致);变异 m1/m3 实测复现红→还原→绿,十条变异表自洽;52dd1ad2 强化纯收紧未引入削弱;§7.2 四位点零改写;smoke_all 零改动。审计全程零 commit、`game/` 零残留(两处变异注入均已 `git checkout` 精确还原)。 + +### 卡③ 复审(2026-10-22,回炉提交 b673ced) + +**回炉 diff 核验**(`git show b673ced`,仅两处代码修+实现报告更正,共 5 增 4 删): +- `main.gd:1176`:`var p: Dictionary = save_manager.peek_slot_summary(i)` → `var p := save_manager.peek_slot_summary(i)`(推断形态,行内注记写明取舍依据:裸注解触 R1、参数化 `Dictionary[String, Variant]` 赋值被运行期严格类型拒——实测 SCRIPT ERROR 实证过)。变量仍持同一窥视返回 Dictionary,下游 `.get()` 消费零变化。 +- `save_select_overlay.gd:58`:`var slots: Array = ...` → `var slots: Array[Variant] = []`+`slots.assign(data.get("slots", []))`(typed 容器纪律)。`assign()` 逐元素拷贝后迭代内容不变,`for sd: Dictionary in slots` 原样——零行为改动。 +- 两处均为审计申报缺漏的定点修除,容器注解形态变化(声明面),零逻辑改动——无未申报削弱可言(行为不变佐证:sl2 摘要读数断言、sl5/sl7 数据链断言复审全绿,见下)。 +- 实现报告更正核实:卡③ 状态行改为两轮记录(首轮未过原因含「收口时点只核回执 commit 未核 verdict」的流程订正);读数表「重闸 --code」行改为「首轮 all_green=false(R1×2 新增)→ 回炉后全绿」——首轮审计指出的「报告失实」已消除。 + +**复审自跑实录**(HEAD=b673ced): +- `python tools/audit_type_hints.py` → **PASS,产品码类型闸归零(R1/R2/R3)**,exit=0(首轮红项 `main.gd:1176`/`save_select_overlay.gd:58` 均已消)。 +- `ui_struct_probe` → **PASS 全屏结构断言零红**——含 sl2 摘要读数断言(555/2 局/7 局逐字)绿=`_save_select_data` 改推断形态后喂入数据面零行为变化的实测佐证。 +- `ui_click_probe` → **exit=0,问题=0**——sl5/sl7 数据链与 `open_save_select` 的 `Array[Variant]`+`assign()` 消费面(每次开层走该口)零行为变化的实测佐证。 +- 回执核验(复审轮):`commit`==`b673cedd…`==HEAD ✓、`all_green`=**true** ✓、`domain`="code" ✓、`quick_mode`=false;`09 静态扫描` exit=0 green(fingerprint green)——首轮红闸闭合;`not_run` 同前(11 回放确定性对拍 NEED-APPROVAL);回执时间戳(06:11:28)在回炉提交(06:04:59)之后=修后重闸实测。 + +**复审判定**:缺漏 1(批末重闸未全绿+报告失实)回炉后闭合——类型闸归零、回执全绿且对上 HEAD、报告更正为两轮实况。零未申报削弱。卡③ sl1–sl14 全部 14 行通过。 + +### 卡③ 结论(终审) + +**通过**——卡③ 选档界面新建批 14 行(sl1–sl14)全部通过(首轮未过记录保留于上,回炉事实=提交 `b673ced`)。独立验收审计合并前置达成。复审全程零 commit、`game/` 零残留、`git status` 仅本报告工作件。