卡①T5(核心数据面结构收敛批): 存档域拆件——SlotStore建件(槽位族10公开+槽位态随迁+_migrate_legacy_save/_copy_file/_remove_file随迁,_rename_overwrite留save IO单点)/MetaStateStore建件(写口原语13+只读投影6+add_keystone私有)/save_manager瘦身(52→7公开:load_from_disk/write_to_disk/register_migration+断点族4;_load_parsed/_peek_migrated读盘域提取,迁移链文案经fail数组逐字带出;类变量6)/全域改址449处(槽位族→save.slots.,元进度族→save.meta_state.,读面投影改形career/tree/forge/attune_state;meta转发与GainJournal.vision_used按卡留T6);[用户特许2026-10-28:本批测试动作移交用户,套件与闸停跑,实现继续]
This commit is contained in:
1 parent
989c489c55
commit
b182073941
29 files changed
+873
-884
No files matched your search
@@ -112,24 +112,24 @@ func _save_summary_integration() -> void:
|
||||
# ⑨ 摘要集成(冒险结构批产物驱动):run_adventure 真局摘要 → apply 原语 → 域读数 → 写盘读回保持
|
||||
var st9: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
|
||||
var f9 = sm_script.new(path9)
|
||||
f9.apply_adventure_summary(st9.get("summary", {}))
|
||||
f9.meta_state.apply_adventure_summary(st9.get("summary", {}))
|
||||
var ks_run := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
|
||||
Kit.check("摘要集成:冒险驱动满配冒险成功(seed 42)", String(st9.get("result", "")) == "completed")
|
||||
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.keystone_balance(), f9.keystone_balance() == ks_run)
|
||||
Kit.check("集成关卡完成次数表全 1", f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1)
|
||||
Kit.check("集成三关首杀标记齐", f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3))
|
||||
Kit.check("集成 recent 1 条且 seed 42", f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42)
|
||||
Kit.check("集成钥石余额 = ΣBoss 首杀行值(%d——逐杀掉落全源)" % f9.meta_state.keystone_balance(), f9.meta_state.keystone_balance() == ks_run)
|
||||
Kit.check("集成关卡完成次数表全 1", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 1)
|
||||
Kit.check("集成三关首杀标记齐", f9.meta_state.career_state().boss_first_kill.has(str(1)) and f9.meta_state.career_state().boss_first_kill.has(str(2)) and f9.meta_state.career_state().boss_first_kill.has(str(3)))
|
||||
Kit.check("集成 recent 1 条且 seed 42", f9.meta_state.career_state().recent_summaries.size() == 1 and int(f9.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 42)
|
||||
var st9b: Dictionary = LevelAutoplay.run_adventure(_db, 42, 900, 400)
|
||||
f9.apply_adventure_summary(st9b.get("summary", {}))
|
||||
Kit.check("第二局后关卡完成次数全 2", f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2)
|
||||
Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.keystone_balance() == 2 * ks_run)
|
||||
Kit.check("集成 recent 长度 2", f9.recent_summaries().size() == 2)
|
||||
f9.meta_state.apply_adventure_summary(st9b.get("summary", {}))
|
||||
Kit.check("第二局后关卡完成次数全 2", f9.meta_state.career_state().clear_counts.get(str(1), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(2), 0) == 2 and f9.meta_state.career_state().clear_counts.get(str(3), 0) == 2)
|
||||
Kit.check("第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)", f9.meta_state.keystone_balance() == 2 * ks_run)
|
||||
Kit.check("集成 recent 长度 2", f9.meta_state.career_state().recent_summaries.size() == 2)
|
||||
Kit.check("集成写盘成功", f9.write_to_disk())
|
||||
var r9 = sm_script.new(path9)
|
||||
Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2
|
||||
and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3))
|
||||
Kit.check("集成写盘读回全量保持", r9.load_from_disk() and r9.meta_state.keystone_balance() == 2 * ks_run and r9.meta_state.career_state().clear_counts.get(str(3), 0) == 2
|
||||
and r9.meta_state.career_state().recent_summaries.size() == 2 and r9.meta_state.career_state().boss_first_kill.has(str(3)))
|
||||
if FileAccess.file_exists(path9): # 段尾清理(与段首清场对称——源 _smoke_save 同款)
|
||||
DirAccess.remove_absolute(path9)
|
||||
|
||||
|
||||
@@ -105,12 +105,12 @@ func _smoke_generated_map_determinism() -> void:
|
||||
var sv = SaveManagerScript.new(_bp_path + ".flag.json")
|
||||
sv.load_from_disk()
|
||||
Kit.check("新档首次冒险布尔位 = false(默认域——首局恒走固定种子 42)",
|
||||
sv.is_first_adventure_done() == false)
|
||||
sv.meta_state.career_state().first_adventure_done == false)
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"})
|
||||
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.is_first_adventure_done() == true)
|
||||
Kit.check("冒险收束点置位(三结局同写——写入只在收束挂点)", sv.meta_state.career_state().first_adventure_done == true)
|
||||
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
|
||||
Kit.check("置位幂等(重复收束不翻转)", sv.is_first_adventure_done() == true)
|
||||
Kit.check("置位幂等(重复收束不翻转)", sv.meta_state.career_state().first_adventure_done == true)
|
||||
DirAccess.remove_absolute(_bp_path)
|
||||
DirAccess.remove_absolute(_bp_path + ".flag.json")
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ func _test_old_version_save_migrates_and_resumes() -> void:
|
||||
var sm_old := SaveManagerScript.new(SAVE_PATH)
|
||||
Kit.check_quiet(sm_old.load_from_disk(), "旧档用例:旧档经迁移链可载")
|
||||
Kit.check_quiet(sm_old.has_breakpoint(), "旧档用例:迁移后断点未被迁移链吃掉")
|
||||
var forge: Dictionary = sm_old.forge_routes_snapshot()
|
||||
var forge: Dictionary = sm_old.meta_state.forge_state()
|
||||
Kit.check_quiet(str(forge.get("weapon", "")) == "sword",
|
||||
"旧档用例:迁移补齐当前版本新增域(缺 forge_routes 的旧档按初始持有态可玩)")
|
||||
|
||||
|
||||
+11
-11
@@ -176,9 +176,9 @@ func _smoke_boot_chain() -> void:
|
||||
check(not sm.has_breakpoint(),
|
||||
"V2d boot 无断点在档(启动态不隐式激活——卡① sv2 钉死)〔装配生命批 V2·卡② mn8 改写〕")
|
||||
# ④-1b 启动态三断言(卡① sv2——真实 autoload boot 链)
|
||||
check(sm.active_slot() == 0 and not sm.has_write_target(),
|
||||
check(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
|
||||
"V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕")
|
||||
check(sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0,
|
||||
check(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint() and sm.meta_state.career_state().adventure_total == 0,
|
||||
"V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕")
|
||||
check(not sm.write_to_disk(),
|
||||
"V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕")
|
||||
@@ -187,7 +187,7 @@ func _smoke_boot_chain() -> void:
|
||||
for p in ["user://smoke_boot_chain.json", "user://smoke_boot_chain.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_boot_chain.json")
|
||||
sm.slots.attach_path("user://smoke_boot_chain.json")
|
||||
check(main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null
|
||||
and main._menu_screen.quitbtn != null,
|
||||
"V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕")
|
||||
@@ -222,8 +222,8 @@ func _smoke_run_lifecycle() -> void:
|
||||
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_run_lifecycle.json")
|
||||
sm.mark_first_adventure_done()
|
||||
sm.slots.attach_path("user://smoke_run_lifecycle.json")
|
||||
sm.meta_state.mark_first_adventure_done()
|
||||
# ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ----
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
main._start_adventure_from_menu()
|
||||
@@ -278,8 +278,8 @@ func _smoke_sim_guards() -> void:
|
||||
for p in ["user://smoke_sim_guards.json", "user://smoke_sim_guards.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_sim_guards.json")
|
||||
sm.mark_first_adventure_done()
|
||||
sm.slots.attach_path("user://smoke_sim_guards.json")
|
||||
sm.meta_state.mark_first_adventure_done()
|
||||
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
|
||||
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
|
||||
"前置:闲时会话(settlement 空 + is_adventure=false)")
|
||||
@@ -420,7 +420,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
for p in ["user://smoke_lifecycle.json", "user://smoke_lifecycle.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_lifecycle.json")
|
||||
sm.slots.attach_path("user://smoke_lifecycle.json")
|
||||
main.debug_overlay.seed_edit.text = "42"
|
||||
# 阶段①闲时
|
||||
await _stage_faces(main, "V3a 闲时", "null", "null", "null",
|
||||
@@ -513,7 +513,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤);槽模板注入隔离命名空间
|
||||
#(防碰真实槽 user://save_slot_N.json——真实档红线),营地闭合后恢复生命周期 attach 写盘目标
|
||||
#(卡① sv3 无激活拒写——后续阶段断点写盘依赖)
|
||||
sm.attach_slot_template("user://smoke_lifecycle_slot_%d.json")
|
||||
sm.slots.attach_slot_template("user://smoke_lifecycle_slot_%d.json")
|
||||
main._open_save_select()
|
||||
for i in range(2):
|
||||
await process_frame
|
||||
@@ -546,7 +546,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||||
for i in range(2):
|
||||
await process_frame
|
||||
sm.attach_path("user://smoke_lifecycle.json") # 恢复生命周期写盘目标
|
||||
sm.slots.attach_path("user://smoke_lifecycle.json") # 恢复生命周期写盘目标
|
||||
for suf in ["", ".tmp"]:
|
||||
var slot_p: String = "user://smoke_lifecycle_slot_1.json" + suf
|
||||
if FileAccess.file_exists(slot_p):
|
||||
@@ -578,7 +578,7 @@ func _smoke_lifecycle_matrix() -> void:
|
||||
for p in ["user://smoke_lifecycle_resume.json", "user://smoke_lifecycle_resume.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_lifecycle_resume.json")
|
||||
sm.slots.attach_path("user://smoke_lifecycle_resume.json")
|
||||
sm.set_breakpoint({
|
||||
"master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0,
|
||||
"level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [],
|
||||
|
||||
@@ -132,22 +132,22 @@ func _meta_pricing_and_buy() -> void:
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
# ③ 购买成功:级+1、余额−价;buy 内含写盘——reload 后持久(树全钥石单币)
|
||||
sv.add_keystone(1000)
|
||||
var bal0: int = sv.keystone_balance()
|
||||
sv.meta_state._add_keystone(1000)
|
||||
var bal0: int = sv.meta_state.keystone_balance()
|
||||
var price0: int = int(_db.tree_node("sp_hp").get("price", 0))
|
||||
Kit.check("树购买受理(Lv 0→1、余额 %d−%d)" % [bal0, price0],
|
||||
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1 and sv.keystone_balance() == bal0 - price0)
|
||||
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv.meta_state.keystone_balance() == bal0 - price0)
|
||||
var sv_r = _meta_new_save(_META_SMOKE_PATHS[0])
|
||||
Kit.check("购买持久(reload 后级与余额不变)",
|
||||
sv_r.tree_level("sp_hp") == 1 and sv_r.keystone_balance() == bal0 - price0)
|
||||
sv_r.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and sv_r.meta_state.keystone_balance() == bal0 - price0)
|
||||
# ③ 余额不足 / 未知节点:false 零副作用
|
||||
var sv_dry = _meta_new_save(_META_SMOKE_PATHS[1])
|
||||
var mp_dry = load("res://core/meta_progression.gd").new(_db, sv_dry)
|
||||
var tn_before: Dictionary = sv_dry.tree_nodes()
|
||||
var fs_before: Dictionary = sv_dry.fork_sides()
|
||||
var tn_before: Dictionary = sv_dry.meta_state.tree_state().nodes
|
||||
var fs_before: Dictionary = sv_dry.meta_state.tree_state().fork_sides
|
||||
Kit.check("余额不足 false 零副作用(0 余额购买拒绝——三域零变化〔天赋树批 V3〕)",
|
||||
not mp_dry.tree_buy("sp_hp") and sv_dry.keystone_balance() == 0
|
||||
and sv_dry.tree_nodes() == tn_before and sv_dry.fork_sides() == fs_before)
|
||||
not mp_dry.tree_buy("sp_hp") and sv_dry.meta_state.keystone_balance() == 0
|
||||
and sv_dry.meta_state.tree_state().nodes == tn_before and sv_dry.meta_state.tree_state().fork_sides == fs_before)
|
||||
Kit.check("未知节点 false 零副作用(词表外出域)", not mp_dry.tree_buy("no_such_node"))
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
Kit.check("明细 total = keystone_total(逐杀掉落全源——无二次派生加账)",
|
||||
int(d1.get("total", 0)) == int(row1.get("first_kill", 0)))
|
||||
Kit.check("余额 = 累计入账(逐杀掉落合计——无重复加账)",
|
||||
sv.keystone_balance() == int(row1.get("first_kill", 0)))
|
||||
sv.meta_state.keystone_balance() == int(row1.get("first_kill", 0)))
|
||||
# ⑥ 再通转复杀档(首杀标记置位后小额恒定不递减——标记面在 apply,金额面在逐杀裁决)
|
||||
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
|
||||
@@ -196,39 +196,39 @@ func _meta_settle_and_workshop() -> void:
|
||||
not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
|
||||
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)))
|
||||
# ⑦ 败北零关卡完成摘要:余额只增不减(零入账)、clear_counts 不变
|
||||
var bal_before: int = sv.keystone_balance()
|
||||
var cc1_before: int = sv.clear_count(1)
|
||||
var bal_before: int = sv.meta_state.keystone_balance()
|
||||
var cc1_before: int = sv.meta_state.career_state().clear_counts.get(str(1), 0)
|
||||
var dz: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
|
||||
"stopped_at": 1, "keystone_total": 0, "master_seed": 15})
|
||||
Kit.check("败北零关卡完成摘要:零入账零标记(钥石只增不减——验收1;败北局 clear_counts 不变)",
|
||||
dz["boss_drops"].is_empty() and sv.keystone_balance() == bal_before and sv.clear_count(1) == cc1_before)
|
||||
dz["boss_drops"].is_empty() and sv.meta_state.keystone_balance() == bal_before and sv.meta_state.career_state().clear_counts.get(str(1), 0) == cc1_before)
|
||||
# ⑧ 秘宝解锁服务(V3 钥石购买/V4 条件解锁;注入=直调解锁写口+直置存档生涯计数,
|
||||
# 不走正常资源积累流程——卡片 7.1 测试方式)
|
||||
var sv_u = _meta_new_save(_META_SMOKE_PATHS[11])
|
||||
var mp_u = load("res://core/meta_progression.gd").new(_db, sv_u)
|
||||
sv_u.add_keystone(10)
|
||||
sv_u.meta_state._add_keystone(10)
|
||||
Kit.check("条件件误购拒绝(购买件专属——契约 8 分配面)〔秘宝批 V3 反例〕",
|
||||
not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.item_unlocked("sky_eye")
|
||||
not mp_u.buy_treasure_unlock("sky_eye") and not sv_u.meta_state.item_unlocked("sky_eye")
|
||||
and not mp_u.buy_treasure_unlock("ranged_pick"))
|
||||
Kit.check("余额不足购买拒绝且零副作用(余额 10 < 价 25)〔秘宝批 V3〕",
|
||||
not mp_u.buy_treasure_unlock("twin_loader") and sv_u.keystone_balance() == 10)
|
||||
not mp_u.buy_treasure_unlock("twin_loader") and sv_u.meta_state.keystone_balance() == 10)
|
||||
Kit.check("钥石购买解锁:余额足→解锁态落档+余额扣减正确(价 10)〔秘宝批 V3〕",
|
||||
mp_u.buy_treasure_unlock("morning_order") and sv_u.item_unlocked("morning_order")
|
||||
and sv_u.keystone_balance() == 0)
|
||||
mp_u.buy_treasure_unlock("morning_order") and sv_u.meta_state.item_unlocked("morning_order")
|
||||
and sv_u.meta_state.keystone_balance() == 0)
|
||||
Kit.check("已解锁重复购买拒绝〔秘宝批 V3〕",
|
||||
not mp_u.buy_treasure_unlock("morning_order"))
|
||||
Kit.check("条件未达不解锁(远程镐=冒险成功 1 次)〔秘宝批 V4〕",
|
||||
mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.item_unlocked("ranged_pick"))
|
||||
mp_u.refresh_treasure_unlocks().is_empty() and not sv_u.meta_state.item_unlocked("ranged_pick"))
|
||||
sv_u._data["meta"]["adventure_cleared"] = 1 # 直置存档生涯计数(调试注入口径)
|
||||
Kit.check("远程镐=adventure_cleared≥1 达标恰解锁一次+重复刷新幂等(C6:冒险成功=打通整局冒险)〔秘宝批 V4〕",
|
||||
(mp_u.refresh_treasure_unlocks() as Array).has("ranged_pick")
|
||||
and sv_u.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty())
|
||||
and sv_u.meta_state.item_unlocked("ranged_pick") and mp_u.refresh_treasure_unlocks().is_empty())
|
||||
sv_u._data["meta"]["boss_kill_count"] = 3
|
||||
Kit.check("刷新执照=boss_kill_count≥3(阈值读 unlock_requires 行值不硬编)〔秘宝批 V4〕",
|
||||
(mp_u.refresh_treasure_unlocks() as Array).has("refresh_license"))
|
||||
sv_u._data["meta"]["adventure_cleared"] = 3
|
||||
Kit.check("天眼单条件不足不解锁(AND 语义——cleared 3 达 boss 4 未达)〔秘宝批 V4〕",
|
||||
not sv_u.item_unlocked("sky_eye"))
|
||||
not sv_u.meta_state.item_unlocked("sky_eye"))
|
||||
sv_u._data["meta"]["boss_kill_count"] = 4
|
||||
Kit.check("天眼双条件 AND(冒险成功≥3 且 Boss≥4)达标解锁〔秘宝批 V4〕",
|
||||
(mp_u.refresh_treasure_unlocks() as Array).has("sky_eye"))
|
||||
@@ -240,7 +240,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
r_n += 1 if bool(st_row.get("unlocked", false)) else 0
|
||||
Kit.check("解锁态持久投影一致(reload 面同源读数;5 件=购买 1+条件 4)〔秘宝批 V4〕", r_n == 5)
|
||||
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
|
||||
var rs: Array = sv.recent_summaries()
|
||||
var rs: Array = sv.meta_state.career_state().recent_summaries
|
||||
Kit.check("摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)",
|
||||
rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
|
||||
and (rs[1] as Dictionary).has("keystone_drops")
|
||||
@@ -251,7 +251,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
mp_c.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 0,
|
||||
"stopped_at": 1, "keystone_total": 0, "master_seed": i})
|
||||
Kit.check("摘要 cap 10 且最新在前(12 连 settle 截断后 seed 11 居首)",
|
||||
sv_c.recent_summaries().size() == 10 and int(sv_c.recent_summaries()[0].get("master_seed", -1)) == 11)
|
||||
sv_c.meta_state.career_state().recent_summaries.size() == 10 and int(sv_c.meta_state.career_state().recent_summaries[0].get("master_seed", -1)) == 11)
|
||||
# ⑪ 同调位账本(装配持久/满位禁选/Y 升级语义;写口 = MetaProgression 同调段)
|
||||
var sv_a = _meta_new_save(_META_SMOKE_PATHS[6])
|
||||
var mp_a = load("res://core/meta_progression.gd").new(_db, sv_a)
|
||||
@@ -259,17 +259,17 @@ func _meta_settle_and_workshop() -> void:
|
||||
Attunement.slots(_db, sv_a) == 0 and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 0)
|
||||
Kit.check("满位禁选(Slots 0 时装配拒绝;兼未解锁拒绝前置——直置解锁后下方装配)〔秘宝批 V5〕",
|
||||
mp_a.toggle_attunement("morning_order") == false)
|
||||
sv_a.add_keystone(200)
|
||||
sv_a.unlock_item("morning_order")
|
||||
sv_a.unlock_item("twin_loader")
|
||||
sv_a.meta_state._add_keystone(200)
|
||||
sv_a.meta_state.unlock_item("morning_order")
|
||||
sv_a.meta_state.unlock_item("twin_loader")
|
||||
Kit.check("Y 升级购买入账(Lv1 → 每冒险 +1 次刷新)",
|
||||
mp_a.buy_attune_refresh() and sv_a.attune_refresh_level() == 1
|
||||
mp_a.buy_attune_refresh() and sv_a.meta_state.attune_state().refresh_level == 1
|
||||
and int(mp_a.attune_camp_status()["refresh"]["charges"]) == 1)
|
||||
mp_a.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 21})
|
||||
Kit.check("完成 1 关 → Slots 恒 0(旧「按击杀 Boss 次数自动涨」条件已删——槽位唯一来源=树点亮)且 Boss 击杀总数=1(计数域保留)〔天赋树批 V14〕",
|
||||
Attunement.slots(_db, sv_a) == 0 and sv_a.boss_kill_count() == 1)
|
||||
sv_a.set_tree_level("att_socket1", 1)
|
||||
Attunement.slots(_db, sv_a) == 0 and sv_a.meta_state.career_state().boss_kill_count == 1)
|
||||
sv_a.meta_state.set_tree_level("att_socket1", 1)
|
||||
Kit.check("树上点亮同调位 1 → Slots=1(槽位收编树节点——Q2 裁甲)",
|
||||
Attunement.slots(_db, sv_a) == 1)
|
||||
Kit.check("稀有件入普通格拒绝(件档 rank 3 > 装入位序 1——契约 2 档位格校验)〔秘宝批 V5〕",
|
||||
@@ -279,7 +279,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
Kit.check("卸下恒可再装配(自卸自装——营地换装=冒险开始前合法面,装配不可变只锁运行中)〔秘宝批换主语〕",
|
||||
mp_a.toggle_attunement("morning_order") and mp_a.toggle_attunement("morning_order"))
|
||||
var sv_a2 = _meta_new_save(_META_SMOKE_PATHS[7])
|
||||
sv_a2.set_attunement_equipped(["morning_order"])
|
||||
sv_a2.meta_state.set_attunement_equipped(["morning_order"])
|
||||
var mp_a2 = load("res://core/meta_progression.gd").new(_db, sv_a2)
|
||||
Kit.check("装配关系读回持久(reload 后投影一致——写新件 id)〔秘宝批 V15〕",
|
||||
(mp_a2.attune_camp_status()["equipped"] as Array).has("morning_order"))
|
||||
@@ -305,9 +305,9 @@ func _meta_settle_and_workshop() -> void:
|
||||
# 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params
|
||||
# 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源)
|
||||
var sv_e = _meta_new_save(_META_SMOKE_PATHS[8])
|
||||
sv_e.set_attunement_equipped(["morning_order"])
|
||||
sv_e.set_attune_refresh_level(2)
|
||||
sv_e.set_fork_side("fg_posture", "a")
|
||||
sv_e.meta_state.set_attunement_equipped(["morning_order"])
|
||||
sv_e.meta_state.set_attune_refresh_level(2)
|
||||
sv_e.meta_state.set_fork_side("fg_posture", "a")
|
||||
var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e)
|
||||
var rules_e: Array = mp_e.tree_rule_keys()
|
||||
Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)",
|
||||
@@ -315,7 +315,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
|
||||
{"treasure_unlocked": {"wide_scout_band": true}, "shop_mode": "pool"})
|
||||
Kit.check("主菜单开新冒险参数面:同调装配 = 存档装配集(营地装了什么开局就带什么)",
|
||||
(p_new.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array))
|
||||
(p_new.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array))
|
||||
Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)",
|
||||
int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges())
|
||||
Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)",
|
||||
@@ -325,7 +325,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
|
||||
{"treasure_unlocked": {}})
|
||||
Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)",
|
||||
(p_resume.get("attunement", []) as Array) == (sv_e.attunement_equipped() as Array)
|
||||
(p_resume.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array)
|
||||
and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges()
|
||||
and (p_resume.get("tree_rules", []) as Array) == rules_e)
|
||||
# 续玩路径 tree_rules 受理断言:直呼 runner 续玩参数面
|
||||
@@ -349,7 +349,7 @@ func _meta_injection() -> void:
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
# 树生命淬炼注入(公式:基础值+树淬炼 flat+锻造路线加值位预留 0——2 级 = +10;
|
||||
# 注入走调试口径直置等级〔两分法——购买链已在 _smoke_tree 覆盖,此处隔离行门槛干扰〕)
|
||||
sv.set_tree_level("sp_hp", 2)
|
||||
sv.meta_state.set_tree_level("sp_hp", 2)
|
||||
Kit.check("生命淬炼 2 级 → hero_override().max_hp == Σ 节点 effects×等级(验收4 换代〔天赋树批 V12〕)",
|
||||
int(mp.hero_override().get("max_hp", -1)) == 2 * int(_db.tree_node("sp_hp").get("effects", {}).get("max_hp", 0)))
|
||||
Kit.check("攻/AC 树零来源(契约 9——树表零 atk/armor_class 行,override 二轴恒 0+预留位 0)",
|
||||
@@ -406,26 +406,26 @@ func _smoke_forge_axes() -> void:
|
||||
var sv2 = _meta_new_save(_META_SMOKE_PATHS[13])
|
||||
var mp2 = load("res://core/meta_progression.gd").new(_db, sv2)
|
||||
# V2①:长剑首购=20(0 级位几何价)、等级 0→1、扣币即盘、reload 持久
|
||||
sv2.add_keystone(100)
|
||||
sv2.meta_state._add_keystone(100)
|
||||
Kit.check("长剑锻造首购:扣 20 钥石、等级 0→1(Q1——第一级=获得装备=首个 +1)〔锻造工坊批 V2〕",
|
||||
mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 1
|
||||
and sv2.keystone_balance() == 80)
|
||||
mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 1
|
||||
and sv2.meta_state.keystone_balance() == 80)
|
||||
var sv2r = _meta_new_save(_META_SMOKE_PATHS[13])
|
||||
Kit.check("购买持久(reload 后等级与余额不变——即时写盘)〔锻造工坊批 V2〕",
|
||||
sv2r.forge_route_level("sword") == 1 and sv2r.keystone_balance() == 80)
|
||||
sv2r.meta_state.forge_state().levels.get("sword", 0) == 1 and sv2r.meta_state.keystone_balance() == 80)
|
||||
# V2②:二级=30(1 级位);六路线独立(买长剑不动轻甲)
|
||||
Kit.check("长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕",
|
||||
mp2.forge_buy_level("sword") and sv2.forge_route_level("sword") == 2
|
||||
and sv2.keystone_balance() == 50 and sv2.forge_route_level("light") == 0)
|
||||
mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 2
|
||||
and sv2.meta_state.keystone_balance() == 50 and sv2.meta_state.forge_state().levels.get("light", 0) == 0)
|
||||
# V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化)
|
||||
var sv2d = _meta_new_save(_META_SMOKE_PATHS[14])
|
||||
var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d)
|
||||
var fr_dry: Dictionary = sv2d.forge_routes_snapshot()
|
||||
var fr_dry: Dictionary = sv2d.meta_state.forge_state()
|
||||
Kit.check("余额不足购买拒绝且零副作用(0 余额——三域零变化)〔锻造工坊批 V2〕",
|
||||
not mp2d.forge_buy_level("sword") and sv2d.keystone_balance() == 0
|
||||
and sv2d.forge_routes_snapshot() == fr_dry)
|
||||
not mp2d.forge_buy_level("sword") and sv2d.meta_state.keystone_balance() == 0
|
||||
and sv2d.meta_state.forge_state() == fr_dry)
|
||||
# V2④:已满级拒购(cap 闸);未解锁拒购
|
||||
sv2.set_forge_route_level("sword", 5)
|
||||
sv2.meta_state.set_forge_route_level("sword", 5)
|
||||
Kit.check("已满级拒购/未解锁拒购(cap 闸+解锁闸——零副作用)〔锻造工坊批 V2〕",
|
||||
not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear"))
|
||||
# V3① 通道一:练满前一档(长剑 cap)→ 长枪/法杖派生解锁(零收费);轻甲未满→甲线仍锁
|
||||
@@ -433,40 +433,40 @@ func _smoke_forge_axes() -> void:
|
||||
mp2.forge_route_unlocked("spear") and mp2.forge_route_unlocked("staff")
|
||||
and not mp2.forge_route_unlocked("heavy") and not mp2.forge_route_unlocked("ward"))
|
||||
Kit.check("派生解锁后长枪可购(首购 20——余额 50−20)〔锻造工坊批 V3〕",
|
||||
mp2.forge_buy_level("spear") and sv2.forge_route_level("spear") == 1
|
||||
and sv2.keystone_balance() == 30)
|
||||
mp2.forge_buy_level("spear") and sv2.meta_state.forge_state().levels.get("spear", 0) == 1
|
||||
and sv2.meta_state.keystone_balance() == 30)
|
||||
# V3② 通道二:一次性解锁价直购(未练满也可买——两通道互不影响)
|
||||
var sv3 = _meta_new_save(_META_SMOKE_PATHS[15])
|
||||
var mp3 = load("res://core/meta_progression.gd").new(_db, sv3)
|
||||
sv3.add_keystone(500)
|
||||
sv3.meta_state._add_keystone(500)
|
||||
Kit.check("通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕",
|
||||
not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
|
||||
and sv3.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"))
|
||||
and sv3.meta_state.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"))
|
||||
# V3③ 直购后练满不重复收(练满通道=派生零收费;已解锁再直购=拒绝)
|
||||
sv3.set_forge_route_level("light", 5)
|
||||
sv3.meta_state.set_forge_route_level("light", 5)
|
||||
Kit.check("直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕",
|
||||
mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
|
||||
and sv3.keystone_balance() == 300)
|
||||
and sv3.meta_state.keystone_balance() == 300)
|
||||
# V3④ 不设串联门槛(法杖直购开——长枪零状态不阻;dry 档充值走直购)
|
||||
sv2d.add_keystone(500)
|
||||
sv2d.meta_state._add_keystone(500)
|
||||
Kit.check("不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕",
|
||||
mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff")
|
||||
and sv2d.forge_route_level("spear") == 0 and not mp2d.forge_route_unlocked("spear"))
|
||||
and sv2d.meta_state.forge_state().levels.get("spear", 0) == 0 and not mp2d.forge_route_unlocked("spear"))
|
||||
# V4:任一购买/解锁只扣钥石——GP 侧零触碰(meta 域无 GP 键=域界扫描同款;此处钉余额差=当级价)
|
||||
var ks4: int = sv3.keystone_balance()
|
||||
var ks4: int = sv3.meta_state.keystone_balance()
|
||||
Kit.check("两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕",
|
||||
mp3.forge_buy_level("ward") and sv3.keystone_balance() == ks4 - 20
|
||||
and sv3.forge_route_level("ward") == 1)
|
||||
mp3.forge_buy_level("ward") and sv3.meta_state.keystone_balance() == ks4 - 20
|
||||
and sv3.meta_state.forge_state().levels.get("ward", 0) == 1)
|
||||
# V6①:营地切换=写存档域+免费(余额零动);旧路线练度保留(只不生效)
|
||||
var ks6: int = sv3.keystone_balance()
|
||||
var ks6: int = sv3.meta_state.keystone_balance()
|
||||
Kit.check("选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕",
|
||||
mp3.forge_select_armor("ward") and sv3.forge_armor() == "ward"
|
||||
and sv3.keystone_balance() == ks6)
|
||||
mp3.forge_select_armor("ward") and sv3.meta_state.forge_state().armor == "ward"
|
||||
and sv3.meta_state.keystone_balance() == ks6)
|
||||
Kit.check("切换后旧路线练度保留(轻甲 5 级不丢——回家待命只不生效)〔锻造工坊批 V6〕",
|
||||
sv3.forge_route_level("light") == 5)
|
||||
sv3.meta_state.forge_state().levels.get("light", 0) == 5)
|
||||
# V6②:未解锁路线拒选零副作用(dry 档:长枪未解锁)
|
||||
Kit.check("未解锁路线拒选零副作用〔锻造工坊批 V6〕",
|
||||
not mp2d.forge_select_weapon("spear") and sv2d.forge_weapon() == "sword")
|
||||
not mp2d.forge_select_weapon("spear") and sv2d.meta_state.forge_state().weapon == "sword")
|
||||
# V6③:类型轴读数随选中走(切换后读 ward;未解锁选中态防御回退默认值)
|
||||
Kit.check("类型轴当前生效值随选中走(ward;未解锁档回退默认 normal/light)〔锻造工坊批 V5/V6〕",
|
||||
mp3.hero_armor_type() == "ward" and mp2d.hero_attack_type() == "normal"
|
||||
@@ -480,7 +480,7 @@ func _smoke_forge_axes() -> void:
|
||||
Kit.check("build_adventure_params 注入默认值 normal/light(树钥匙未点亮——新档初始持有态)〔锻造工坊批 V5〕",
|
||||
String(bp_params.get("attack_type", "")) == "normal"
|
||||
and String(bp_params.get("armor_type", "")) == "light")
|
||||
sv4.add_keystone(1000)
|
||||
sv4.meta_state._add_keystone(1000)
|
||||
Kit.check("长枪直购解锁+首购+选中(注入面夹具——200+20 钥石)",
|
||||
mp4.forge_buy_unlock("spear") and mp4.forge_buy_level("spear") and mp4.forge_select_weapon("spear"))
|
||||
var bp_sel: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv4, mp4)
|
||||
@@ -493,7 +493,7 @@ func _smoke_forge_axes() -> void:
|
||||
and int(mp4.hero_override().get("max_hp", -1)) == 0
|
||||
and int(mp4.hero_override().get("armor_class", -1)) == 0
|
||||
and not mp4.hero_override().has("hit"))
|
||||
sv4.set_forge_route_level("sword", 3) # 长剑练度待命(未选中)
|
||||
sv4.meta_state.set_forge_route_level("sword", 3) # 长剑练度待命(未选中)
|
||||
Kit.check("换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕",
|
||||
int(mp4.hero_override().get("atk", -1)) == 1)
|
||||
Kit.check("切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕",
|
||||
@@ -501,7 +501,7 @@ func _smoke_forge_axes() -> void:
|
||||
# V7 三层 AC:重甲(基础 12)+路线 Lv1 加值+局内 0——经参数面起冒险断 runner 生效值
|
||||
var sv5 = _meta_new_save(_META_SMOKE_PATHS[17])
|
||||
var mp5 = load("res://core/meta_progression.gd").new(_db, sv5)
|
||||
sv5.add_keystone(1000)
|
||||
sv5.meta_state._add_keystone(1000)
|
||||
Kit.check("重甲直购解锁+首购+选中(三层 AC 夹具)",
|
||||
mp5.forge_buy_unlock("heavy") and mp5.forge_buy_level("heavy") and mp5.forge_select_armor("heavy"))
|
||||
var adr5 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(77))
|
||||
@@ -582,9 +582,9 @@ func _smoke_tree() -> void:
|
||||
# 购买面(V2 主干)
|
||||
var sv = _meta_new_save(_META_SMOKE_PATHS[8])
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.add_keystone(1000)
|
||||
sv.meta_state._add_keystone(1000)
|
||||
Kit.check("树购买受理(脊柱生命 L1——树全钥石单币)",
|
||||
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 1)
|
||||
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 1)
|
||||
Kit.check("行门槛:第 2 级不可购(总等级 1 < 4——一线一节点主语,买第 N 级需全树总投入 ≥4×(N−1))〔天赋树批 V2〕",
|
||||
not mp.tree_can_buy("sp_hp"))
|
||||
Kit.check("命中 L1/锻造钥匙(free 无前置=初始零购零币可点亮)/同调位 1(根节点无前置)受理(总等级 1→4)",
|
||||
@@ -592,33 +592,33 @@ func _smoke_tree() -> void:
|
||||
Kit.check("锻造工坊钥匙零币(free)", int(_db.tree_node("forge_workshop").get("price", -1)) == 0)
|
||||
Kit.check("行门槛恰达开放(总等级 4 ≥ 4×1)", mp.tree_can_buy("sp_hp"))
|
||||
Kit.check("第 2 级购买受理(门槛恰达当刻开放)",
|
||||
mp.tree_buy("sp_hp") and sv.tree_level("sp_hp") == 2)
|
||||
mp.tree_buy("sp_hp") and sv.meta_state.tree_state().nodes.get("sp_hp", 0) == 2)
|
||||
# Fork 一节点双态(V9)
|
||||
sv.add_keystone(100)
|
||||
sv.meta_state._add_keystone(100)
|
||||
Kit.check("Fork 购买=解锁+默认 A 态(一节点双态——单行承载)",
|
||||
mp.tree_buy_fork("fork_posture") and sv.tree_level("fork_posture") == 1 and sv.fork_side("fg_posture") == "a")
|
||||
mp.tree_buy_fork("fork_posture") and sv.meta_state.tree_state().nodes.get("fork_posture", 0) == 1 and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "a")
|
||||
Kit.check("已解锁 Fork 重复购买拒绝(cap 1)", not mp.tree_can_buy("fork_posture"))
|
||||
Kit.check("同态重复切换零操作(false)", not mp.tree_toggle_fork("fg_posture", "a"))
|
||||
Kit.check("未解锁 Fork 切换拒绝(解锁唯一入口=购买面)", not mp.tree_toggle_fork("fg_sense", "b"))
|
||||
var bal_pre_toggle: int = sv.keystone_balance()
|
||||
var bal_pre_toggle: int = sv.meta_state.keystone_balance()
|
||||
Kit.check("零币切态 b(购买解锁后 A/B 可切)",
|
||||
mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b")
|
||||
Kit.check("切态零消费(钥石余额差分不动)", int(sv.keystone_balance()) == bal_pre_toggle)
|
||||
mp.tree_toggle_fork("fg_posture", "b") and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b")
|
||||
Kit.check("切态零消费(钥石余额差分不动)", int(sv.meta_state.keystone_balance()) == bal_pre_toggle)
|
||||
Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)",
|
||||
(mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike"))
|
||||
Kit.check("切回 A 态键集随换(六键词表在册——V11 校验面归单元段)",
|
||||
mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike"))
|
||||
# 彩蛋特化觉醒(V8)
|
||||
Kit.check("彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)", not mp.tree_can_buy("easter_ley"))
|
||||
Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.tree_level("easter_ley") == 0)
|
||||
sv.set_tree_level("sp_hp", 4)
|
||||
sv.set_tree_level("sp_hit", 4)
|
||||
sv.set_tree_level("att_socket2", 1)
|
||||
Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 0)
|
||||
sv.meta_state.set_tree_level("sp_hp", 4)
|
||||
sv.meta_state.set_tree_level("sp_hit", 4)
|
||||
sv.meta_state.set_tree_level("att_socket2", 1)
|
||||
mp.tree_buy("att_socket3") # 经购买口触发 _check_easter_egg
|
||||
Kit.check("两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)", sv.tree_level("easter_ley") == 1)
|
||||
var bal_now: int = sv.keystone_balance()
|
||||
Kit.check("两线拉满+同调位全解锁 → 地脉共鸣自动点亮(零币特化判定)", sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 1)
|
||||
var bal_now: int = sv.meta_state.keystone_balance()
|
||||
mp.tree_buy("sp_hp")
|
||||
Kit.check("觉醒零消费(钥石余额不动)", int(sv.keystone_balance()) == bal_now)
|
||||
Kit.check("觉醒零消费(钥石余额不动)", int(sv.meta_state.keystone_balance()) == bal_now)
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ func _init() -> void:
|
||||
# ① 无参实例(autoload 实例化形态)=无激活态+默认合法空态(sv2 类级)
|
||||
func _test_no_arg_instance_has_no_activation() -> void:
|
||||
var sm := SaveManagerScript.new()
|
||||
Kit.check_quiet(sm._path == "" and sm.active_slot() == 0 and not sm.has_write_target(),
|
||||
Kit.check_quiet(sm._path == "" and sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
|
||||
"sv2 无参实例无激活态(路径空/激活号 0/写盘目标空)〔卡① sv2〕")
|
||||
Kit.check_quiet(sm.keystone_balance() == 0 and not sm.has_breakpoint()
|
||||
and sm.adventure_total() == 0 and sm.adventure_cleared() == 0,
|
||||
Kit.check_quiet(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint()
|
||||
and sm.meta_state.career_state().adventure_total == 0 and sm.meta_state.career_state().adventure_cleared == 0,
|
||||
"sv2 内存=默认合法空态(钥石/断点/总局数/冒险成功数全零值——访问器 fail-safe)〔卡① sv2〕")
|
||||
# attach_path 注入任意隔离路径=注入即激活(契约 7 单路径语义——隔离面行为零变化)
|
||||
sm.attach_path("user://smoke_boot_noact_iso.json")
|
||||
Kit.check_quiet(sm.has_write_target() and sm.keystone_balance() == 0,
|
||||
sm.slots.attach_path("user://smoke_boot_noact_iso.json")
|
||||
Kit.check_quiet(sm.slots.has_write_target() and sm.meta_state.keystone_balance() == 0,
|
||||
"sv2 attach_path 注入即激活(写盘目标在位、数据回默认空态)〔卡① sv2/契约7〕")
|
||||
if FileAccess.file_exists("user://smoke_boot_noact_iso.json"):
|
||||
DirAccess.remove_absolute("user://smoke_boot_noact_iso.json")
|
||||
@@ -43,7 +43,7 @@ func _test_write_rejected_without_target() -> void:
|
||||
var sm := SaveManagerScript.new()
|
||||
var before := {}
|
||||
for i in range(1, 4):
|
||||
var p: String = sm.slot_path(i)
|
||||
var p: String = sm.slots.slot_path(i)
|
||||
before[i] = [FileAccess.file_exists(p),
|
||||
FileAccess.get_file_as_bytes(p) if FileAccess.file_exists(p) else PackedByteArray(),
|
||||
FileAccess.get_modified_time(p) if FileAccess.file_exists(p) else 0]
|
||||
@@ -55,7 +55,7 @@ func _test_write_rejected_without_target() -> void:
|
||||
"sv3 无激活写盘拒绝(write_to_disk=false)〔卡① sv3〕")
|
||||
var unchanged := true
|
||||
for i in range(1, 4):
|
||||
var p: String = sm.slot_path(i)
|
||||
var p: String = sm.slots.slot_path(i)
|
||||
var now_state: Array = [FileAccess.file_exists(p),
|
||||
FileAccess.get_file_as_bytes(p) if FileAccess.file_exists(p) else PackedByteArray(),
|
||||
FileAccess.get_modified_time(p) if FileAccess.file_exists(p) else 0]
|
||||
@@ -88,11 +88,11 @@ func _test_settle_chain_write_rejected() -> void:
|
||||
for sp in ["res://.tmp", "res://.tmp.tmp"]:
|
||||
if FileAccess.file_exists(sp):
|
||||
DirAccess.remove_absolute(sp)
|
||||
Kit.check_quiet(not sm.has_write_target() and not sm.write_to_disk(),
|
||||
Kit.check_quiet(not sm.slots.has_write_target() and not sm.write_to_disk(),
|
||||
"sv3 链前置:无激活态在位且直写被拒〔卡① sv3〕")
|
||||
meta.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 3})
|
||||
Kit.check_quiet(not sm.has_write_target() and sm.adventure_total() == 1,
|
||||
Kit.check_quiet(not sm.slots.has_write_target() and sm.meta_state.career_state().adventure_total == 1,
|
||||
"sv3 生产链:settle_adventure 落账内存(adventure_total=1)而写盘仍无目标(被拒不落盘)〔卡① sv3〕")
|
||||
Kit.check_quiet(not FileAccess.file_exists("res://.tmp")
|
||||
and not FileAccess.file_exists("res://.tmp.tmp"),
|
||||
@@ -102,11 +102,11 @@ func _test_settle_chain_write_rejected() -> void:
|
||||
# ③ 越界槽号三原语拒绝+窥视越界返回不可读(原语边界 fail-safe)
|
||||
func _test_out_of_range_slots_rejected() -> void:
|
||||
var sm := SaveManagerScript.new()
|
||||
sm.attach_slot_template("user://smoke_boot_noact_%d.json")
|
||||
Kit.check_quiet(not sm.activate_slot(0) and not sm.activate_slot(4)
|
||||
and not sm.create_slot(0) and not sm.create_slot(4)
|
||||
and not sm.delete_slot(0) and not sm.delete_slot(4),
|
||||
sm.slots.attach_slot_template("user://smoke_boot_noact_%d.json")
|
||||
Kit.check_quiet(not sm.slots.activate_slot(0) and not sm.slots.activate_slot(4)
|
||||
and not sm.slots.create_slot(0) and not sm.slots.create_slot(4)
|
||||
and not sm.slots.delete_slot(0) and not sm.slots.delete_slot(4),
|
||||
"原语边界:槽号 0/4 全拒绝(位数写死 3——契约 1)〔卡① sv1〕")
|
||||
var out: Dictionary = sm.peek_slot_summary(4)
|
||||
var out: Dictionary = sm.slots.peek_slot_summary(4)
|
||||
Kit.check_quiet(not bool(out.get("readable", true)),
|
||||
"原语边界:越界窥视返回不可读标记〔卡① sv7〕")
|
||||
@@ -67,55 +67,55 @@ func _smoke_save() -> void:
|
||||
|
||||
# ① 往返:全访问器写非默认值 → write → 新实例 load → 逐域逐值相等
|
||||
var w = sm_script.new(paths[0])
|
||||
w.add_keystone(7)
|
||||
w.set_tree_level("sp_hp", 2)
|
||||
w.set_tree_level("sp_hit", 1)
|
||||
w.set_fork_side("fg_posture", "b") # 树域写入
|
||||
w.set_forge_route_level("spear", 3) # 锻造工坊域写入(等级+直购解锁+选中标记)
|
||||
w.add_forge_unlock("spear")
|
||||
w.set_forge_weapon("spear")
|
||||
w.set_forge_armor("ward")
|
||||
w.unlock_item("probe_compass")
|
||||
w.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
w.meta_state._add_keystone(7)
|
||||
w.meta_state.set_tree_level("sp_hp", 2)
|
||||
w.meta_state.set_tree_level("sp_hit", 1)
|
||||
w.meta_state.set_fork_side("fg_posture", "b") # 树域写入
|
||||
w.meta_state.set_forge_route_level("spear", 3) # 锻造工坊域写入(等级+直购解锁+选中标记)
|
||||
w.meta_state.add_forge_unlock("spear")
|
||||
w.meta_state.set_forge_weapon("spear")
|
||||
w.meta_state.set_forge_armor("ward")
|
||||
w.meta_state.unlock_item("probe_compass")
|
||||
w.meta_state.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
"stopped_at": 3, "keystone_total": 3, "master_seed": 99})
|
||||
Kit.check("apply 原语入账(钥石 7+3、关卡完成次数 1/1/0)",
|
||||
w.keystone_balance() == 10 and w.clear_count(1) == 1 and w.clear_count(2) == 1 and w.clear_count(3) == 0)
|
||||
w.meta_state.keystone_balance() == 10 and w.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and w.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and w.meta_state.career_state().clear_counts.get(str(3), 0) == 0)
|
||||
Kit.check("首杀标记随关卡完成置位(1/2 有、3 无)",
|
||||
w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3))
|
||||
w.meta_state.career_state().boss_first_kill.has(str(1)) and w.meta_state.career_state().boss_first_kill.has(str(2)) and not w.meta_state.career_state().boss_first_kill.has(str(3)))
|
||||
Kit.check("生涯终身计数随 apply 自增恰一(中止局:总局数 1、冒险成功局数 0——迭代 18 卡④)",
|
||||
w.adventure_total() == 1 and w.adventure_cleared() == 0)
|
||||
w.meta_state.career_state().adventure_total == 1 and w.meta_state.career_state().adventure_cleared == 0)
|
||||
Kit.check("写盘成功", w.write_to_disk())
|
||||
var r = sm_script.new(paths[0])
|
||||
Kit.check("读回成功", r.load_from_disk())
|
||||
Kit.check("钥石余额往返相等(10)", r.keystone_balance() == 10)
|
||||
Kit.check("钥石余额往返相等(10)", r.meta_state.keystone_balance() == 10)
|
||||
Kit.check("树域往返相等(tree_nodes/fork_sides——迭代 21 换代承载)",
|
||||
r.tree_level("sp_hp") == 2 and r.tree_level("sp_hit") == 1 and r.fork_side("fg_posture") == "b")
|
||||
r.meta_state.tree_state().nodes.get("sp_hp", 0) == 2 and r.meta_state.tree_state().nodes.get("sp_hit", 0) == 1 and r.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b")
|
||||
Kit.check("锻造工坊域往返相等(levels/unlocked/weapon/armor——迭代 23 T5)〔锻造工坊批 V8〕",
|
||||
r.forge_route_level("spear") == 3 and r.forge_route_level("sword") == 0
|
||||
and r.forge_unlocked_purchases() == ["spear"]
|
||||
and r.forge_weapon() == "spear" and r.forge_armor() == "ward")
|
||||
Kit.check("道具解锁往返相等", r.item_unlocked("probe_compass") and not r.item_unlocked("rope"))
|
||||
Kit.check("关卡完成次数表往返相等", r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0)
|
||||
r.meta_state.forge_state().levels.get("spear", 0) == 3 and r.meta_state.forge_state().levels.get("sword", 0) == 0
|
||||
and r.meta_state.forge_state().unlocked == ["spear"]
|
||||
and r.meta_state.forge_state().weapon == "spear" and r.meta_state.forge_state().armor == "ward")
|
||||
Kit.check("道具解锁往返相等", r.meta_state.item_unlocked("probe_compass") and not r.meta_state.item_unlocked("rope"))
|
||||
Kit.check("关卡完成次数表往返相等", r.meta_state.career_state().clear_counts.get(str(1), 0) == 1 and r.meta_state.career_state().clear_counts.get(str(2), 0) == 1 and r.meta_state.career_state().clear_counts.get(str(3), 0) == 0)
|
||||
Kit.check("首杀标记往返相等",
|
||||
r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3))
|
||||
Kit.check("生涯终身计数往返相等(1/0)", r.adventure_total() == 1 and r.adventure_cleared() == 0)
|
||||
var s0: Dictionary = r.recent_summaries()[0]
|
||||
r.meta_state.career_state().boss_first_kill.has(str(1)) and r.meta_state.career_state().boss_first_kill.has(str(2)) and not r.meta_state.career_state().boss_first_kill.has(str(3)))
|
||||
Kit.check("生涯终身计数往返相等(1/0)", r.meta_state.career_state().adventure_total == 1 and r.meta_state.career_state().adventure_cleared == 0)
|
||||
var s0: Dictionary = r.meta_state.career_state().recent_summaries[0]
|
||||
Kit.check("冒险摘要往返相等(1 条,seed 99)",
|
||||
r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3)
|
||||
r.meta_state.career_state().recent_summaries.size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3)
|
||||
|
||||
# ② 新档合法:不存在路径 load → true + 全零默认值
|
||||
var f2 = sm_script.new(paths[1])
|
||||
Kit.check("新档合法态(load true + 全零默认)",
|
||||
f2.load_from_disk() and f2.keystone_balance() == 0 and f2.tree_level("sp_hp") == 0
|
||||
and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty())
|
||||
f2.load_from_disk() and f2.meta_state.keystone_balance() == 0 and f2.meta_state.tree_state().nodes.get("sp_hp", 0) == 0
|
||||
and not f2.meta_state.item_unlocked("anything") and f2.meta_state.career_state().clear_counts.get(str(1), 0) == 0 and f2.meta_state.career_state().recent_summaries.is_empty())
|
||||
Kit.check("新档生涯终身计数缺省全零(迭代 18 卡④两新域——白板存档完整可玩)",
|
||||
f2.adventure_total() == 0 and f2.adventure_cleared() == 0)
|
||||
f2.meta_state.career_state().adventure_total == 0 and f2.meta_state.career_state().adventure_cleared == 0)
|
||||
|
||||
# ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外)
|
||||
var f3 = sm_script.new(paths[2])
|
||||
f3.add_keystone(5)
|
||||
Kit.check("余额 5 花 8 拒绝且余额不动", not f3.spend_keystone(8) and f3.keystone_balance() == 5)
|
||||
Kit.check("余额内消费成功(5→2)", f3.spend_keystone(3) and f3.keystone_balance() == 2)
|
||||
f3.meta_state._add_keystone(5)
|
||||
Kit.check("余额 5 花 8 拒绝且余额不动", not f3.meta_state.spend_keystone(8) and f3.meta_state.keystone_balance() == 5)
|
||||
Kit.check("余额内消费成功(5→2)", f3.meta_state.spend_keystone(3) and f3.meta_state.keystone_balance() == 2)
|
||||
|
||||
# ⑤ 高版本拒读:load false、原文件字节不动、后续写盘拒绝且文件仍字节不动(契约 4)
|
||||
var hv_text := "{\"version\": 99, \"meta\": {\"keystone_balance\": 1}}"
|
||||
@@ -135,7 +135,7 @@ func _smoke_save() -> void:
|
||||
fj.close()
|
||||
var f6 = sm_script.new(paths[4])
|
||||
Kit.check("损坏档兜底(默认数据继续)",
|
||||
f6.load_from_disk() and f6.keystone_balance() == 0 and f6.recent_summaries().is_empty())
|
||||
f6.load_from_disk() and f6.meta_state.keystone_balance() == 0 and f6.meta_state.career_state().recent_summaries.is_empty())
|
||||
Kit.check("坏档备份 .corrupt.bak 存在", FileAccess.file_exists(paths[4] + ".corrupt.bak"))
|
||||
Kit.check("备份内容 = 原坏档字节",
|
||||
FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk)
|
||||
@@ -149,7 +149,7 @@ func _smoke_save() -> void:
|
||||
fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12}))
|
||||
fm.close()
|
||||
Kit.check("v0 旧档经迁移链入账(keystone_old 12 → 余额 12)",
|
||||
f7.load_from_disk() and f7.keystone_balance() == 12)
|
||||
f7.load_from_disk() and f7.meta_state.keystone_balance() == 12)
|
||||
Kit.check("迁移链 0→1(测试钩)→7(生产) 写盘 version == 7(v7=锻造工坊域,迭代 23 T5——v6=生涯终身计数域迭代 18 卡④)",
|
||||
f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 7)
|
||||
|
||||
@@ -167,9 +167,9 @@ func _smoke_save() -> void:
|
||||
# ⑩ 摘要 cap:12 份 → 保留 10、最新在前
|
||||
var f10 = sm_script.new(paths[7])
|
||||
for i in range(12):
|
||||
f10.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 0,
|
||||
f10.meta_state.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 0,
|
||||
"stopped_at": 0, "keystone_total": 0, "master_seed": i})
|
||||
var arr: Array = f10.recent_summaries()
|
||||
var arr: Array = f10.meta_state.career_state().recent_summaries
|
||||
Kit.check("摘要 cap 10(12 份 → 10 条)", arr.size() == 10)
|
||||
Kit.check("最新在前(头 seed 11 / 尾 seed 2)",
|
||||
int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2)
|
||||
@@ -227,7 +227,7 @@ func _smoke_save() -> void:
|
||||
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
|
||||
fold.close()
|
||||
Kit.check("v1 旧档迁移 v7:断点域补空(合法空态,无断点域不炸)",
|
||||
fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint())
|
||||
fo.load_from_disk() and fo.meta_state.keystone_balance() == 4 and not fo.has_breakpoint())
|
||||
Kit.check("v1 旧档迁移后写盘 version == 7(v1→v7 链式——锻造工坊域 2026-10-12)",
|
||||
fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 7)
|
||||
|
||||
@@ -241,7 +241,7 @@ func _smoke_save() -> void:
|
||||
"visited_ids": []}}))
|
||||
fv2f.close()
|
||||
Kit.check("v2 旧档迁移 v3:断点域与 meta 域原样保留(缺新附随件=合法空态)",
|
||||
fv2.load_from_disk() and fv2.has_breakpoint() and fv2.keystone_balance() == 7)
|
||||
fv2.load_from_disk() and fv2.has_breakpoint() and fv2.meta_state.keystone_balance() == 7)
|
||||
Kit.check("v2 旧断点字段零丢失(向后兼容——breakpoint 内容不因 schema 升版重写)",
|
||||
int((fv2.breakpoint_snapshot() as Dictionary).get("master_seed", 0)) == 777)
|
||||
var adr_v2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777))
|
||||
@@ -255,22 +255,22 @@ func _smoke_save() -> void:
|
||||
fv5u.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 9}}))
|
||||
fv5u.close()
|
||||
Kit.check("v5 旧档升 v7:计数域缺省补零且既有余额原样(迁移只增字段不改值——v5→v6→v7 链式)",
|
||||
f13.load_from_disk() and f13.adventure_total() == 0 and f13.adventure_cleared() == 0
|
||||
and f13.keystone_balance() == 9)
|
||||
f13.load_from_disk() and f13.meta_state.career_state().adventure_total == 0 and f13.meta_state.career_state().adventure_cleared == 0
|
||||
and f13.meta_state.keystone_balance() == 9)
|
||||
Kit.check("升档幂等:v7 档写盘再载零漂移(重复迁移零副作用——锻造工坊域同批补齐)",
|
||||
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)) == 7
|
||||
and f13.adventure_total() == 0 and f13.adventure_cleared() == 0)
|
||||
and f13.meta_state.career_state().adventure_total == 0 and f13.meta_state.career_state().adventure_cleared == 0)
|
||||
var f13b = sm_script.new(paths[12])
|
||||
f13b.load_from_disk()
|
||||
f13b.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
|
||||
f13b.meta_state.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
|
||||
"stopped_at": 0, "keystone_total": 0, "master_seed": 1})
|
||||
Kit.check("收束自增恰一·冒险成功局(三关全通 cleared=true:总局数+1 且冒险成功局数+1)",
|
||||
f13b.adventure_total() == 1 and f13b.adventure_cleared() == 1)
|
||||
f13b.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
|
||||
f13b.meta_state.career_state().adventure_total == 1 and f13b.meta_state.career_state().adventure_cleared == 1)
|
||||
f13b.meta_state.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 2})
|
||||
Kit.check("收束自增恰一·中止局(放弃局:仅总局数+1,冒险成功局数不动)",
|
||||
f13b.adventure_total() == 2 and f13b.adventure_cleared() == 1)
|
||||
f13b.meta_state.career_state().adventure_total == 2 and f13b.meta_state.career_state().adventure_cleared == 1)
|
||||
|
||||
# ⑮ 旧档破坏性废弃(契约 4:换代前结构检测=树域废弃重置,禁迁移禁折算)〔V16〕
|
||||
var f15 = sm_script.new(paths[13])
|
||||
@@ -281,12 +281,12 @@ func _smoke_save() -> void:
|
||||
"fork_sides": {"fg_posture": "a", "fg_mastery": "b"}}}))
|
||||
fv16.close()
|
||||
Kit.check("换代前结构存档:树域废弃重置(旧节点 id/fork 侧/forge_levels 旧域残留清退)、钥石余额零折算零变动",
|
||||
f15.load_from_disk() and f15.keystone_balance() == 33
|
||||
and f15.tree_nodes().is_empty() and f15.fork_sides().is_empty())
|
||||
f15.load_from_disk() and f15.meta_state.keystone_balance() == 33
|
||||
and f15.meta_state.tree_state().nodes.is_empty() and f15.meta_state.tree_state().fork_sides.is_empty())
|
||||
var mp16 = load("res://core/meta_progression.gd").new(_db, f15)
|
||||
f15.add_keystone(50)
|
||||
f15.meta_state._add_keystone(50)
|
||||
Kit.check("废弃重置后后续购买正常受理(无残留脏节点)",
|
||||
mp16.tree_buy("sp_hp") and f15.tree_level("sp_hp") == 1)
|
||||
mp16.tree_buy("sp_hp") and f15.meta_state.tree_state().nodes.get("sp_hp", 0) == 1)
|
||||
|
||||
# ⑯ 锻造工坊域 forge_routes(V8):v6 旧档缺键补齐=初始持有态+无特效域+域界干净
|
||||
var f16 = sm_script.new(paths[14])
|
||||
@@ -295,26 +295,26 @@ func _smoke_save() -> void:
|
||||
"tree_nodes": {"sp_hp": 1}, "tree_struct": 2, "adventure_total": 4, "adventure_cleared": 1}}))
|
||||
fv6.close()
|
||||
Kit.check("v6 旧档升 v7:既有域逐位不动(钥石/树/生涯计数零折算——迁移只增不改)〔锻造工坊批 V8〕",
|
||||
f16.load_from_disk() and f16.keystone_balance() == 21
|
||||
and f16.tree_level("sp_hp") == 1 and f16.adventure_total() == 4 and f16.adventure_cleared() == 1)
|
||||
f16.load_from_disk() and f16.meta_state.keystone_balance() == 21
|
||||
and f16.meta_state.tree_state().nodes.get("sp_hp", 0) == 1 and f16.meta_state.career_state().adventure_total == 4 and f16.meta_state.career_state().adventure_cleared == 1)
|
||||
Kit.check("v6 旧档缺键补齐=初始持有态(六路线 0 级/直购清单空/选中=sword+light)〔锻造工坊批 V8〕",
|
||||
f16.forge_route_level("sword") == 0 and f16.forge_route_level("spear") == 0
|
||||
and f16.forge_route_level("light") == 0 and f16.forge_route_level("ward") == 0
|
||||
and f16.forge_unlocked_purchases().is_empty()
|
||||
and f16.forge_weapon() == "sword" and f16.forge_armor() == "light")
|
||||
var fr_keys: Array = (f16.forge_routes_snapshot() as Dictionary).keys()
|
||||
f16.meta_state.forge_state().levels.get("sword", 0) == 0 and f16.meta_state.forge_state().levels.get("spear", 0) == 0
|
||||
and f16.meta_state.forge_state().levels.get("light", 0) == 0 and f16.meta_state.forge_state().levels.get("ward", 0) == 0
|
||||
and f16.meta_state.forge_state().unlocked.is_empty()
|
||||
and f16.meta_state.forge_state().weapon == "sword" and f16.meta_state.forge_state().armor == "light")
|
||||
var fr_keys: Array = (f16.meta_state.forge_state() as Dictionary).keys()
|
||||
fr_keys.sort()
|
||||
Kit.check("forge_routes 域键集恰四项(无特效域——满级特效本期不做,契约 7)〔锻造工坊批 V8/V11〕",
|
||||
fr_keys == ["armor", "levels", "unlocked", "weapon"])
|
||||
var lv_keys: Array = ((f16.forge_routes_snapshot() as Dictionary).get("levels", {}) as Dictionary).keys()
|
||||
var lv_keys: Array = ((f16.meta_state.forge_state() as Dictionary).get("levels", {}) as Dictionary).keys()
|
||||
lv_keys.sort()
|
||||
Kit.check("levels 六路线键集恰全(sword/spear/staff/light/heavy/ward)〔锻造工坊批 V8〕",
|
||||
lv_keys == ["heavy", "light", "spear", "staff", "sword", "ward"])
|
||||
# 值拷贝出户(改快照不渗档——本类契约)
|
||||
var snap: Dictionary = f16.forge_routes_snapshot()
|
||||
var snap: Dictionary = f16.meta_state.forge_state()
|
||||
(snap.get("levels", {}) as Dictionary)["sword"] = 99
|
||||
Kit.check("forge_routes_snapshot 值拷贝出户(改快照不渗档)〔锻造工坊批 V8〕",
|
||||
f16.forge_route_level("sword") == 0)
|
||||
f16.meta_state.forge_state().levels.get("sword", 0) == 0)
|
||||
|
||||
# ⑭ 收束链入账(真实收束挂点全链恰一:主流程 adventure_finished →
|
||||
# MetaProgression.settle_adventure → apply_adventure_summary 单口;两夹具经同链入账)
|
||||
@@ -322,11 +322,11 @@ func _smoke_save() -> void:
|
||||
mp13.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
"stopped_at": 3, "keystone_total": 0, "master_seed": 3})
|
||||
Kit.check("收束链自增·中止局(settle_adventure 真实挂点:总局数+1、冒险成功局数不动)",
|
||||
f13b.adventure_total() == 3 and f13b.adventure_cleared() == 1)
|
||||
f13b.meta_state.career_state().adventure_total == 3 and f13b.meta_state.career_state().adventure_cleared == 1)
|
||||
mp13.settle_adventure({"result": "completed", "cleared": true, "levels_cleared": 3,
|
||||
"stopped_at": 0, "keystone_total": 0, "master_seed": 4})
|
||||
Kit.check("收束链自增·冒险成功局(三关全通:总局数+1 且冒险成功局数+1——全链恰一)",
|
||||
f13b.adventure_total() == 4 and f13b.adventure_cleared() == 2)
|
||||
f13b.meta_state.career_state().adventure_total == 4 and f13b.meta_state.career_state().adventure_cleared == 2)
|
||||
|
||||
# 段尾清理(测试文件绝不残留)
|
||||
for p in stale:
|
||||
@@ -352,39 +352,39 @@ func _smoke_save_slots() -> void:
|
||||
DirAccess.remove_absolute(p)
|
||||
|
||||
var t = sm_script.new()
|
||||
t.attach_slot_template(slot_tpl)
|
||||
t.slots.attach_slot_template(slot_tpl)
|
||||
|
||||
# ① 三槽往返×3+位间隔离+进档(sv1/sv5/sv10)
|
||||
for i in range(1, 4):
|
||||
Kit.check("sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" % i, t.activate_slot(i))
|
||||
t.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
|
||||
Kit.check("sv5 进档受理·槽 %d(激活+读盘)〔卡① sv5〕" % i, t.slots.activate_slot(i))
|
||||
t.meta_state.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
|
||||
"stopped_at": 0, "keystone_total": 100 * i, "master_seed": i})
|
||||
Kit.check("sv3 激活态写盘受理·槽 %d〔卡① sv1〕" % i, t.write_to_disk())
|
||||
var t2 = sm_script.new()
|
||||
t2.attach_slot_template(slot_tpl)
|
||||
t2.slots.attach_slot_template(slot_tpl)
|
||||
for i in range(1, 4):
|
||||
Kit.check("sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" % i,
|
||||
t2.activate_slot(i) and t2.keystone_balance() == 100 * i
|
||||
and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0))
|
||||
t2.slots.activate_slot(i) and t2.meta_state.keystone_balance() == 100 * i
|
||||
and t2.meta_state.career_state().adventure_total == 1 and t2.meta_state.career_state().adventure_cleared == (1 if i == 1 else 0))
|
||||
var slot2_before := FileAccess.get_file_as_bytes(slot_tpl % 2)
|
||||
t.activate_slot(1)
|
||||
t.add_keystone(7)
|
||||
t.slots.activate_slot(1)
|
||||
t.meta_state._add_keystone(7)
|
||||
Kit.check("sv3 写盘受理·槽 1 加值后〔卡① sv1〕", t.write_to_disk())
|
||||
Kit.check("sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕",
|
||||
FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_before)
|
||||
|
||||
# ② 删档原语(sv6):删激活槽(指针连清除)→ 删非激活槽(指针保持+他槽字节不动)
|
||||
Kit.check("sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕", t.activate_slot(3))
|
||||
Kit.check("sv6 删档受理·激活槽 3〔卡① sv6〕", t.delete_slot(3))
|
||||
Kit.check("sv5 切激活至槽 3(删激活槽夹具前置)〔卡① sv5〕", t.slots.activate_slot(3))
|
||||
Kit.check("sv6 删档受理·激活槽 3〔卡① sv6〕", t.slots.delete_slot(3))
|
||||
Kit.check("sv6 删后文件不在盘+无 .tmp 残留〔卡① sv6〕",
|
||||
not FileAccess.file_exists(slot_tpl % 3) and not FileAccess.file_exists(slot_tpl % 3 + ".tmp"))
|
||||
Kit.check("sv6 删激活槽激活指针连清除(该槽不再可写)〔卡① sv6〕",
|
||||
t.active_slot() == 0 and not t.has_write_target())
|
||||
t.slots.active_slot() == 0 and not t.slots.has_write_target())
|
||||
var slot1_before := FileAccess.get_file_as_bytes(slot_tpl % 1)
|
||||
t.activate_slot(1)
|
||||
Kit.check("sv6 删非激活槽受理·槽 2〔卡① sv6〕", t.delete_slot(2))
|
||||
t.slots.activate_slot(1)
|
||||
Kit.check("sv6 删非激活槽受理·槽 2〔卡① sv6〕", t.slots.delete_slot(2))
|
||||
Kit.check("sv6 删非激活槽:文件不在盘+激活指针保持〔卡① sv6〕",
|
||||
not FileAccess.file_exists(slot_tpl % 2) and t.active_slot() == 1)
|
||||
not FileAccess.file_exists(slot_tpl % 2) and t.slots.active_slot() == 1)
|
||||
Kit.check("sv6 删槽 2 槽 1 字节不动(位间互隔离)〔卡① sv6〕",
|
||||
FileAccess.get_file_as_bytes(slot_tpl % 1) == slot1_before)
|
||||
|
||||
@@ -394,10 +394,10 @@ func _smoke_save_slots() -> void:
|
||||
fold.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 999,
|
||||
"adventure_total": 5, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
|
||||
fold.close()
|
||||
Kit.check("sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕", t.create_slot(3))
|
||||
Kit.check("sv4 新档受理·槽 3(创建即激活)〔卡① sv4〕", t.slots.create_slot(3))
|
||||
Kit.check("sv4 新档=全零可玩合法白板态(旧内容 999/5 零带入——钥石/总局数/断点全零)〔卡① sv4〕",
|
||||
t.active_slot() == 3 and t.keystone_balance() == 0 and not t.has_breakpoint()
|
||||
and t.adventure_total() == 0)
|
||||
t.slots.active_slot() == 3 and t.meta_state.keystone_balance() == 0 and not t.has_breakpoint()
|
||||
and t.meta_state.career_state().adventure_total == 0)
|
||||
var created: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(slot_tpl % 3))
|
||||
Kit.check("sv4 新档落盘=白板内容(盘面核验非 999 旧值)〔卡① sv4〕",
|
||||
int((created.get("meta", {}) as Dictionary).get("keystone_balance", -1)) == 0)
|
||||
@@ -407,14 +407,14 @@ func _smoke_save_slots() -> void:
|
||||
fhv.store_string(JSON.stringify({"version": 99, "meta": {}}))
|
||||
fhv.close()
|
||||
var hv = sm_script.new()
|
||||
hv.attach_slot_template(slot_tpl)
|
||||
hv.slots.attach_slot_template(slot_tpl)
|
||||
Kit.check("sv5 进档槽 1 读回(107)〔卡① sv10〕",
|
||||
hv.activate_slot(1) and hv.keystone_balance() == 107)
|
||||
hv.slots.activate_slot(1) and hv.meta_state.keystone_balance() == 107)
|
||||
var slot2_hv := FileAccess.get_file_as_bytes(slot_tpl % 2)
|
||||
Kit.check("sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕", not hv.activate_slot(2))
|
||||
Kit.check("sv9 高版本槽进档拒绝(拒读禁写链按槽)〔卡① sv9〕", not hv.slots.activate_slot(2))
|
||||
Kit.check("sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕",
|
||||
hv.active_slot() == 1 and hv.keystone_balance() == 107)
|
||||
hv.add_keystone(1)
|
||||
hv.slots.active_slot() == 1 and hv.meta_state.keystone_balance() == 107)
|
||||
hv.meta_state._add_keystone(1)
|
||||
Kit.check("sv10 恢复后写盘落激活槽 1(高版本槽 2 零误写)〔卡① sv10〕",
|
||||
hv.write_to_disk() and FileAccess.get_file_as_bytes(slot_tpl % 2) == slot2_hv)
|
||||
|
||||
@@ -425,14 +425,14 @@ func _smoke_save_slots() -> void:
|
||||
fm.close()
|
||||
for i in range(1, 4):
|
||||
var mi = sm_script.new()
|
||||
mi.attach_slot_template(slot_tpl)
|
||||
mi.slots.attach_slot_template(slot_tpl)
|
||||
Kit.check("sv9 迁移链按槽成立·槽 %d(v5→v7 各槽独立+写盘 version=7)〔卡① sv9〕" % i,
|
||||
mi.activate_slot(i) and mi.keystone_balance() == i * 11
|
||||
mi.slots.activate_slot(i) and mi.meta_state.keystone_balance() == i * 11
|
||||
and mi.write_to_disk())
|
||||
|
||||
# ⑥ 窥视三态零副作用(sv7/sv8):异值三槽逐槽三项+空槽零值+坏/高版本字节 mtime 双比对
|
||||
var pv = sm_script.new()
|
||||
pv.attach_slot_template(slot_tpl)
|
||||
pv.slots.attach_slot_template(slot_tpl)
|
||||
for i in range(1, 4):
|
||||
var fp := FileAccess.open(slot_tpl % i, FileAccess.WRITE)
|
||||
fp.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 200 * i,
|
||||
@@ -440,12 +440,12 @@ func _smoke_save_slots() -> void:
|
||||
"tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
|
||||
fp.close()
|
||||
for i in range(1, 4):
|
||||
var ps: Dictionary = pv.peek_slot_summary(i)
|
||||
var ps: Dictionary = pv.slots.peek_slot_summary(i)
|
||||
Kit.check("sv7 窥视三项逐槽正确·槽 %d〔卡① sv7〕" % i,
|
||||
bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i
|
||||
and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i)
|
||||
Kit.check("sv6 删槽 3 备空槽窥视〔卡① sv6〕", pv.delete_slot(3))
|
||||
var pe: Dictionary = pv.peek_slot_summary(3)
|
||||
Kit.check("sv6 删槽 3 备空槽窥视〔卡① sv6〕", pv.slots.delete_slot(3))
|
||||
var pe: Dictionary = pv.slots.peek_slot_summary(3)
|
||||
Kit.check("sv7 空槽窥视=零值合法态〔卡① sv7〕",
|
||||
bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0
|
||||
and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0)
|
||||
@@ -454,7 +454,7 @@ func _smoke_save_slots() -> void:
|
||||
fbad.close()
|
||||
var bad_bytes := FileAccess.get_file_as_bytes(slot_tpl % 1)
|
||||
var bad_mtime := FileAccess.get_modified_time(slot_tpl % 1)
|
||||
var pb: Dictionary = pv.peek_slot_summary(1)
|
||||
var pb: Dictionary = pv.slots.peek_slot_summary(1)
|
||||
Kit.check("sv8 坏槽窥视=显式损坏标记〔卡① sv8〕", not bool(pb.get("readable", true)))
|
||||
Kit.check("sv8 坏槽窥视零副作用(字节+mtime 双比对不变+零 .corrupt.bak 改名)〔卡① sv8〕",
|
||||
FileAccess.get_file_as_bytes(slot_tpl % 1) == bad_bytes
|
||||
@@ -465,7 +465,7 @@ func _smoke_save_slots() -> void:
|
||||
fhv2.close()
|
||||
var hv_bytes := FileAccess.get_file_as_bytes(slot_tpl % 2)
|
||||
var hv_mtime := FileAccess.get_modified_time(slot_tpl % 2)
|
||||
var ph: Dictionary = pv.peek_slot_summary(2)
|
||||
var ph: Dictionary = pv.slots.peek_slot_summary(2)
|
||||
Kit.check("sv8 高版本槽窥视=不可读标记〔卡① sv8〕", not bool(ph.get("readable", true)))
|
||||
Kit.check("sv8 高版本窥视零副作用(字节+mtime 不变+零改名)〔卡① sv8〕",
|
||||
FileAccess.get_file_as_bytes(slot_tpl % 2) == hv_bytes
|
||||
@@ -473,7 +473,7 @@ func _smoke_save_slots() -> void:
|
||||
|
||||
# ⑦ 旧单档迁移(sv12):字节搬运+源改名留痕+幂等+他槽零触碰
|
||||
var ml = sm_script.new()
|
||||
ml.attach_slot_template(slot_tpl)
|
||||
ml.slots.attach_slot_template(slot_tpl)
|
||||
var fsrc := FileAccess.open(legacy_src, FileAccess.WRITE)
|
||||
fsrc.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 1234}}))
|
||||
fsrc.close()
|
||||
|
||||
@@ -39,46 +39,46 @@ func _write_v7_keystone(path: String, keystone: int) -> void:
|
||||
# ① 三槽往返:各槽写异值 → 写盘 → 新实例按位读回逐域相等;改槽 1 槽 2 字节不动(sv1)
|
||||
func _test_three_slot_roundtrip_and_isolation() -> void:
|
||||
var t := SaveManagerScript.new()
|
||||
t.attach_slot_template(TPL)
|
||||
t.slots.attach_slot_template(TPL)
|
||||
for i in range(1, 4):
|
||||
Kit.check_quiet(t.activate_slot(i), "sv5 进档受理·槽 %d〔卡① sv5〕" % i)
|
||||
t.add_keystone(100 * i)
|
||||
t.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
|
||||
Kit.check_quiet(t.slots.activate_slot(i), "sv5 进档受理·槽 %d〔卡① sv5〕" % i)
|
||||
t.meta_state._add_keystone(100 * i)
|
||||
t.meta_state.apply_adventure_summary({"result": "completed", "cleared": i == 1, "levels_cleared": i,
|
||||
"stopped_at": 0, "keystone_total": 0, "master_seed": i})
|
||||
Kit.check_quiet(t.write_to_disk(), "sv3 激活态写盘受理·槽 %d〔卡① sv1〕" % i)
|
||||
var t2 := SaveManagerScript.new()
|
||||
t2.attach_slot_template(TPL)
|
||||
t2.slots.attach_slot_template(TPL)
|
||||
for i in range(1, 4):
|
||||
Kit.check_quiet(t2.activate_slot(i) and t2.keystone_balance() == 100 * i
|
||||
and t2.adventure_total() == 1 and t2.adventure_cleared() == (1 if i == 1 else 0),
|
||||
Kit.check_quiet(t2.slots.activate_slot(i) and t2.meta_state.keystone_balance() == 100 * i
|
||||
and t2.meta_state.career_state().adventure_total == 1 and t2.meta_state.career_state().adventure_cleared == (1 if i == 1 else 0),
|
||||
"sv1 三槽往返一致·槽 %d(异值按位读回)〔卡① sv1〕" % i)
|
||||
var slot2_before := FileAccess.get_file_as_bytes(TPL % 2)
|
||||
t.activate_slot(1)
|
||||
t.add_keystone(7)
|
||||
t.slots.activate_slot(1)
|
||||
t.meta_state._add_keystone(7)
|
||||
Kit.check_quiet(t.write_to_disk(), "sv3 写盘受理·槽 1 加值后〔卡① sv1〕")
|
||||
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 2) == slot2_before,
|
||||
"sv1 位间隔离:改槽 1 槽 2 字节不动〔卡① sv1〕")
|
||||
# 激活号记账:activate 槽派生名后 active_slot 同步;attach_path 注入槽名亦然
|
||||
Kit.check_quiet(t.active_slot() == 1 and t.has_write_target(),
|
||||
Kit.check_quiet(t.slots.active_slot() == 1 and t.slots.has_write_target(),
|
||||
"sv1 activate 后激活号同步(active_slot=1 写盘目标在位)〔卡① sv1〕")
|
||||
|
||||
|
||||
# ② 高版本槽进档失败:拒绝+恢复切换前三态(路径/激活号/数据)+拒读槽零误写(sv9/sv10)
|
||||
func _test_activate_failure_restores_previous_slot() -> void:
|
||||
var t := SaveManagerScript.new()
|
||||
t.attach_slot_template(TPL)
|
||||
t.slots.attach_slot_template(TPL)
|
||||
_write_v7_keystone(TPL % 1, 311)
|
||||
_write_v7_keystone(TPL % 2, 422)
|
||||
var fhv := FileAccess.open(TPL % 3, FileAccess.WRITE)
|
||||
fhv.store_string(JSON.stringify({"version": 99, "meta": {}}))
|
||||
fhv.close()
|
||||
Kit.check_quiet(t.activate_slot(1) and t.keystone_balance() == 311,
|
||||
Kit.check_quiet(t.slots.activate_slot(1) and t.meta_state.keystone_balance() == 311,
|
||||
"sv5 进档槽 1 读回(311)〔卡① sv10〕")
|
||||
var slot2_before := FileAccess.get_file_as_bytes(TPL % 2)
|
||||
Kit.check_quiet(not t.activate_slot(3), "sv9 高版本槽进档拒绝(拒读链按槽)〔卡① sv9〕")
|
||||
Kit.check_quiet(t.active_slot() == 1 and t.has_write_target() and t.keystone_balance() == 311,
|
||||
Kit.check_quiet(not t.slots.activate_slot(3), "sv9 高版本槽进档拒绝(拒读链按槽)〔卡① sv9〕")
|
||||
Kit.check_quiet(t.slots.active_slot() == 1 and t.slots.has_write_target() and t.meta_state.keystone_balance() == 311,
|
||||
"sv10 激活失败恢复切换前三态(路径/激活号/数据归槽 1)〔卡① sv10〕")
|
||||
t.add_keystone(1)
|
||||
t.meta_state._add_keystone(1)
|
||||
Kit.check_quiet(t.write_to_disk(), "sv3 恢复后写盘受理〔卡① sv10〕")
|
||||
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 2) == slot2_before,
|
||||
"sv10 拒读槽零误写(写盘落激活槽 1,槽 2/3 字节不动)〔卡① sv10〕")
|
||||
@@ -87,17 +87,17 @@ func _test_activate_failure_restores_previous_slot() -> void:
|
||||
# ③ MetaProgression 宿主链切槽:引用同一性保持+写盘落当前槽(sv10——禁换实例先例)
|
||||
func _test_metaprogression_reference_identity() -> void:
|
||||
var t := SaveManagerScript.new()
|
||||
t.attach_slot_template(TPL)
|
||||
t.slots.attach_slot_template(TPL)
|
||||
_write_v7_keystone(TPL % 1, 533)
|
||||
_write_v7_keystone(TPL % 2, 644)
|
||||
Kit.check_quiet(t.activate_slot(1), "sv5 进档槽 1〔卡① sv10〕")
|
||||
Kit.check_quiet(t.slots.activate_slot(1), "sv5 进档槽 1〔卡① sv10〕")
|
||||
var meta := MetaProgressionScript.new(null, t)
|
||||
Kit.check_quiet(meta.save == t and meta.keystone_balance() == 533,
|
||||
"sv10 宿主链取数走动态分发(meta.save 同一实例、读槽 1)〔卡① sv10〕")
|
||||
Kit.check_quiet(t.activate_slot(2) and meta.save == t and meta.keystone_balance() == 644,
|
||||
Kit.check_quiet(t.slots.activate_slot(2) and meta.save == t and meta.keystone_balance() == 644,
|
||||
"sv10 切槽后引用同一性保持(零换实例)且读数随槽(644)〔卡① sv10〕")
|
||||
meta.save.add_keystone(6)
|
||||
Kit.check_quiet(t.write_to_disk() and t.keystone_balance() == 650,
|
||||
meta.save.meta_state._add_keystone(6)
|
||||
Kit.check_quiet(t.write_to_disk() and t.meta_state.keystone_balance() == 650,
|
||||
"sv10 宿主链写盘落当前槽 2(650=644+6)〔卡① sv10〕")
|
||||
Kit.check_quiet(int((JSON.parse_string(FileAccess.get_file_as_string(TPL % 2)) as Dictionary)\
|
||||
.get("meta", {}).get("keystone_balance", -1)) == 650,
|
||||
|
||||
@@ -37,20 +37,20 @@ func _write_slot_json(path: String, content: String) -> void:
|
||||
# ① 三槽异值逐槽三项正确+不激活不写内存(sv7)
|
||||
func _test_peek_three_slots_distinct_values() -> void:
|
||||
var t := SaveManagerScript.new()
|
||||
t.attach_slot_template(TPL)
|
||||
t.slots.attach_slot_template(TPL)
|
||||
for i in range(1, 4):
|
||||
_write_slot_json(TPL % i, JSON.stringify({"version": 7, "meta": {
|
||||
"keystone_balance": 200 * i, "adventure_total": 5 * i, "adventure_cleared": i,
|
||||
"tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
|
||||
for i in range(1, 4):
|
||||
var ps: Dictionary = t.peek_slot_summary(i)
|
||||
var ps: Dictionary = t.slots.peek_slot_summary(i)
|
||||
Kit.check_quiet(bool(ps.get("readable", false)) and int(ps.get("keystone", -1)) == 200 * i
|
||||
and int(ps.get("cleared", -1)) == i and int(ps.get("total", -1)) == 5 * i,
|
||||
"sv7 窥视三项逐槽正确·槽 %d(钥石/冒险成功/总局)〔卡① sv7〕" % i)
|
||||
Kit.check_quiet(t.active_slot() == 0 and not t.has_write_target() and t.keystone_balance() == 0,
|
||||
Kit.check_quiet(t.slots.active_slot() == 0 and not t.slots.has_write_target() and t.meta_state.keystone_balance() == 0,
|
||||
"sv7 窥视不激活不写内存(激活号 0/无写盘目标/内存余额零)〔卡① sv7〕")
|
||||
# 路径注入口同语义(契约 7 注入面)
|
||||
var pinj: Dictionary = t.peek_summary_at(TPL % 2)
|
||||
var pinj: Dictionary = t.slots.peek_summary_at(TPL % 2)
|
||||
Kit.check_quiet(bool(pinj.get("readable", false)) and int(pinj.get("keystone", -1)) == 400,
|
||||
"sv7 peek_summary_at 注入口同语义(槽 2 钥石 400)〔卡① sv7〕")
|
||||
|
||||
@@ -58,32 +58,32 @@ func _test_peek_three_slots_distinct_values() -> void:
|
||||
# ② 空槽零值合法态+不可读三态显式标记(sv7/契约 9)
|
||||
func _test_peek_empty_and_unreadable_states() -> void:
|
||||
var t := SaveManagerScript.new()
|
||||
t.attach_slot_template(TPL)
|
||||
t.slots.attach_slot_template(TPL)
|
||||
DirAccess.remove_absolute(TPL % 3) # 清①段残留——空槽夹具
|
||||
var pe: Dictionary = t.peek_slot_summary(3) # 槽 3 无文件=空槽
|
||||
var pe: Dictionary = t.slots.peek_slot_summary(3) # 槽 3 无文件=空槽
|
||||
Kit.check_quiet(bool(pe.get("readable", false)) and int(pe.get("keystone", -1)) == 0
|
||||
and int(pe.get("cleared", -1)) == 0 and int(pe.get("total", -1)) == 0,
|
||||
"sv7 空槽窥视=零值合法态(可读+三项全零)〔卡① sv7〕")
|
||||
_write_slot_json(TPL % 1, "this is not json{{{")
|
||||
Kit.check_quiet(not bool(t.peek_slot_summary(1).get("readable", true)),
|
||||
Kit.check_quiet(not bool(t.slots.peek_slot_summary(1).get("readable", true)),
|
||||
"sv7 坏槽窥视=显式损坏标记〔卡① sv7〕")
|
||||
_write_slot_json(TPL % 2, JSON.stringify({"version": 99, "meta": {}}))
|
||||
Kit.check_quiet(not bool(t.peek_slot_summary(2).get("readable", true)),
|
||||
Kit.check_quiet(not bool(t.slots.peek_slot_summary(2).get("readable", true)),
|
||||
"sv7 高版本槽窥视=不可读标记〔卡① sv7〕")
|
||||
_write_slot_json(TPL % 3, JSON.stringify({"meta": {"keystone_balance": 5}})) # 缺版本字段
|
||||
Kit.check_quiet(not bool(t.peek_slot_summary(3).get("readable", true)),
|
||||
Kit.check_quiet(not bool(t.slots.peek_slot_summary(3).get("readable", true)),
|
||||
"sv7 缺版本字段槽窥视=不可读标记〔卡① sv7/契约9〕")
|
||||
|
||||
|
||||
# ③ 窥视零副作用:字节+mtime 双比对不变+零 .corrupt.bak 改名(sv8——真实入口冒烟段同判据)
|
||||
func _test_peek_zero_side_effect() -> void:
|
||||
var t := SaveManagerScript.new()
|
||||
t.attach_slot_template(TPL)
|
||||
t.slots.attach_slot_template(TPL)
|
||||
_write_slot_json(TPL % 1, "corrupt bytes {{{{")
|
||||
var bytes_before := FileAccess.get_file_as_bytes(TPL % 1)
|
||||
var mtime_before := FileAccess.get_modified_time(TPL % 1)
|
||||
t.peek_slot_summary(1)
|
||||
t.peek_summary_at(TPL % 1)
|
||||
t.slots.peek_slot_summary(1)
|
||||
t.slots.peek_summary_at(TPL % 1)
|
||||
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 1) == bytes_before
|
||||
and FileAccess.get_modified_time(TPL % 1) == mtime_before
|
||||
and not FileAccess.file_exists(TPL % 1 + ".corrupt.bak"),
|
||||
@@ -91,7 +91,7 @@ func _test_peek_zero_side_effect() -> void:
|
||||
_write_slot_json(TPL % 2, JSON.stringify({"version": 99, "meta": {}}))
|
||||
var hv_bytes := FileAccess.get_file_as_bytes(TPL % 2)
|
||||
var hv_mtime := FileAccess.get_modified_time(TPL % 2)
|
||||
t.peek_slot_summary(2)
|
||||
t.slots.peek_slot_summary(2)
|
||||
Kit.check_quiet(FileAccess.get_file_as_bytes(TPL % 2) == hv_bytes
|
||||
and FileAccess.get_modified_time(TPL % 2) == hv_mtime
|
||||
and not FileAccess.file_exists(TPL % 2 + ".corrupt.bak"),
|
||||
@@ -99,7 +99,7 @@ func _test_peek_zero_side_effect() -> void:
|
||||
# .tmp 残留按空槽处理(契约 9)
|
||||
DirAccess.remove_absolute(TPL % 3) # 主文件清场——只留 .tmp 残留
|
||||
_write_slot_json(TPL % 3 + ".tmp", "{}")
|
||||
var pt: Dictionary = t.peek_slot_summary(3)
|
||||
var pt: Dictionary = t.slots.peek_slot_summary(3)
|
||||
Kit.check_quiet(bool(pt.get("readable", false)) and int(pt.get("keystone", -1)) == 0,
|
||||
"sv8 .tmp 残留按空槽处理(主文件不在=空槽零值)〔卡① sv8〕")
|
||||
if FileAccess.file_exists(TPL % 3 + ".tmp"):
|
||||
|
||||
@@ -53,7 +53,7 @@ func _init() -> void:
|
||||
var mp = load("res://core/meta_progression.gd").new(db, sv)
|
||||
mt.rows = [base_pre.duplicate(true), base_dep.duplicate(true)]
|
||||
db.set("meta_tree", mt)
|
||||
sv.set_tree_level("gate_pre", 1)
|
||||
sv.meta_state.set_tree_level("gate_pre", 1)
|
||||
if mp.tree_can_buy("gate_dep"):
|
||||
failures += 1
|
||||
print("[r1] FAIL 前置 Lv1 < 阈值 2 应拒购")
|
||||
@@ -62,17 +62,17 @@ func _init() -> void:
|
||||
failures += 1
|
||||
print("[r1] FAIL 无 requires_level 行判定不受机制位影响")
|
||||
# ⑦ 判定面:达标开放
|
||||
sv.set_tree_level("gate_pre", 2)
|
||||
sv.meta_state.set_tree_level("gate_pre", 2)
|
||||
if not mp.tree_can_buy("gate_dep"):
|
||||
failures += 1
|
||||
print("[r1] FAIL 前置 Lv2 达阈值应可购")
|
||||
# ⑧ 投影:gate_text 未达标拼门槛文案、达标清空
|
||||
sv.set_tree_level("gate_pre", 1)
|
||||
sv.meta_state.set_tree_level("gate_pre", 1)
|
||||
var gate: String = mp.tree_gate_text("gate_dep")
|
||||
if gate != "需 门槛前置 Lv2":
|
||||
failures += 1
|
||||
print("[r1] FAIL 未达标 gate_text 应=「需 门槛前置 Lv2」实得「%s」" % gate)
|
||||
sv.set_tree_level("gate_pre", 2)
|
||||
sv.meta_state.set_tree_level("gate_pre", 2)
|
||||
if mp.tree_gate_text("gate_dep") != "":
|
||||
failures += 1
|
||||
print("[r1] FAIL 达标后 gate_text 应为空串")
|
||||
|
||||
@@ -85,7 +85,7 @@ func _run() -> void:
|
||||
rows.sword.buy_btn.disabled and rows.spear.unlock_btn.disabled)
|
||||
|
||||
# ⑥ 满级行:无购买钮、状态=已满级(直置存档域构造——两分法调试口径)
|
||||
sv.set_forge_route_level("sword", 5)
|
||||
sv.meta_state.set_forge_route_level("sword", 5)
|
||||
ov.refresh_camp({"balance": 500, "forge": mp.forge_shop_status(),
|
||||
"tree": [{"node_id": "forge_workshop", "level": 1}]})
|
||||
rows = _row_by_id(ov)
|
||||
|
||||
@@ -51,8 +51,8 @@ func _init() -> void:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
sm.attach_path("user://test_pacing_queue_save.json")
|
||||
sm.mark_first_adventure_done()
|
||||
sm.slots.attach_path("user://test_pacing_queue_save.json")
|
||||
sm.meta_state.mark_first_adventure_done()
|
||||
# 行信号计数器(真实信号路径旁挂计数——零触碰既有接线)
|
||||
var rows_seen: Array = []
|
||||
main._start_adventure_from_menu()
|
||||
|
||||
@@ -831,21 +831,21 @@ func _career_overlay_projection() -> void:
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
# 已玩态夹具:两局收束(一通一止)+ 钥石非零——投影可辨非全零白板
|
||||
# (生涯页不统计天赋树与账号成长态,只统计终身计数)
|
||||
sv.add_keystone(45)
|
||||
sv.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
|
||||
sv.meta_state._add_keystone(45)
|
||||
sv.meta_state.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,
|
||||
sv.meta_state.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 0, "master_seed": 22})
|
||||
sv.write_to_disk()
|
||||
# 投影拼装(HudPresenter.career_data 同构)+ 视图喂入
|
||||
var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(),
|
||||
var data := {"total": sv.meta_state.career_state().adventure_total, "cleared": sv.meta_state.career_state().adventure_cleared,
|
||||
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
|
||||
"boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance()}
|
||||
"boss_kills": sv.meta_state.career_state().boss_kill_count, "keystone": mp.keystone_balance()}
|
||||
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):
|
||||
"count": sv.meta_state.career_state().clear_counts.get(str(li), 0)})
|
||||
if sv.meta_state.career_state().boss_first_kill.has(str(li)):
|
||||
data["boss_first_kills"] += 1
|
||||
var co = load("res://presentation/views/career_overlay.gd").new()
|
||||
co.open_career(data)
|
||||
@@ -881,14 +881,14 @@ func _career_overlay_projection() -> void:
|
||||
fv5c.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 3}}))
|
||||
fv5c.close()
|
||||
Kit.check("v5 现役档升 v6 载入成功(计数域缺省补零)",
|
||||
v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0)
|
||||
v5.load_from_disk() and v5.meta_state.career_state().adventure_total == 0 and v5.meta_state.career_state().adventure_cleared == 0)
|
||||
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
|
||||
var data_v5 := {"total": v5.adventure_total(), "cleared": v5.adventure_cleared(),
|
||||
var data_v5 := {"total": v5.meta_state.career_state().adventure_total, "cleared": v5.meta_state.career_state().adventure_cleared,
|
||||
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(),
|
||||
"boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance()}
|
||||
"boss_kills": v5.meta_state.career_state().boss_kill_count, "keystone": mp_v5.keystone_balance()}
|
||||
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)})
|
||||
"count": v5.meta_state.career_state().clear_counts.get(str(i + 1), 0)})
|
||||
var co_v5 = load("res://presentation/views/career_overlay.gd").new()
|
||||
co_v5.open_career(data_v5)
|
||||
Kit.check("旧档载入后生涯页正常显示(v5 升 v6:总局数/冒险成功局数显示 0,既有钥石余额原样呈现)",
|
||||
|
||||
Reference in new issue
Block a user