④T3: 生涯页实装——career_overlay.gd 新视图(page_shell 暖亮全屏页·商店同壳:56px 通栏顶条标题「生 涯」+紧凑返回钮;汇总段六类九项逐行〔组一·冒险战绩=总局数/通关局数/各关通关次数/Boss 首杀/Boss 总击杀,组二·资源与成长=钥石余额徽/属性锻造合计/道具工坊解锁计数/天赋树已购聚合〕;只读契约=快照随开随建零核心访问零写通路唯一信号 closed;零跳转〔页内零「前往他处」入口〕零动效〔无 tween 无 AnimationPlayer〕;全部构成件经 UiKit 工厂零新色值)+主菜单入口钮(ui_assembler 营地与系统组营地钮后·普通钮同设置行形制+tooltip·返回句柄 careerbtn)+组合根接线(main.gd:成员声明+暂停层后 addChild〔树序同步=P3 甲登记序营地同档、确认弹窗之前〕+登记册插位 career_overlay+登记序注释换代+start.careerbtn→_open_career+开合仿设置面先例〔开层互斥开始层/关闭恢复+继续钮重刷〕+_career_data 快照投影〔SaveManager 计数访问器+MetaProgression 访问器同源·display_name 表取数〕)+冒烟投影断言族 11 件(_career_overlay_projection:六类九项逐行==访问器读数+开层可见+v5 现役档升 v6 载入后页正常显示〔完成定义 6〕;类型显式 int 修正两处 Variant 推断)——冒烟 834/0(T2 后 823+11)+编辑器扫描 class_name 缓存与 .uid 边车随批

This commit is contained in:
DKChan committed 2026-10-07 08:51:35 +08:00
1 parent 62d433c15b
commit e5cd9bd5d4
5 files changed
+299 -6

No files matched your search

