From 477a73e763cd5a82abf423e6e530e3606fe1266b Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 24 Oct 2026 08:35:39 +0800 Subject: [PATCH] =?UTF-8?q?iter(36=E5=8D=A1=E2=91=A2):=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E9=9D=A2=E6=9D=BF=E7=98=A6=E8=BA=AB=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=85=AB=E9=94=AE=E6=8A=95=E5=BD=B1=E6=8D=A2=E4=BB=A3=EF=BC=8B?= =?UTF-8?q?=E6=9C=AC=E5=B1=80=E4=BF=A1=E6=81=AF=E9=9D=A2=E6=9D=BF=E4=B8=89?= =?UTF-8?q?=E6=AE=B5=E6=96=B0=E8=A3=85=E4=B8=A4=E6=AE=B5=E9=80=80=E5=BD=B9?= =?UTF-8?q?=EF=BC=8B=E4=B8=A4=E8=A7=92=E6=A0=87tooltip=E6=8D=A2=E4=BB=A3?= =?UTF-8?q?=EF=BC=8B=E6=96=AD=E8=A8=80=E9=9D=A2=E6=8D=A2=E4=BB=A3=EF=BC=88?= =?UTF-8?q?theme=E6=8A=95=E5=BD=B1=E6=97=8F=E4=B9=9D=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E6=89=8B=E6=8B=BC=E5=A4=B9=E5=85=B7=EF=BC=8B=E6=8E=A2=E9=92=88?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E5=85=A5=E5=8F=A3=E4=B8=89=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E5=AF=B9=E6=8B=8D=E7=8B=AC=E7=AB=8Boracle=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E5=8F=98=E5=BC=82=E9=AA=8C=E8=AF=81=E6=8C=89=E7=94=A8=E6=88=B7?= =?UTF-8?q?P0=E7=A6=81=E4=BB=A4=E9=9B=B6=E6=89=A7=E8=A1=8C=EF=BC=88?= =?UTF-8?q?=E6=AD=A3=E5=90=91=E9=AA=8C=E6=94=B6=E6=B5=8B=E8=AF=95=E7=85=A7?= =?UTF-8?q?=E8=B7=91=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/main.gd | 26 +++- game/presentation/views/board_zone_view.gd | 2 +- game/presentation/views/level_map_screen.gd | 2 +- game/presentation/views/status_panel.gd | 119 ++++++------------ game/probe/ui_click_probe.gd | 40 ++++++ .../presentation/theme_projection_test.gd | 103 +++++++-------- 6 files changed, 152 insertions(+), 140 deletions(-) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 2c79f97b..23dad591 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -1068,18 +1068,34 @@ func _on_settings_closed() -> void: # ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- -# 状态面板快照投影(数据只读同源:每次开层实时读 MetaProgression 访问器现值拼装—— +# 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装—— # 快照仅服务单次开层生命周期(open_status 重建各行),跨开层零持有;零写调用。 -# 状态面板零锻造段) +# 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息; +# ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退 +# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链) func _status_data() -> Dictionary: return { "balance": meta.keystone_balance(), - "workshop": meta.treasure_shop_status(), # 秘宝匣 12 件解锁投影 - "tree": meta.tree_camp_rows(), - "fork_groups": meta.tree_fork_groups(), + "hero_hp": int(run_scope.runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命) + "max_hp": run_scope.runner.effective_max_hp(), + "atk": run_scope.runner.effective_atk(), + "ac": run_scope.runner.effective_ac(), + "gp": int(run_scope.runner.gp), + "curios": _status_curios(), + "feats": HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()), } +# 秘宝行映射(inventory_projection 现役投影逐行取 name/used 两键——受理序沿库存投影; +# 禁第二份投影:件名换算已在 inventory_projection 收口,此处只做键裁剪) +func _status_curios() -> Array: + var out: Array = [] + for row in (_treasure_ui.inventory_projection() as Array): + var r: Dictionary = row + out.append({"name": String(r.get("name", "")), "used": bool(r.get("used", false))}) + return out + + func _open_status_panel() -> void: _open_modal(status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) status_panel.open_status(_status_data()) diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index 4c2d6861..68573246 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -55,7 +55,7 @@ func _init() -> void: corner.add_theme_constant_override("separation", 8) status_badge = UiKit.button("状态", 0.0) # 角落小件豁免宽度基准 status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) - status_badge.tooltip_text = "查看钥石与永久强化进度(只读)" + status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.visible = false # 显式初始化——非对局期不误现(可见性归组合根同步) corner.add_child(status_badge) corner_wrap.add_child(corner) diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index a0de2bf1..bd95e641 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -104,7 +104,7 @@ func _init() -> void: # 信号连接归组合根(视图零连接)) status_badge = UiKit.button("状态", 0.0) status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT) - status_badge.tooltip_text = "查看钥石与永久强化进度(只读)" + status_badge.tooltip_text = "查看本局冒险进度(只读)" status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER top.add_child(status_badge) _title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM) diff --git a/game/presentation/views/status_panel.gd b/game/presentation/views/status_panel.gd index 5976d3ae..fbeb8842 100644 --- a/game/presentation/views/status_panel.gd +++ b/game/presentation/views/status_panel.gd @@ -1,14 +1,16 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 局内状态面板·只读:钥石余额与进度概览(入口=两屏角标,本类不含入口)。 +## 局内状态面板·只读:本局信息面板(钥石总量+本局四件:带的秘宝/已获专长/当前属性/现有 GP)。 +## 跨关解锁概览(秘宝匣解锁态/天赋树已购)由营地/生涯承载——本面板不再镜像(迭代 36 卡③)。 ## 账面快照由组合根 open_status 喂入;进出零动效直开直关;构成件全部经 UiKit 工厂产出——禁裸色值自建样式。 class_name StatusPanel extends Control signal closed -var _balance_badge: UiKit.KeystoneBadge # 钥石余额徽(badge 工厂件——Q10 甲) -var _workshop_holder: VBoxContainer -var _tree_label: Label +var _balance_badge: UiKit.KeystoneBadge # 钥石总量徽(badge 工厂件——Q10 甲) +var _attrs_label: Label +var _curios_label: Label +var _feats_label: Label func _init() -> void: @@ -19,11 +21,11 @@ func _init() -> void: var title := UiKit.label("状态",UiKit.FS_TITLE, true, UiKit.INK_TITLE) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) - var hint := UiKit.label("查看本局钥石与永久强化进度(只读——购买请回营地)",UiKit.FS_HINT, false, UiKit.INK_HINT) + var hint := UiKit.label("查看本局冒险进度(只读)",UiKit.FS_HINT, false, UiKit.INK_HINT) hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(hint) - # 钥石余额行(badge 工厂件——与营地顶栏钥石徽同形制) + # 钥石总量行(badge 工厂件——与营地顶栏钥石徽同形制) var bal_row := HBoxContainer.new() bal_row.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(bal_row) @@ -31,13 +33,20 @@ func _init() -> void: bal_row.add_child(_balance_badge) box.add_child(HSeparator.new()) - _section_head(box, "道具工坊 · 解锁态") - _workshop_holder = _section_holder(box) + _section_head(box, "本局状态") + _attrs_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) + _attrs_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + box.add_child(_attrs_label) - _section_head(box, "天赋树 · 已购概览") - _tree_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) - _tree_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - box.add_child(_tree_label) + _section_head(box, "带的秘宝") + _curios_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) + _curios_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + box.add_child(_curios_label) + + _section_head(box, "已获专长") + _feats_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL) + _feats_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + box.add_child(_feats_label) box.add_child(HSeparator.new()) var close_row := HBoxContainer.new() @@ -56,80 +65,26 @@ func _section_head(box: VBoxContainer, text: String) -> void: box.add_child(head) -func _section_holder(box: VBoxContainer) -> VBoxContainer: - var holder := VBoxContainer.new() - holder.add_theme_constant_override("separation", 4) - box.add_child(holder) - return holder - - # ---- 开层(组合根唯一调用口——快照随开随建,R4 单次开层生命周期口径)---- - +# 八键快照(组合根 _status_data 换代):balance/hero_hp/max_hp/atk/ac/gp/curios/feats—— +# 本局四件拼装在此,禁第二份投影(秘宝=inventory_projection 行映射、专长=feat_holdings_lines 现成行) func open_status(data: Dictionary) -> void: _balance_badge.set_text("钥石 %d" % int(data.get("balance", 0))) - _rebuild_workshop(data) - _rebuild_tree(data) - visible = true - - -# 道具工坊六行(解锁态文案与营地柜台同口径——退役/封存/已解锁/未解锁;进度=解锁判定量读数) -# 秘宝匣 12 行只读概览(数据源=treasure_shop_status 投影, -# 解锁态文案与营地柜台同口径;进度列=解锁混合制三形态:钥石价/条件进度拼接/已入手) -func _rebuild_workshop(data: Dictionary) -> void: - for c in _workshop_holder.get_children(): - c.queue_free() - for row in (data.get("workshop", []) as Array): - var line := HBoxContainer.new() - line.add_theme_constant_override("separation", 8) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) - name_l.custom_minimum_size = Vector2(120, 0) - name_l.clip_text = true - name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(name_l) - var unlocked := bool(row.get("unlocked", false)) - var prog_text := "已入手" - if not unlocked: - if String(row.get("mode", "")) == "keystone": - prog_text = "钥石 %d" % int(row.get("price", 0)) - else: - var parts: Array[String] = [] - for req_v in (row.get("reqs", []) as Array): - var req: Dictionary[Variant, Variant] = req_v as Dictionary - var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) - prog_text = "、".join(parts) - var prog_l := UiKit.label(prog_text,UiKit.FS_HINT, false, UiKit.INK_HINT) - prog_l.custom_minimum_size = Vector2(150, 0) - prog_l.clip_text = true - prog_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS - line.add_child(prog_l) - var st_l := UiKit.label("已解锁" if unlocked else "未解锁",UiKit.FS_HINT, true, UiKit.INK_LABEL) - st_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL - line.add_child(st_l) - _workshop_holder.add_child(line) - - -# 天赋树已购概览(单条聚合文案——已购节点等级 + Fork 组激活侧;零已购=空态句) -func _rebuild_tree(data: Dictionary) -> void: - var owned: Array[String] = [] - for row in (data.get("tree", []) as Array): - var lvl := int(row.get("level", 0)) - if lvl > 0: - owned.append("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, int(row.get("cap", 1))]) - for fg in (data.get("fork_groups", []) as Array): - var active := String(fg.get("active_side", "")) - if active == "": - continue - var side_name := active - for sd in (fg.get("sides", []) as Array): - if String(sd.get("side", "")) == active: - side_name = String(sd.get("display_name", active)) - break - owned.append("%s·激活 %s" % [String(fg.get("display_name", "")), side_name]) - if owned.is_empty(): - _tree_label.text = "尚未购买任何节点(天赋树在营地主柜台)" + _attrs_label.text = "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [ + int(data.get("hero_hp", 0)), int(data.get("max_hp", 0)), int(data.get("atk", 0)), + int(data.get("ac", 0)), int(data.get("gp", 0))] + var curios: Array = data.get("curios", []) + if (curios as Array).is_empty(): + _curios_label.text = "未携带秘宝" else: - _tree_label.text = "已购 %d 项:%s" % [owned.size(), " · ".join(owned)] + var parts: Array[String] = [] + for row in curios: + var r: Dictionary = row + parts.append(String(r.get("name", "")) + ("(已用)" if bool(r.get("used", false)) else "")) + _curios_label.text = " · ".join(parts) + var feats: Array = data.get("feats", []) + _feats_label.text = "未获专长" if (feats as Array).is_empty() else " · ".join(feats) + visible = true func _emit_closed() -> void: diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 36af9112..5ff14b2a 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -762,6 +762,46 @@ func _run_modal_probes_once() -> void: var bal_txt: String = main.status_panel._balance_badge.value_label.text if not bal_txt.begins_with("钥石 "): _problems.append("状态面板余额徽投影缺失(%s)" % bal_txt) + # ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)---- + # ① 面板子树零旧段文案(跨关概览退役零残留) + var sv_labels: Array = [] + _collect_labels_by_text(main.status_panel, sv_labels) + for l in sv_labels: + if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"): + _problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text)) + # ② 属性行全值 = 契约4 格式 % runner 五现值(main 侧投影值层真链路) + var rnr = main.run_scope.runner + if rnr == null: + _problems.append("状态面板开层时 runner 空指针(契约5——状态机缺陷,禁软回退)") + else: + var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [ + int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(), + rnr.effective_ac(), int(rnr.gp)] + if String(main.status_panel._attrs_label.text) != attr_expect: + _problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text]) + # ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle) + var sd: Dictionary = main._status_data() + var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"] + var keys_ok: bool = (sd as Dictionary).size() == 8 + for k in keys: + keys_ok = keys_ok and (sd as Dictionary).has(k) + if not keys_ok: + _problems.append("_status_data() 键集≠八键(sv7——实测 %s)" % str((sd as Dictionary).keys())) + var oracle: Array = [] + for rec in rnr.treasure_inventory.query_inventory(Callable()): + var rr: Dictionary = rec + oracle.append({"name": String(main.db.treasure_item_row(String(rr.get("item_id", ""))).get("display_name", "")), + "used": bool(rr.get("used", false))}) + var sd_curios: Array = (sd as Dictionary).get("curios", []) + var curios_ok: bool = (sd_curios as Array).size() == (oracle as Array).size() + if curios_ok: + for i in (oracle as Array).size(): + curios_ok = curios_ok and String((sd_curios[i] as Dictionary).get("name", "")) == String((oracle[i] as Dictionary).get("name", "")) and bool((sd_curios[i] as Dictionary).get("used", false)) == bool((oracle[i] as Dictionary).get("used", false)) + if not curios_ok: + _problems.append("_status_data().curios 对拍独立 oracle 失守(sv7——实测 %s vs %s)" % [str(sd_curios), str(oracle)]) + var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held()) + if str((sd as Dictionary).get("feats", [])) != str(feats_expect): + _problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)") var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix") if resume_btn == null: _problems.append("状态面板关闭钮缺失(「继续」)") diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 9f00b3df..dd71c5b6 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -746,63 +746,64 @@ func _smoke_diag_button_retired() -> void: dbg_src.contains('UiKit.button("转储")')) -# ---- 状态面板只读投影一致性(R4 数据只读同源:状态面板展示值逐项 == MetaProgression -# 访问器读数;投影拼装与 main._status_data 同构(同访问器族同序)。只读性连接巡检与零动效 -# 断言归 ui_struct_probe 呈现侧,本段只管数据面)---- +# ---- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具:展示值逐项 == +# 快照钉值;投影拼装口径=组合根 _status_data 八键(数据只读同源)。只读性连接巡检与零动效 +# 断言归 ui_struct_probe 呈现侧,本段只管数据面;跨关解锁概览断言族随面板换代退役—— +# 替换判据=sv5 零旧段+八键投影断言(实现报告 §8.3 申报))---- func _status_panel_projection() -> void: - print("--- 状态面板·只读投影一致性(隔离档 smoke_meta_buy 复用)---") - _meta_smoke_cleanup() - var sv = _meta_new_save(_META_SMOKE_PATHS[0]) - var mp = load("res://core/meta_progression.gd").new(_db, sv) - # 已购态夹具:余额非零 + 秘宝匣首件解锁——投影可辨非全零白板 - sv.add_keystone(37) - sv.unlock_item("morning_order") - sv.write_to_disk() - # 投影拼装(main._status_data 同构)+ 视图喂入 - var data := {"balance": mp.keystone_balance(), "workshop": mp.treasure_shop_status(), - "tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()} + print("--- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具,同构口径不依赖 runner)---") + _meta_smoke_cleanup() # 套件级夹具清场(生涯页 fixture 依赖此前置——旧状态段同款保留) + # 夹具(代拍板 4——任意非零可辨值,禁读 data/tables 现值、禁与属性行断言共值致误判掩盖) + var data := {"balance": 77, "hero_hp": 13, "max_hp": 29, "atk": 7, "ac": 11, "gp": 41, + "curios": [{"name": "青苔密卷", "used": false}, {"name": "锈蚀怀表", "used": true}], + "feats": ["疼痛记忆 ×2", "双管镐 ×1"]} var po = load("res://presentation/views/status_panel.gd").new() po.open_status(data) Kit.check("状态面板开层可见(open_status 直开——P4 甲零动效路径)", po.visible) - Kit.check("余额徽投影 = keystone_balance() 读数(%s)" % po._balance_badge.value_label.text, - String(po._balance_badge.value_label.text) == "钥石 %d" % mp.keystone_balance()) - var ws: Array = mp.treasure_shop_status() - var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size() - var uikit_s = load("res://presentation/views/ui_kit.gd") - for i in mini(ws.size(), po._workshop_holder.get_child_count()): - var row: Dictionary = ws[i] - var lbls2: Array = [] - for ch in po._workshop_holder.get_child(i).get_children(): - if ch is Label: - lbls2.append(ch) - var unlocked: bool = bool(row.get("unlocked", false)) - var expect_prog := "已入手" - if not unlocked: - if String(row.get("mode", "")) == "keystone": - expect_prog = "钥石 %d" % int(row.get("price", 0)) - elif String(row.get("mode", "")) == "condition": - var parts: Array[String] = [] - for req_v2 in (row.get("reqs", []) as Array): - var req2: Dictionary = req_v2 as Dictionary - var lbl2 := String(uikit_s.TREASURE_REQ_LABELS.get(String(req2.get("key", "")), "条件")) - parts.append("%s %d/%d" % [lbl2, int(req2.get("progress", 0)), int(req2.get("threshold", 0))]) - expect_prog = "、".join(parts) - var expect_st2 := "已解锁" if unlocked else "未解锁" - ws_ok = ws_ok and lbls2.size() == 3 \ - and String(lbls2[0].text) == String(row.get("display_name", "")) \ - and String(lbls2[1].text) == expect_prog \ - and String(lbls2[2].text) == expect_st2 - Kit.check("秘宝匣 12 行投影 = treasure_shop_status() 逐项(名称/钥石价与条件进度/解锁态——首件解锁夹具在列)〔秘宝批 V19 呈现数据源〕", - ws_ok) - var owned := 0 - for trow in (mp.tree_camp_rows() as Array): - if int(trow.get("level", 0)) > 0: - owned += 1 - var tree_txt := String(po._tree_label.text) - Kit.check("天赋树概览聚合文案与已购节点数一致(零已购=空态句)", - tree_txt == "尚未购买任何节点(天赋树在营地主柜台)" or tree_txt.begins_with("已购 %d 项:" % owned)) + Kit.check("余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕", + String(po._balance_badge.value_label.text) == "钥石 77") + Kit.check("属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕", + String(po._attrs_label.text) == "生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41") + Kit.check("秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕", + String(po._curios_label.text) == "青苔密卷 · 锈蚀怀表(已用)") + Kit.check("专长行投影 = 行串 join〔sv3〕", + String(po._feats_label.text) == "疼痛记忆 ×2 · 双管镐 ×1") + # 零旧段(sv5——跨关概览退役零残留:面板子树零旧文案+旧成员退役) + var stale := "" + for ch in _all_labels(po): + if String(ch.text).contains("道具工坊") or String(ch.text).contains("天赋树"): + stale += String(ch.text) + "|" + Kit.check("面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕", stale == "") + Kit.check("旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕", + po.get("_workshop_holder") == null and po.get("_tree_label") == null) + # 空态句×2(curios/feats 空夹具各跑一遍——契约4 空态呈现) + var data_empty := {"balance": 3, "hero_hp": 5, "max_hp": 6, "atk": 2, "ac": 9, "gp": 1, + "curios": [], "feats": []} + var po2 = load("res://presentation/views/status_panel.gd").new() + po2.open_status(data_empty) + Kit.check("空秘宝态句=「未携带秘宝」〔sv2〕", String(po2._curios_label.text) == "未携带秘宝") + Kit.check("空专长态句=「未获专长」〔sv3〕", String(po2._feats_label.text) == "未获专长") + # 提示行文案(sv8——新定位换代;旧「永久强化进度」半句随两段退役失效) + var hint_txt := "" + for ch in _all_labels(po): + if String(ch.text).begins_with("查看本局"): + hint_txt = String(ch.text) + Kit.check("提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕", + hint_txt == "查看本局冒险进度(只读)") po.visible = false po.free() + po2.visible = false + po2.free() + + +# 面板子树 Label 收集(sv5 零旧段扫描辅助——递归全子树) +func _all_labels(root: Node) -> Array: + var out: Array = [] + if root is Label: + out.append(root) + for child in root.get_children(): + out.append_array(_all_labels(child)) + return out # ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/