From 669c1c7d01bd0333ca5dd420f77e8a12505f7ec6 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 7 Oct 2026 08:35:47 +0800 Subject: [PATCH] =?UTF-8?q?=E2=91=A3T1:=20=E5=AD=98=E6=A1=A3=20v6=20?= =?UTF-8?q?=E7=94=9F=E6=B6=AF=E7=BB=88=E8=BA=AB=E8=AE=A1=E6=95=B0=E5=9F=9F?= =?UTF-8?q?=E2=80=94=E2=80=94SAVE=5FFORMAT=5FVERSION=205=E2=86=926?= =?UTF-8?q?=EF=BC=88=E7=89=88=E6=9C=AC=E5=8F=B2=E6=B3=A8=E9=87=8A=E9=9A=8F?= =?UTF-8?q?=E6=89=B9=E6=8D=A2=E4=BB=A3=EF=BC=89+meta=20=E4=B8=A4=E6=96=B0?= =?UTF-8?q?=E5=9F=9F=20adventure=5Ftotal=E3=80=94=E6=80=BB=E5=B1=80?= =?UTF-8?q?=E6=95=B0=EF=BC=8C=E5=BC=80=E5=B1=80=E5=8D=B3=E8=AE=A1=E5=90=AB?= =?UTF-8?q?=E6=94=BE=E5=BC=83=E4=B8=8E=E5=A4=B1=E8=B4=A5=E3=80=95/adventur?= =?UTF-8?q?e=5Fcleared=E3=80=94=E9=80=9A=E5=85=B3=E5=B1=80=E6=95=B0?= =?UTF-8?q?=EF=BC=8C=E4=B8=89=E5=85=B3=E5=85=A8=E9=80=9A=E6=89=8D=E8=AE=A1?= =?UTF-8?q?=201=E2=80=94=E2=80=94=E5=88=A4=E5=AE=9A=E5=AD=97=E6=AE=B5=20su?= =?UTF-8?q?mmary.cleared=20=E7=8E=B0=E6=88=90=E7=9B=B4=E7=94=A8=E3=80=95+?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=87=BD=E6=95=B0=20=5Fmigrate=5Fv5=5Fv6?= =?UTF-8?q?=EF=BC=88=E6=B2=BF=E7=8E=B0=E5=BD=B9=E8=BF=81=E7=A7=BB=E9=93=BE?= =?UTF-8?q?=20=5Finit=20=E8=87=AA=E6=B3=A8=E5=86=8C=EF=BC=9B=E6=97=A7?= =?UTF-8?q?=E6=A1=A3=E7=BC=BA=E9=94=AE=20=5Fnormalize=20=E6=8C=89=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E8=A1=A5=E9=9B=B6=E3=80=81=E5=8F=AA=E5=A2=9E=E4=B8=8D?= =?UTF-8?q?=E6=94=B9=3D=E5=B9=82=E7=AD=89=EF=BC=8Cv3=E2=86=92v4=20?= =?UTF-8?q?=E5=85=88=E4=BE=8B=EF=BC=89+=E5=8F=AA=E8=AF=BB=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E5=99=A8=20adventure=5Ftotal()/adventure=5Fcleared()+?= =?UTF-8?q?=E5=86=99=E5=85=A5=E5=8D=95=E5=8F=A3=3Dapply=5Fadventure=5Fsumm?= =?UTF-8?q?ary=20=E6=94=B6=E6=9D=9F=E5=8F=A3=E5=A4=B4=E9=83=A8=E8=87=AA?= =?UTF-8?q?=E5=A2=9E=EF=BC=88=E4=B8=89=E7=BB=93=E5=B1=80=E5=90=8C=E7=BB=8F?= =?UTF-8?q?=E6=9C=AC=E5=8F=A3=E6=81=B0=E8=AE=A1=E4=B8=80=E6=AC=A1=EF=BC=8C?= =?UTF-8?q?=E7=A6=81=E7=AC=AC=E4=BA=8C=E5=86=99=E5=85=A5=E7=82=B9=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=A5=91=E7=BA=A6=202=EF=BC=9B=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E9=93=BE=E9=99=88=E6=97=A7=E6=B3=A8=E9=87=8A=20v1=E2=86=92v3/v?= =?UTF-8?q?1=E2=86=92v6=20=E9=A1=BA=E6=89=8B=E6=A0=A1=E6=AD=A3=EF=BC=89+?= =?UTF-8?q?=E5=9F=9F=E6=B8=85=E5=8D=95=20owner=20=E6=B3=A8=E8=AE=B0?= =?UTF-8?q?=EF=BC=88=E7=94=9F=E6=B6=AF.md=EF=BC=89+registry=20save=5Ffile?= =?UTF-8?q?=20=E6=9D=A1=E7=9B=AE=20v4=E2=86=92v6=20=E6=8D=A2=E4=BB=A3?= =?UTF-8?q?=EF=BC=88v5=20=E6=89=B9=E6=BC=8F=E8=B4=A6=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=EF=BC=89+=E5=86=92=E7=83=9F=E9=9A=8F=E6=89=B9=EF=BC=9A?= =?UTF-8?q?=E7=A1=AC=E6=96=AD=E8=A8=80=E3=80=8C=E5=86=99=E7=9B=98=20versio?= =?UTF-8?q?n=3D=3D5=E3=80=8D=E4=B8=A4=E5=A4=84=E6=94=B9=206=EF=BC=88?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E9=93=BE=E6=AE=B5+v1=20=E6=97=A7=E6=A1=A3?= =?UTF-8?q?=E6=AE=B5=EF=BC=89+=E2=91=A0=E5=BE=80=E8=BF=94=E6=AE=B5?= =?UTF-8?q?=E6=94=B6=E6=9D=9F=E8=87=AA=E5=A2=9E=E6=81=B0=E4=B8=80/?= =?UTF-8?q?=E5=BE=80=E8=BF=94=E7=9B=B8=E7=AD=89=E4=B8=A4=E6=96=AD=E8=A8=80?= =?UTF-8?q?+=E2=91=A1=E6=96=B0=E6=A1=A3=E6=AE=B5=E7=BC=BA=E7=9C=81?= =?UTF-8?q?=E5=85=A8=E9=9B=B6=E6=96=AD=E8=A8=80+=E2=91=AC=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=AE=B5=EF=BC=88v5=20=E6=97=A7=E6=A1=A3=E5=8D=87=20v?= =?UTF-8?q?6=20=E7=BC=BA=E7=9C=81=E8=A1=A5=E9=9B=B6=E4=B8=94=E6=97=A2?= =?UTF-8?q?=E6=9C=89=E4=BD=99=E9=A2=9D=E5=8E=9F=E6=A0=B7/=E5=8D=87?= =?UTF-8?q?=E6=A1=A3=E5=B9=82=E7=AD=89=E5=86=99=E7=9B=98=E5=86=8D=E8=BD=BD?= =?UTF-8?q?=E9=9B=B6=E6=BC=82=E7=A7=BB/=E9=80=9A=E5=85=B3=E5=B1=80=20clear?= =?UTF-8?q?ed+1=20=E5=A4=B9=E5=85=B7=EF=BC=89=E2=80=94=E2=80=94=E5=86=92?= =?UTF-8?q?=E7=83=9F=20821/0=EF=BC=88=E5=9F=BA=E7=BA=BF=20814+7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/registry/entities.yaml | 4 ++-- game/core/save_manager.gd | 41 ++++++++++++++++++++++++++++++---- game/smoke/smoke_all.gd | 42 ++++++++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 7cadfa9d..f6282379 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -69,8 +69,8 @@ entities: attributes: path: user://save.json(单档 MVP——多存档槽位 P2 保留) format: JSON 单档(人可读可调试;字典键一律 String) - version: SAVE_FORMAT_VERSION=4(registry schema_version 同源;v4=同调批扩域 2026-10-03 迭代 11 第①批——meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level + 断点附随件 attunement_charges,旧档缺键由 _normalize 按默认补齐;v3=断点附随件扩列+摘要 keystone_drops 2026-10-01 迭代 10;v2=断点域出生 2026-09-29 迭代 8 卡①;生产迁移链 v1→v4 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底) - domains: meta 域九件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——同调位里程碑计数源,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除) + version: SAVE_FORMAT_VERSION=6(registry schema_version 同源;v6=生涯终身计数域 2026-10-07 迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary;v5=天赋树域 2026-10-04 迭代 12 T9——meta 三新域 tree_nodes/fork_sides/migration_note;v4=同调批扩域 2026-10-03 迭代 11 第①批——meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level + 断点附随件 attunement_charges,旧档缺键由 _normalize 按默认补齐;v3=断点附随件扩列+摘要 keystone_drops 2026-10-01 迭代 10;v2=断点域出生 2026-09-29 迭代 8 卡①;生产迁移链 v1→v6 链式补空;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底) + domains: meta 域十一件——keystone_balance(元进度)/ forge_levels 三档(元进度)/ item_unlocks(元进度)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ boss_kill_count(秘宝——同调位里程碑计数源,Boss 恒唯一终点故收束入账=levels_cleared)/ attunement_equipped(秘宝——装配关系有序数组,局外进度事实非局内道具库存,装配序即呈现序)/ attune_refresh_level(秘宝——Y 刷新升级购买等级)/ tree_nodes+fork_sides(天赋树——迭代 12 T9 树域)/ adventure_total+adventure_cleared(生涯——终身计数域,迭代 18 卡④,写入唯一经 apply_adventure_summary)/ recent_summaries cap 10(冒险结构,元素附 boss_drops 副本);+ breakpoint 域(断点快照:七字段拍板面+附随件〔主种子/取数计数器/钥石累计/已到节点序/道具库存/bonus_* 附随件/attunement_charges 充能余量——迭代 11 第①批 T6〕,空字典=无断点合法态,三结局清除) write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);冒险选路停点断点快照写盘 + 三结局清断点写盘(迭代 8 卡①);调试两层与单关测试路径(LevelRunner.start_level,仅 QA 工具)不写档 atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载) corruption_policy: 坏档改名 .corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash) diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index d57b7fd6..b2db2cff 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -17,6 +17,9 @@ # breakpoint.attunement_charges(主动件充能余量)——每冒险重置语义的断点瞬态附随件,内容装配归 # AdventureRunner(随 T6 接线入断点),本类照单承载;契约 2 域界注记同断点域豁免先例 # recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径) +# adventure_total / adventure_cleared(生涯终身计数域)—— 内容 owner = 生涯.md(跨局汇总页 +# 数据源:总局数开局即计〔三结局同经收束恰计一次,含放弃与失败〕/通关局数三关全通才计 1 +# ——判定字段 summary.cleared;写入唯一经 apply_adventure_summary,迭代 18 卡④) # breakpoint(冒险断点快照——停点粒度续玩域)—— 内容 owner = 冒险结构.md(七字段拍板面 # +恢复必需附随件:主种子/取数计数器/钥石累计/已到节点序/道具库存);契约 2 域界反面清单 # 只约束 meta 永久进度域——断点域=冒险进行中瞬态快照,生命/GP/道具库存按拍板面入域 @@ -36,7 +39,7 @@ class_name SaveManager extends RefCounted -const SAVE_FORMAT_VERSION := 5 # registry schema_version:v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;任务书「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 +const SAVE_FORMAT_VERSION := 6 # registry schema_version:v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;任务书「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台) @@ -44,7 +47,7 @@ const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合 var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径) var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写) var _data: Dictionary[String, Variant] = {} # 内存数据模型(_init 即内置默认;load 后为档案内容) -var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;v1→v3 生产迁移链现行——_init 自注册) +var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;v1→v6 生产迁移链现行——_init 自注册) func _init(path: String = "user://save.json") -> void: @@ -55,6 +58,7 @@ func _init(path: String = "user://save.json") -> void: register_migration(2, _migrate_v2_v3) register_migration(3, _migrate_v3_v4) register_migration(4, _migrate_v4_v5) + register_migration(5, _migrate_v5_v6) # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) @@ -97,6 +101,14 @@ func _migrate_v4_v5(data: Dictionary) -> Dictionary: return data +# v5→v6:生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total/adventure_cleared,旧档缺键 +# 由 _normalize 按默认补零,无结构迁移体;版本号推进仅为 schema_version 对齐——v3→v4 先例。 +# 幂等性:重复迁移只覆写 version,既有值零改动;加载后 _normalize 补齐缺省——旧档可载可玩) +func _migrate_v5_v6(data: Dictionary) -> Dictionary: + data["version"] = SAVE_FORMAT_VERSION + return data + + # ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)---- func _default_data() -> Dictionary[String, Variant]: @@ -115,6 +127,8 @@ func _default_data() -> Dictionary[String, Variant]: "fork_sides": {}, # A/B 激活侧(fork_group→"a"|"b"——营地免费切换承载) "recent_summaries": [], "first_adventure_done": false, + "adventure_total": 0, # 生涯终身计数·冒险总局数(迭代 18 卡④——开局即计含放弃与失败,收束唯一入账) + "adventure_cleared": 0, # 生涯终身计数·通关局数(三关全通才计 1——判定字段 summary.cleared) }, "breakpoint": {}, # 冒险断点快照(空 = 无断点合法态——「继续上局」隐藏;迭代 8 卡①) } @@ -191,7 +205,7 @@ func write_to_disk() -> bool: # 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约: -# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v3,_init 自注册), +# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v6,_init 自注册), # 冒烟经本口验证链式升级。 func register_migration(from_version: int, fn: Callable) -> void: _migrations[from_version] = fn @@ -251,6 +265,20 @@ func boss_kill_count() -> int: return int(_data["meta"].get("boss_kill_count", 0)) +# ---- 生涯终身计数域(迭代 18 卡④——跨局汇总页数据源;只读面经访问器, +# 写入唯一经 apply_adventure_summary 收束口,禁第二写入点)---- + +# 冒险总局数(口径=开局即计:三结局 completed/failed/abandoned 同经收束恰计一次,含放弃与失败) +func adventure_total() -> int: + return int(_data["meta"].get("adventure_total", 0)) + + +# 通关局数(口径=三关全通才计 1——判定字段 summary.cleared,adventure_runner「completed」 +# 唯一出口=末关通关) +func adventure_cleared() -> int: + return int(_data["meta"].get("adventure_cleared", 0)) + + # ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)---- func tree_nodes() -> Dictionary: @@ -321,8 +349,13 @@ func recent_summaries() -> Array: # 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为 # 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计 # (迭代 11 第①批 T2——同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附 -# keystone_drops 副本——复盘可归因)。 +# keystone_drops 副本——复盘可归因)+ 生涯终身计数自增(迭代 18 卡④——总局数开局即计: +# 三结局同经本口恰计一次;通关局数按 summary.cleared 判定三关全通才计 1。终身计数写入 +# 单口=本函数,禁第二写入点)。 func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void: + _data["meta"]["adventure_total"] = adventure_total() + 1 + if bool(summary.get("cleared", false)): + _data["meta"]["adventure_cleared"] = adventure_cleared() + 1 add_keystone(int(summary.get("keystone_total", 0))) var cleared := int(summary.get("levels_cleared", 0)) for li in range(1, cleared + 1): diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 4bf08e32..300eecdb 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -4655,7 +4655,7 @@ func _smoke_battle_log_view() -> void: # ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/ -# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成)。测试文件统一 user://smoke_save_*.json 隔离, +# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离, # 段首清场+段尾清理——绝不碰 user://save.json(红线)。 func _smoke_save() -> void: print("--- SaveManager 存档读写/迁移/兜底 ---") @@ -4665,7 +4665,8 @@ func _smoke_save() -> void: "user://smoke_save_corrupt.json", "user://smoke_save_migrate.json", "user://smoke_save_domain.json", "user://smoke_save_cap.json", "user://smoke_save_integration.json", "user://smoke_save_breakpoint.json", - "user://smoke_save_v2compat.json"] + "user://smoke_save_v2compat.json", "user://smoke_save_v5upgrade.json", + "user://smoke_save_v5legacy.json"] var stale: Array = [] for p in paths: stale.append(p) @@ -4687,6 +4688,8 @@ func _smoke_save() -> void: "apply 原语入账(钥石 7+3、通关次数 1/1/0)") check(w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3), "首杀标记随通关关卡置位(1/2 有、3 无)") + check(w.adventure_total() == 1 and w.adventure_cleared() == 0, + "生涯终身计数随 apply 自增恰一(中止局:总局数 1、通关局数 0——迭代 18 卡④)") check(w.write_to_disk(), "写盘成功") var r = sm_script.new(paths[0]) check(r.load_from_disk(), "读回成功") @@ -4697,6 +4700,7 @@ func _smoke_save() -> void: check(r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0, "通关次数表往返相等") check(r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3), "首杀标记往返相等") + check(r.adventure_total() == 1 and r.adventure_cleared() == 0, "生涯终身计数往返相等(1/0)") var s0: Dictionary = r.recent_summaries()[0] check(r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3, "冒险摘要往返相等(1 条,seed 99)") @@ -4706,6 +4710,8 @@ func _smoke_save() -> void: check(f2.load_from_disk() and f2.keystone_balance() == 0 and f2.forge_level("atk") == 0 and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty(), "新档合法态(load true + 全零默认)") + check(f2.adventure_total() == 0 and f2.adventure_cleared() == 0, + "新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)") # ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外) var f3 = sm_script.new(paths[2]) @@ -4748,8 +4754,8 @@ func _smoke_save() -> void: fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12})) fm.close() check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)") - check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 5, - "迁移链 0→1(测试钩)→5(生产) 写盘 version == 5(v5=天赋树域,迭代 12 T9——v4 被同调批占用顺延披露)") + check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 6, + "迁移链 0→1(测试钩)→6(生产) 写盘 version == 6(v6=生涯终身计数域,迭代 18 卡④——v5=天赋树域迭代 12 T9)") # ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2; # 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免) @@ -4846,8 +4852,8 @@ func _smoke_save() -> void: fold.close() check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(), "v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)") - check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 5, - "v1 旧档迁移后写盘 version == 5(v1→v5 链式——天赋树域 2026-10-04)") + check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 6, + "v1 旧档迁移后写盘 version == 6(v1→v6 链式——生涯终身计数域 2026-10-07)") # ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩) var fv2 = sm_script.new(paths[10]) @@ -4866,6 +4872,30 @@ func _smoke_save() -> void: check(adr_v2.resume_adventure(fv2.breakpoint_snapshot(), {"hero_override": {}}), "v2 旧断点续玩受理(缺 bonus 附随件走常规重建路径——g 裁 semver 口径)") + # ⑬ 生涯终身计数域(迭代 18 卡④):v5 旧档升 v6 缺省补零 + 升档幂等 + 收束自增恰一 + #(真实旧档形态=计数域两键不存在;升档只增不改——既有 meta 值零触碰,存档红线 S2 断言面) + var f13 = sm_script.new(paths[11]) + var fv5u := FileAccess.open(paths[11], FileAccess.WRITE) + fv5u.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 9}})) + fv5u.close() + check(f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0 + and f13.keystone_balance() == 9, + "v5 旧档升 v6:计数域缺省补零且既有余额原样(迁移只增字段不改值)") + check(f13.write_to_disk() and f13.load_from_disk() + and int(JSON.parse_string(FileAccess.open(paths[11], FileAccess.READ).get_as_text()).get("version", -1)) == 6 + and f13.adventure_total() == 0 and f13.adventure_cleared() == 0, + "升档幂等:v6 档写盘再载零漂移(重复迁移零副作用)") + var f13b = sm_script.new(paths[12]) + f13b.load_from_disk() + f13b.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3, + "stopped_at": 0, "keystone_total": 0, "master_seed": 1}) + check(f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1, + "收束自增恰一·通关局(三关全通 cleared=true:总局数+1 且通关局数+1)") + f13b.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1, + "stopped_at": 2, "keystone_total": 0, "master_seed": 2}) + check(f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1, + "收束自增恰一·中止局(放弃局:仅总局数+1,通关局数不动)") + # 段尾清理(测试文件绝不残留) for p in stale: if FileAccess.file_exists(p):