+55 -3
View File
@@ -120,6 +120,7 @@ var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批
var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效)
var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面)
var pause_overlay: PauseOverlay # 局内暂停层·只读(迭代 17 卡③——钥石余额+营地三柜台进度查看;登记设置层同档)
var career_overlay: CareerOverlay # 生涯页·只读全屏页(迭代 18 卡④——跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕)
var debug_overlay: DebugOverlay # 调试浮窗(迭代 14.5 调试整合批——种子/数值调整/批量模拟/两层调试入口收一)
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
@@ -248,7 +249,7 @@ func _build_ui() -> void:
logger.ap_per_round = int(db.action_economy.ap_per_round)
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
# < 商店 < 营地 < 设置 < 暂停(只读) < 确认弹窗 < 调试浮窗)
# < 商店 < 营地 < 设置 < 暂停(只读) < 生涯页(只读·迭代 18 卡④) < 确认弹窗 < 调试浮窗)
var overlays: Dictionary = UiAssembler.build_overlays(self, db)
legend_overlay = overlays.legend_overlay
node_overlay = overlays.node_overlay
@@ -275,6 +276,12 @@ func _build_ui() -> void:
pause_overlay = PauseOverlay.new()
add_child(pause_overlay)
pause_overlay.closed.connect(_on_pause_closed)
# 生涯页·只读全屏页(迭代 18 卡④——P3 甲:登记序=营地同档,设置层旁、确认弹窗之前;
# 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数
# 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮)
career_overlay = CareerOverlay.new()
add_child(career_overlay)
career_overlay.closed.connect(_on_career_closed)
# 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进)
curio_overlay = CurioPickOverlay.new()
add_child(curio_overlay)
@@ -312,6 +319,7 @@ func _build_ui() -> void:
camp_overlay.tree_fork_toggled.connect(_camp_pres.on_camp_fork_toggle)
camp_overlay.closed.connect(_camp_pres.on_camp_closed)
start.campbtn.pressed.connect(_camp_pres.open_camp)
start.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组)
start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口
start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单
@@ -344,9 +352,9 @@ func _build_ui() -> void:
debug_overlay.debug_levels_requested.connect(_start_debug_levels)
# 模态登记(缺陷⑤纪律——登记序 = 树序:含 move_child 调位后的关卡画面/三选一终位;
# 暂停层设置层同档、确认弹窗之前——P2 甲)
# 暂停层设置层同档、确认弹窗之前——P2 甲;生涯页同档随暂停层之后(迭代 18 卡④ P3 甲))
_modals = ModalStack.new()
for m: Control in [legend_overlay, node_overlay, level_screen, curio_overlay, _end_overlay, shop_overlay, camp_overlay, settings_overlay, pause_overlay, confirm_dialog, debug_overlay]:
for m: Control in [legend_overlay, node_overlay, level_screen, curio_overlay, _end_overlay, shop_overlay, camp_overlay, settings_overlay, pause_overlay, career_overlay, confirm_dialog, debug_overlay]:
_register_modal(m)
_tune_sim = TuneSimController.new(db, debug_overlay, int(db.hero.max_hp))
@@ -990,6 +998,50 @@ func _on_pause_closed() -> void:
_close_modal(pause_overlay) # 模态栈单口
# ---- 生涯页·只读(迭代 18 卡④——入口=开始层生涯钮;开合仿设置面先例:开始层互斥;
# 数据快照随开随建零写通路,页内零跳转)----
func _open_career() -> void:
_start_overlay.visible = false
_open_modal(career_overlay) # 模态栈单口
career_overlay.open_career(_career_data())
func _on_career_closed() -> void:
_close_modal(career_overlay) # 模态栈单口
_start_overlay.visible = true
_refresh_resume_button()
# 生涯页快照投影(只读同源——每次开层实时读 SaveManager 计数访问器 + MetaProgression 访问器
# 现值拼装(暂停层 _pause_data 同构);快照仅服务单次开层生命周期,跨开层零持有;零写调用)
func _career_data() -> Dictionary:
var clears: Array[Dictionary] = []
var boss_first := 0
for i in db.level_count():
var li: int = i + 1
clears.append({"display_name": String(db.level_row(li).get("display_name", "")),
"count": save_manager.clear_count(li)})
if save_manager.boss_first_kill_marked(li):
boss_first += 1
var forge_total := 0
for attr in MetaTable.FORGE_ATTRS:
forge_total += meta.forge_level(attr)
return {
"total": save_manager.adventure_total(),
"cleared": save_manager.adventure_cleared(),
"clears": clears,
"boss_first_kills": boss_first,
"boss_first_total": db.level_count(),
"boss_kills": save_manager.boss_kill_count(),
"keystone": meta.keystone_balance(),
"forge_levels_total": forge_total,
"workshop": meta.workshop_status(),
"tree": meta.tree_camp_rows(),
"fork_groups": meta.tree_fork_groups(),
}
# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,
# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding)
func _on_pause_badge_pressed() -> void:
+7 -2
View File
@@ -92,7 +92,7 @@ static func build_overlays(host: Control, db) -> Dictionary:
}
# 开始覆盖层(返回 {overlay, box, lvbtn, resumebtn, campbtn, settingsbtn, quitbtn}——按钮接线归组合根;
# 开始覆盖层(返回 {overlay, box, lvbtn, resumebtn, campbtn, careerbtn, settingsbtn, quitbtn}——按钮接线归组合根;
# 迭代 14.5 调试整合批:随机种子/数值调整板/两层调试三散件撤出主按钮组,收进调试浮窗——
# 迭代 18 卡⑦ P2 甲:右下角「调试」角标亦退役(入口迁宿主=战斗屏 HUD+关卡地图屏两枚角标,
# DS_DEBUG=1 会话浮窗启动即开——本层零调试入口)
@@ -150,6 +150,10 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
box.add_child(UiKit.label("── 营地与系统 ──",UiKit.FS_CAPTION, false, UiKit.WARM_FAINT))
var campbtn := UiKit.verb_button("营地", UiKit.TEX_ICON_KEYSTONE, "永久强化:钥石换永久属性——跨局成长账本") # 元进度实现批——主菜单营地入口
box.add_child(campbtn)
# 生涯页一级入口(迭代 18 卡④——与营地平级;页=局外只读跨局汇总,零跳转零购买)
var careerbtn := UiKit.button("生涯")
careerbtn.tooltip_text = "跨局生涯汇总:总局数 / 通关局数 / Boss 击杀 / 钥石余额 / 三柜台解锁概览(只读)"
box.add_child(careerbtn)
var settingsbtn := UiKit.button("设置") # 迭代 8 卡④——最小设置面入口
settingsbtn.tooltip_text = "音量 / 全屏 / 减动效"
box.add_child(settingsbtn)
@@ -196,7 +200,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
return {"overlay": overlay, "box": box,
"lvbtn": lvbtn, "resumebtn": resumebtn,
"campbtn": campbtn, "settingsbtn": settingsbtn, "quitbtn": quitbtn}
"campbtn": campbtn, "careerbtn": careerbtn,
"settingsbtn": settingsbtn, "quitbtn": quitbtn}
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
+138
View File
@@ -0,0 +1,138 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 生涯覆盖层·只读全屏页(迭代 18 卡④ 生涯界面实装批·期一 2026-10-07——任务书 v1.2 裁点全甲;
# 设计权威 design/gdd/systems/跨局循环/生涯.md v0.1 期一范围)。
# 裁决落形:P1/P2 甲=汇总段六类九项(冒险总局数/通关局数/各关通关次数/Boss 首杀/Boss 总击杀/
# 钥石余额/属性锻造/道具工坊/天赋树——逐行只读);P3 甲=登记 modal_stack 营地同档(设置层旁、
# 确认弹窗之前——登记序=树序=z 序,组合根 addChild 同位);P4 甲=零跳转(页内零「前往他处」
# 入口,仅只读展示+关闭)+局内不放生涯入口(开始覆盖层「营地与系统」组唯一入口)。
# 期二/期三内容(历史回看/图鉴/单局明细)零触碰——页内无任何期二占位件。
# 只读契约(任务书契约 3/4):数据快照由组合根 open_career 喂入(每次开层实时读 SaveManager
# 计数访问器 + MetaProgression 访问器重建——暂停层 R4 同构,快照仅单次开层生命周期,跨开层
# 零持有);本类零核心访问、零写通路——唯一信号 closed(关闭意图,组合根经模态栈单口收口)。
# 零动效(暂停层 P4 甲先例同款):直开直关,无 tween 无 AnimationPlayer。
# R1 公共件单点:全部构成件经 UiKit 工厂产出(page_shell/label/button/keystone_badge),
# 零新素材零新色值零新字体零自建样式。页壳=page_shell 暖亮底(商店同壳——主菜单页族形态)。
class_name CareerOverlay
extends Control
signal closed
var _total_label: Label # 冒险总局数(开局即计含放弃与失败)
var _cleared_label: Label # 通关局数(三关全通才计 1)
var _clears_label: Label # 各关通关次数(一行三关——display_name ×次数)
var _boss_first_label: Label # Boss 首杀(已首杀关数/总关数)
var _boss_kills_label: Label # Boss 总击杀(含重复)
var _keystone_badge: UiKit.KeystoneBadge # 钥石余额(badge 工厂件——暂停层同形制)
var _forge_label: Label # 属性锻造(已购等级合计)
var _workshop_label: Label # 道具工坊(已解锁件数/总件数)
var _tree_label: Label # 天赋树已购概览(聚合单条——暂停层同构)
func _init() -> void:
# 全屏页壳(T6/T7 对齐批先例——暖亮底+石纹弱质感+暗角+56px 通栏顶条+内容列)
var page: Dictionary = 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
var hint := UiKit.label("跨局生涯汇总(只读——购买与装配在营地;历史冒险回看随后续批次)",
UiKit.FS_HINT, false, UiKit.WARM_SOFT)
box.add_child(hint)
# 组一·冒险战绩(终身计数域——存档 v6:总局数开局即计、通关局数三关全通才计)
_section_head(box, "冒险战绩")
_total_label = _row(box, "冒险总局数")
_cleared_label = _row(box, "通关局数")
_clears_label = _row(box, "各关通关次数")
_boss_first_label = _row(box, "Boss 首杀")
_boss_kills_label = _row(box, "Boss 总击杀")
# 组二·资源与成长(钥石/三柜台解锁概览——现役只读投影)
_section_head(box, "资源与成长")
var bal_row := HBoxContainer.new()
bal_row.add_theme_constant_override("separation", 8)
box.add_child(bal_row)
var bal_name := UiKit.label("钥石余额",UiKit.FS_BODY, false, UiKit.INK_LABEL)
bal_row.add_child(bal_name)
_keystone_badge = UiKit.keystone_badge("钥石 0")
bal_row.add_child(_keystone_badge)
_forge_label = _row(box, "属性锻造")
_workshop_label = _row(box, "道具工坊")
_tree_label = _row(box, "天赋树")
_tree_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
visible = false # 显式初始化——不靠树序巧合(缺陷⑤家族纪律)
# ---- 开层(组合根唯一调用口——快照随开随建,只读同源口径同暂停层 R4)----
func open_career(data: Dictionary) -> void:
_total_label.text = "%d 局" % int(data.get("total", 0))
_cleared_label.text = "%d 局(三关全通)" % int(data.get("cleared", 0))
var parts: Array[String] = []
for row in (data.get("clears", []) as Array):
parts.append("%s ×%d" % [String(row.get("display_name", "")), int(row.get("count", 0))])
_clears_label.text = " · ".join(parts) if not parts.is_empty() else "—"
_boss_first_label.text = "%d/%d 关" % [int(data.get("boss_first_kills", 0)), int(data.get("boss_first_total", 0))]
_boss_kills_label.text = "%d 次" % int(data.get("boss_kills", 0))
_keystone_badge.set_text("钥石 %d" % int(data.get("keystone", 0)))
_forge_label.text = "已购 %d 级(三属性合计)" % int(data.get("forge_levels_total", 0))
var ws: Array = data.get("workshop", []) as Array
var unlocked := 0
for row in ws:
if bool(row.get("unlocked", false)):
unlocked += 1
_workshop_label.text = "已解锁 %d/%d 件" % [unlocked, ws.size()] if not ws.is_empty() else "—"
_tree_label.text = _tree_summary(data)
visible = true
# 天赋树聚合文案(暂停层 _rebuild_tree 同构——已购节点等级+Fork 激活侧;零已购=空态句)
func _tree_summary(data: Dictionary) -> String:
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():
return "尚未购买任何节点"
return "已购 %d 项:%s" % [owned.size(), " · ".join(owned)]
func _emit_closed() -> void:
closed.emit()
# ---- 行构件(名称列 + 数值列——暂停层行族同构;数值 Label 返回供探针/冒烟投影断言)----
func _section_head(box: VBoxContainer, text: String) -> void:
box.add_child(UiKit.label(text,UiKit.FS_BODY, true, UiKit.INK_LABEL))
func _row(box: VBoxContainer, name_text: String) -> Label:
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 8)
box.add_child(line)
var name_l := UiKit.label(name_text,UiKit.FS_BODY, false, UiKit.INK_LABEL)
name_l.custom_minimum_size = Vector2(150, 0)
line.add_child(name_l)
var val_l := UiKit.label("—",UiKit.FS_BODY, true, UiKit.INK_TITLE)
line.add_child(val_l)
return val_l
@@ -0,0 +1 @@
uid://bt66vryciqn8v
+98 -1
View File
@@ -4255,6 +4255,7 @@ func _smoke_meta() -> void:
_meta_settle_and_workshop()
_meta_injection()
_pause_overlay_projection()
_career_overlay_projection()
# ---- 暂停层只读投影一致性(迭代 17 卡③——R4 数据只读同源:暂停层展示值逐项 == MetaProgression
@@ -4332,12 +4333,108 @@ func _pause_overlay_projection() -> void:
po.free()
# ---- 生涯页只读投影一致性(迭代 18 卡④——汇总段六类九项逐行 == SaveManager 计数访问器/
# MetaProgression 访问器读数;投影拼装与 main._career_data 同构(同访问器族同序)。
# 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示
# (v5 样档升 v6——完成定义 6「计数域缺省补零、页正常显示」))----
func _career_overlay_projection() -> void:
print("--- 生涯页·只读投影一致性(隔离档 smoke_meta_career/career_v5)---")
var sv = _meta_new_save(_META_SMOKE_PATHS[9])
var mp = load("res://core/meta_progression.gd").new(_db, sv)
# 已玩态夹具:两局收束(一通一止)+ 钥石/锻造/工坊首件/树首节点非零——投影可辨非全零白板
sv.add_keystone(45)
sv.set_forge_level("atk", 2)
sv.set_forge_level("max_hp", 1)
var ws0: Array = _db.meta_workshop_items()
if not ws0.is_empty():
sv.unlock_item(String(ws0[0].get("item_id", "")))
sv.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 21})
sv.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 22})
var tree_rows: Array = mp.tree_camp_rows()
var tree_node := ""
if not tree_rows.is_empty():
tree_node = String(tree_rows[0].get("node_id", ""))
sv.set_tree_level(tree_node, 1)
sv.write_to_disk()
# 投影拼装(main._career_data 同构)+ 视图喂入
var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance(), "forge_levels_total": 0,
"workshop": mp.workshop_status(), "tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()}
for i in _db.level_count():
var li: int = i + 1
data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")),
"count": sv.clear_count(li)})
if sv.boss_first_kill_marked(li):
data["boss_first_kills"] += 1
for attr in MetaTable.FORGE_ATTRS:
data["forge_levels_total"] += mp.forge_level(attr)
var co = load("res://presentation/views/career_overlay.gd").new()
co.open_career(data)
check(co.visible, "生涯页开层可见(open_career 直开——零动效路径)")
check(String(co._total_label.text) == "2 局",
"总局数投影 = adventure_total() 读数(%s)" % co._total_label.text)
check(String(co._cleared_label.text) == "1 局(三关全通)",
"通关局数投影 = adventure_cleared() 读数(%s)" % co._cleared_label.text)
var expect_clears: Array[String] = []
for row in (data["clears"] as Array):
expect_clears.append("%s ×%d" % [String(row.get("display_name", "")), int(row.get("count", 0))])
check(String(co._clears_label.text) == " · ".join(expect_clears),
"各关通关次数投影 = clear_count() 逐关读数(%s)" % co._clears_label.text)
check(String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次",
"Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" % [co._boss_first_label.text, co._boss_kills_label.text])
check(String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance(),
"钥石余额徽投影 = keystone_balance() 读数(%s)" % co._keystone_badge.value_label.text)
check(String(co._forge_label.text) == "已购 %d 级(三属性合计)" % data["forge_levels_total"],
"属性锻造投影 = forge_level() 三属性合计(%s)" % co._forge_label.text)
var ws_unlocked := 0
for row in (mp.workshop_status() as Array):
if bool(row.get("unlocked", false)):
ws_unlocked += 1
check(String(co._workshop_label.text) == "已解锁 %d/%d 件" % [ws_unlocked, (mp.workshop_status() as Array).size()],
"道具工坊投影 = workshop_status() 解锁计数(%s——首件解锁夹具在列)" % co._workshop_label.text)
var tree_expect := "尚未购买任何节点"
if tree_node != "":
var trow: Dictionary = tree_rows[0]
tree_expect = "已购 1 项:%s Lv1/%d" % [String(trow.get("display_name", "")), int(trow.get("cap", 1))]
check(String(co._tree_label.text) == tree_expect,
"天赋树概览投影 = tree_camp_rows() 已购聚合(%s)" % co._tree_label.text)
co.visible = false
co.free()
# 旧档载入显示(完成定义 6):v5 现役档形态(计数域两键不存在)升 v6 → 生涯页正常呈现缺省值
var v5 = load("res://core/save_manager.gd").new(_META_SMOKE_PATHS[10])
var fv5c := FileAccess.open(_META_SMOKE_PATHS[10], FileAccess.WRITE)
fv5c.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 3}}))
fv5c.close()
check(v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0,
"v5 现役档升 v6 载入成功(计数域缺省补零)")
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
var data_v5 := {"total": v5.adventure_total(), "cleared": v5.adventure_cleared(),
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
"boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance(), "forge_levels_total": 0,
"workshop": mp_v5.workshop_status(), "tree": mp_v5.tree_camp_rows(), "fork_groups": mp_v5.tree_fork_groups()}
for i in _db.level_count():
data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")),
"count": v5.clear_count(i + 1)})
var co_v5 = load("res://presentation/views/career_overlay.gd").new()
co_v5.open_career(data_v5)
check(co_v5.visible and String(co_v5._total_label.text) == "0 局" \
and String(co_v5._cleared_label.text) == "0 局(三关全通)" \
and String(co_v5._keystone_badge.value_label.text) == "钥石 3",
"旧档载入后生涯页正常显示(v5 升 v6:总局数/通关局数显示 0,既有钥石余额原样呈现)")
co_v5.visible = false
co_v5.free()
# 冒烟隔离存档名册(段首清陈旧 + 段尾清理——契约:测试不污染 user://save.json 真实档)
const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json",
"user://smoke_meta_settle.json", "user://smoke_meta_workshop.json",
"user://smoke_meta_cap.json", "user://smoke_meta_white.json",
"user://smoke_meta_attune.json", "user://smoke_meta_attune2.json",
"user://smoke_meta_tree.json"]
"user://smoke_meta_tree.json", "user://smoke_meta_career.json",
"user://smoke_meta_career_v5.json"]
func _meta_smoke_cleanup() -> void: