卡①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
@@ -369,7 +369,7 @@ func _loadout_on_level_inventory() -> void:
|
||||
func _boss_keystone_drop(li: int) -> void:
|
||||
var first := true
|
||||
if _save_manager != null:
|
||||
first = not _save_manager.boss_first_kill_marked(li)
|
||||
first = not _save_manager.meta_state.career_state().boss_first_kill.has(str(li))
|
||||
var row: Dictionary[Variant, Variant] = db.meta_boss_keystone_row(li)
|
||||
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
|
||||
_journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语
|
||||
|
||||
@@ -12,6 +12,6 @@ static func slots(db: ConfigDb, save: Variant) -> int:
|
||||
return 0
|
||||
var n := 0
|
||||
for i in range(1, 4):
|
||||
if save.tree_level("att_socket%d" % i) >= 1:
|
||||
if save.meta_state.tree_state().nodes.get("att_socket%d" % i, 0) >= 1:
|
||||
n += 1
|
||||
return n
|
||||
@@ -26,7 +26,7 @@ func _ready() -> void:
|
||||
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return save.keystone_balance()
|
||||
return save.meta_state.keystone_balance()
|
||||
|
||||
|
||||
# ---- 秘宝解锁服务(解锁混合制,契约 6)----
|
||||
@@ -35,7 +35,7 @@ func keystone_balance() -> int:
|
||||
|
||||
# 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager)
|
||||
func is_item_unlocked(item_id: String) -> bool:
|
||||
return save.item_unlocked(item_id)
|
||||
return save.meta_state.item_unlocked(item_id)
|
||||
|
||||
|
||||
# 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS;
|
||||
@@ -43,11 +43,11 @@ func is_item_unlocked(item_id: String) -> bool:
|
||||
func _treasure_unlock_progress(key: String) -> int:
|
||||
match key:
|
||||
"adventure_cleared":
|
||||
return save.adventure_cleared()
|
||||
return save.meta_state.career_state().adventure_cleared
|
||||
"boss_kill_count":
|
||||
return save.boss_kill_count()
|
||||
return save.meta_state.career_state().boss_kill_count
|
||||
"vision_used":
|
||||
return save.vision_used()
|
||||
return save.meta_state.career_state().vision_used
|
||||
_:
|
||||
return 0 # 未知计数键恒 0 = 永不解锁(fail-safe 非致命——schema 词表校验兜底,不静默误解锁)
|
||||
|
||||
@@ -59,11 +59,11 @@ func buy_treasure_unlock(item_id: String) -> bool:
|
||||
var price := int(arow.get("unlock_keystone_cost", 0))
|
||||
if arow.is_empty() or price < 1 or (arow.get("unlock_requires", []) as Array).size() > 0:
|
||||
return false # 非购买件(条件件/无价行)拒绝
|
||||
if save.item_unlocked(item_id):
|
||||
if save.meta_state.item_unlocked(item_id):
|
||||
return false # 已解锁重复购买拒绝
|
||||
if not save.spend_keystone(price):
|
||||
if not save.meta_state.spend_keystone(price):
|
||||
return false # 余额不足(零副作用)
|
||||
save.unlock_item(item_id)
|
||||
save.meta_state.unlock_item(item_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ func refresh_treasure_unlocks() -> Array:
|
||||
var new_unlocks: Array[String] = []
|
||||
for row in db.treasure_attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if save.item_unlocked(iid):
|
||||
if save.meta_state.item_unlocked(iid):
|
||||
continue
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array
|
||||
if ureqs.is_empty():
|
||||
@@ -85,7 +85,7 @@ func refresh_treasure_unlocks() -> Array:
|
||||
met = false
|
||||
break
|
||||
if met:
|
||||
save.unlock_item(iid)
|
||||
save.meta_state.unlock_item(iid)
|
||||
new_unlocks.append(iid)
|
||||
if not new_unlocks.is_empty():
|
||||
save.write_to_disk()
|
||||
@@ -98,7 +98,7 @@ func treasure_shop_status() -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for row in db.treasure_attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var unlocked := save.item_unlocked(iid)
|
||||
var unlocked := save.meta_state.item_unlocked(iid)
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array
|
||||
var req_view: Array[Variant] = []
|
||||
for req_v in ureqs:
|
||||
@@ -134,12 +134,12 @@ func forge_route_unlocked(route_id: String) -> bool:
|
||||
return false
|
||||
if bool(row.get("initial_owned", false)):
|
||||
return true
|
||||
if save.forge_unlocked_purchases().has(route_id):
|
||||
if save.meta_state.forge_state().unlocked.has(route_id):
|
||||
return true
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
if prereq != "":
|
||||
var prow: Dictionary[Variant, Variant] = db.forge_route(prereq)
|
||||
if not prow.is_empty() and save.forge_route_level(prereq) >= int(prow.get("cap", 0)):
|
||||
if not prow.is_empty() and save.meta_state.forge_state().levels.get(prereq, 0) >= int(prow.get("cap", 0)):
|
||||
return true
|
||||
return false
|
||||
|
||||
@@ -150,13 +150,13 @@ func forge_buy_level(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
if row.is_empty() or not forge_route_unlocked(route_id):
|
||||
return false
|
||||
var lvl := save.forge_route_level(route_id)
|
||||
var lvl := save.meta_state.forge_state().levels.get(route_id, 0)
|
||||
if lvl >= int(row.get("cap", 0)):
|
||||
return false
|
||||
var price := ForgeTable.level_price(row, lvl)
|
||||
if not save.spend_keystone(price):
|
||||
if not save.meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.set_forge_route_level(route_id, lvl + 1)
|
||||
save.meta_state.set_forge_route_level(route_id, lvl + 1)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -169,9 +169,9 @@ func forge_buy_unlock(route_id: String) -> bool:
|
||||
if forge_route_unlocked(route_id):
|
||||
return false
|
||||
var price := int(row.get("unlock_price", 0))
|
||||
if price < 1 or not save.spend_keystone(price):
|
||||
if price < 1 or not save.meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.add_forge_unlock(route_id)
|
||||
save.meta_state.add_forge_unlock(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ func forge_select_weapon(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(route_id):
|
||||
return false
|
||||
save.set_forge_weapon(route_id)
|
||||
save.meta_state.set_forge_weapon(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -189,32 +189,32 @@ func forge_select_armor(route_id: String) -> bool:
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(route_id)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(route_id):
|
||||
return false
|
||||
save.set_forge_armor(route_id)
|
||||
save.meta_state.set_forge_armor(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 当前选中武器攻击加值 / 当前甲 AC 加值(路线等级×每级增量——0=裸体态/未锻造合法态)。
|
||||
# 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。
|
||||
func forge_weapon_atk_bonus() -> int:
|
||||
var rid := save.forge_weapon()
|
||||
var rid := save.meta_state.forge_state().weapon
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid):
|
||||
return 0
|
||||
return save.forge_route_level(rid) * int(row.get("per_level_gain", 0))
|
||||
return save.meta_state.forge_state().levels.get(rid, 0) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
func forge_armor_ac_bonus() -> int:
|
||||
var rid := save.forge_armor()
|
||||
var rid := save.meta_state.forge_state().armor
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid):
|
||||
return 0
|
||||
return save.forge_route_level(rid) * int(row.get("per_level_gain", 0))
|
||||
return save.meta_state.forge_state().levels.get(rid, 0) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
# 两根类型轴当前生效值(选中路线类型轴;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
|
||||
# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。
|
||||
func hero_attack_type() -> String:
|
||||
var rid := save.forge_weapon()
|
||||
var rid := save.meta_state.forge_state().weapon
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid):
|
||||
return db.forge_default_attack_type()
|
||||
@@ -222,7 +222,7 @@ func hero_attack_type() -> String:
|
||||
|
||||
|
||||
func hero_armor_type() -> String:
|
||||
var rid := save.forge_armor()
|
||||
var rid := save.meta_state.forge_state().armor
|
||||
var row: Dictionary[Variant, Variant] = db.forge_route(rid)
|
||||
if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid):
|
||||
return db.forge_default_armor_type()
|
||||
@@ -237,14 +237,14 @@ func forge_shop_status() -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for row in db.forge_routes():
|
||||
var rid := String(row.get("route_id", ""))
|
||||
var lvl := save.forge_route_level(rid)
|
||||
var lvl := save.meta_state.forge_state().levels.get(rid, 0)
|
||||
var cap_v := int(row.get("cap", 0))
|
||||
var unlocked := forge_route_unlocked(rid)
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
var plvl := save.forge_route_level(prereq) if prereq != "" else 0
|
||||
var plvl := save.meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0
|
||||
var pcap := int(db.forge_route(prereq).get("cap", 0)) if prereq != "" else 0
|
||||
var selected := (String(row.get("kind", "")) == "weapon" and save.forge_weapon() == rid) \
|
||||
or (String(row.get("kind", "")) == "armor" and save.forge_armor() == rid)
|
||||
var selected := (String(row.get("kind", "")) == "weapon" and save.meta_state.forge_state().weapon == rid) \
|
||||
or (String(row.get("kind", "")) == "armor" and save.meta_state.forge_state().armor == rid)
|
||||
out.append({
|
||||
"route_id": rid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
@@ -274,10 +274,10 @@ func forge_shop_status() -> Array:
|
||||
# apply_adventure_summary 逐关卡完成关次置位)。本函数只做标记+入账+归档。
|
||||
func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
|
||||
save.mark_first_adventure_done()
|
||||
save.meta_state.mark_first_adventure_done()
|
||||
var drops: Array[Variant] = (summary.get("keystone_drops", []) as Array).duplicate(true)
|
||||
save.apply_adventure_summary(summary, drops)
|
||||
save.add_vision_used(int(summary.get("vision_used_delta", 0))) # 真视使用计数收束聚合入档(信标网络行为条件)
|
||||
save.meta_state.apply_adventure_summary(summary, drops)
|
||||
save.meta_state.add_vision_used(int(summary.get("vision_used_delta", 0))) # 真视使用计数收束聚合入档(信标网络行为条件)
|
||||
var new_unlocks := refresh_treasure_unlocks() # 条件解锁刷新
|
||||
save.write_to_disk()
|
||||
var total := int(summary.get("keystone_total", 0))
|
||||
@@ -311,7 +311,7 @@ func hero_override() -> Dictionary:
|
||||
# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」)
|
||||
func tree_spent_total() -> int:
|
||||
var total := 0
|
||||
for lvl in (save.tree_nodes() as Dictionary).values():
|
||||
for lvl in (save.meta_state.tree_state().nodes as Dictionary).values():
|
||||
total += int(lvl)
|
||||
return total
|
||||
|
||||
@@ -324,24 +324,24 @@ func tree_can_buy(node_id: String) -> bool:
|
||||
# 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红)
|
||||
if String(row.get("kind", "")) == "easter":
|
||||
return false
|
||||
var lvl := save.tree_level(node_id)
|
||||
var lvl := save.meta_state.tree_state().nodes.get(node_id, 0)
|
||||
if lvl >= int(row.get("cap", 1)):
|
||||
return false
|
||||
# 刷新升级节点:cap 主语=Y 账本(单一事实源——树上入口与装配段同 cap 同价,防双账漂移)
|
||||
if node_id == "ks_refresh" and db.meta_attune_refresh_row(save.attune_refresh_level() + 1).is_empty():
|
||||
if node_id == "ks_refresh" and db.meta_attune_refresh_row(save.meta_state.attune_state().refresh_level + 1).is_empty():
|
||||
return false
|
||||
# 行门槛(一线一节点:脊柱节点第 N 级需全树总投入 ≥4×(N−1))
|
||||
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat":
|
||||
if tree_spent_total() < db.meta_tree.level_gate(lvl + 1):
|
||||
return false
|
||||
for req in (row.get("requires", []) as Array):
|
||||
if save.tree_level(String(req)) < 1:
|
||||
if save.meta_state.tree_state().nodes.get(String(req), 0) < 1:
|
||||
return false
|
||||
# 等级阈值前置(requires_level {node_id: n}:前置节点等级 ≥ n 才可购;
|
||||
# 现役表零配置=空转零行为影响)
|
||||
var req_lv: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary
|
||||
for req_id in req_lv.keys():
|
||||
if save.tree_level(String(req_id)) < int(req_lv[req_id]):
|
||||
if save.meta_state.tree_state().nodes.get(String(req_id), 0) < int(req_lv[req_id]):
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -357,9 +357,9 @@ func tree_buy(node_id: String) -> bool:
|
||||
if row.is_empty() or not tree_can_buy(node_id):
|
||||
return false
|
||||
var price := int(row.get("price", 0))
|
||||
if not save.spend_keystone(price):
|
||||
if not save.meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.set_tree_level(node_id, save.tree_level(node_id) + 1)
|
||||
save.meta_state.set_tree_level(node_id, save.meta_state.tree_state().nodes.get(node_id, 0) + 1)
|
||||
_check_easter_egg()
|
||||
return save.write_to_disk()
|
||||
|
||||
@@ -369,10 +369,10 @@ func tree_buy(node_id: String) -> bool:
|
||||
func tree_toggle_fork(group: String, side: String) -> bool:
|
||||
if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
|
||||
return false
|
||||
var cur := save.fork_side(group)
|
||||
var cur := save.meta_state.tree_state().fork_sides.get(group, "")
|
||||
if cur == "" or cur == side:
|
||||
return false # 未解锁走购买面;同态零操作
|
||||
save.set_fork_side(group, side)
|
||||
save.meta_state.set_fork_side(group, side)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -384,10 +384,10 @@ func tree_buy_fork(node_id: String) -> bool:
|
||||
if not tree_can_buy(node_id):
|
||||
return false
|
||||
var price := int(row.get("price", 0))
|
||||
if not save.spend_keystone(price):
|
||||
if not save.meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.set_tree_level(node_id, 1)
|
||||
save.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
|
||||
save.meta_state.set_tree_level(node_id, 1)
|
||||
save.meta_state.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -395,7 +395,7 @@ func tree_buy_fork(node_id: String) -> bool:
|
||||
func tree_stat_flat(key: String) -> int:
|
||||
var total := 0
|
||||
for row in db.tree_rows():
|
||||
var lvl := save.tree_level(String(row.get("node_id", "")))
|
||||
var lvl := save.meta_state.tree_state().nodes.get(String(row.get("node_id", "")), 0)
|
||||
if lvl <= 0:
|
||||
continue
|
||||
total += int((row.get("effects", {}) as Dictionary).get(key, 0)) * lvl
|
||||
@@ -404,7 +404,7 @@ func tree_stat_flat(key: String) -> int:
|
||||
|
||||
func tree_rule_keys() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var sides: Dictionary[Variant, Variant] = save.fork_sides()
|
||||
var sides: Dictionary[Variant, Variant] = save.meta_state.tree_state().fork_sides
|
||||
for row in db.tree_rows():
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
@@ -430,7 +430,7 @@ func tree_camp_rows() -> Array:
|
||||
"vein": String(row.get("vein", "")),
|
||||
"kind": String(row.get("kind", "")),
|
||||
"cap": int(row.get("cap", 1)),
|
||||
"level": save.attune_refresh_level() if String(row.get("node_id", "")) == "ks_refresh" else save.tree_level(String(row.get("node_id", ""))), # 刷新升级行主语=Y 账本(同账路由 V15)
|
||||
"level": save.meta_state.attune_state().refresh_level if String(row.get("node_id", "")) == "ks_refresh" else save.meta_state.tree_state().nodes.get(String(row.get("node_id", "")), 0), # 刷新升级行主语=Y 账本(同账路由 V15)
|
||||
"price": int(row.get("price", 0)),
|
||||
"currency": String(row.get("currency", "keystone")),
|
||||
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
|
||||
@@ -446,7 +446,7 @@ func tree_gate_text(node_id: String) -> String:
|
||||
var rl: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary
|
||||
var parts: Array[String] = []
|
||||
for req_id in rl.keys():
|
||||
if save.tree_level(String(req_id)) < int(rl[req_id]):
|
||||
if save.meta_state.tree_state().nodes.get(String(req_id), 0) < int(rl[req_id]):
|
||||
var req_row: Dictionary[Variant, Variant] = db.tree_node(String(req_id))
|
||||
parts.append("需 %s Lv%d" % [String(req_row.get("display_name", req_id)), int(rl[req_id])])
|
||||
return "、".join(parts)
|
||||
@@ -454,7 +454,7 @@ func tree_gate_text(node_id: String) -> String:
|
||||
|
||||
func tree_fork_groups() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var sides: Dictionary[Variant, Variant] = save.fork_sides()
|
||||
var sides: Dictionary[Variant, Variant] = save.meta_state.tree_state().fork_sides
|
||||
for row in db.tree_rows():
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
@@ -464,7 +464,7 @@ func tree_fork_groups() -> Array:
|
||||
"node_id": String(row.get("node_id", "")),
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"active_side": String(sides.get(fg, "")),
|
||||
"owned": save.tree_level(String(row.get("node_id", ""))) > 0,
|
||||
"owned": save.meta_state.tree_state().nodes.get(String(row.get("node_id", "")), 0) > 0,
|
||||
"can_buy": tree_can_buy(String(row.get("node_id", ""))),
|
||||
"price": int(row.get("price", 0)),
|
||||
"sides": [
|
||||
@@ -478,16 +478,16 @@ func tree_fork_groups() -> Array:
|
||||
# 彩蛋觉醒检查(特化判定——生命/命中两线拉满 且 同调位 1/2/3 全解锁 → easter_ley 免费点亮;
|
||||
# 不依赖 requires/requires_level 前置形态)
|
||||
func _check_easter_egg() -> void:
|
||||
if save.tree_level("easter_ley") > 0:
|
||||
if save.meta_state.tree_state().nodes.get("easter_ley", 0) > 0:
|
||||
return
|
||||
for nid in ["sp_hp", "sp_hit"]:
|
||||
var row: Dictionary[Variant, Variant] = db.tree_node(nid)
|
||||
if row.is_empty() or save.tree_level(nid) < int(row.get("cap", 1)):
|
||||
if row.is_empty() or save.meta_state.tree_state().nodes.get(nid, 0) < int(row.get("cap", 1)):
|
||||
return
|
||||
for i in range(1, 4):
|
||||
if save.tree_level("att_socket%d" % i) < 1:
|
||||
if save.meta_state.tree_state().nodes.get("att_socket%d" % i, 0) < 1:
|
||||
return
|
||||
save.set_tree_level("easter_ley", 1)
|
||||
save.meta_state.set_tree_level("easter_ley", 1)
|
||||
|
||||
|
||||
# ---- 同调位账本(装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2)----
|
||||
@@ -498,12 +498,12 @@ func _check_easter_egg() -> void:
|
||||
# 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。
|
||||
# 装配关系写存档持久。
|
||||
func toggle_attunement(item_id: String) -> bool:
|
||||
var equipped: Array[Variant] = save.attunement_equipped()
|
||||
var equipped: Array[Variant] = save.meta_state.attune_state().equipped
|
||||
if not equipped.has(item_id):
|
||||
var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id)
|
||||
if arow.is_empty() or String(arow.get("form", "")) == "":
|
||||
return false # 非同调行(过渡/退役行)禁装配
|
||||
if not save.item_unlocked(item_id):
|
||||
if not save.meta_state.item_unlocked(item_id):
|
||||
return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置)
|
||||
if equipped.size() >= Attunement.slots(db, save):
|
||||
return false # 满位禁选
|
||||
@@ -513,13 +513,13 @@ func toggle_attunement(item_id: String) -> bool:
|
||||
equipped.append(item_id)
|
||||
else:
|
||||
equipped.erase(item_id)
|
||||
save.set_attunement_equipped(equipped)
|
||||
save.meta_state.set_attunement_equipped(equipped)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工)
|
||||
func attune_camp_status() -> Dictionary:
|
||||
var equipped: Array[Variant] = save.attunement_equipped()
|
||||
var equipped: Array[Variant] = save.meta_state.attune_state().equipped
|
||||
var items: Array[Dictionary] = []
|
||||
for row in db.treasure_attunement_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
@@ -530,9 +530,9 @@ func attune_camp_status() -> Dictionary:
|
||||
"tier": String(row.get("tier", "")),
|
||||
"form": String(row.get("form", "")),
|
||||
"equipped": equipped.has(iid),
|
||||
"unlocked": save.item_unlocked(iid),
|
||||
"unlocked": save.meta_state.item_unlocked(iid),
|
||||
})
|
||||
var lvl := save.attune_refresh_level()
|
||||
var lvl := save.meta_state.attune_state().refresh_level
|
||||
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(lvl + 1)
|
||||
return {
|
||||
"slots": Attunement.slots(db, save),
|
||||
@@ -549,15 +549,15 @@ func attune_camp_status() -> Dictionary:
|
||||
|
||||
# 每冒险开局刷新次数 Y(当前等级行表值;0 级 = 0 次即无刷新能力——P1 甲初始 0 同构)
|
||||
func attune_refresh_charges() -> int:
|
||||
return int(db.meta_attune_refresh_row(save.attune_refresh_level()).get("refresh_charges", 0))
|
||||
return int(db.meta_attune_refresh_row(save.meta_state.attune_state().refresh_level).get("refresh_charges", 0))
|
||||
|
||||
|
||||
# Y 升级购买(直购价 = 下一级行表值 keystone_cost;已满级/余额不足 false 零副作用——buy_forge 同款)
|
||||
func buy_attune_refresh() -> bool:
|
||||
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(save.attune_refresh_level() + 1)
|
||||
var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(save.meta_state.attune_state().refresh_level + 1)
|
||||
if next_row.is_empty():
|
||||
return false
|
||||
if not save.spend_keystone(int(next_row.get("keystone_cost", 0))):
|
||||
if not save.meta_state.spend_keystone(int(next_row.get("keystone_cost", 0))):
|
||||
return false
|
||||
save.set_attune_refresh_level(save.attune_refresh_level() + 1)
|
||||
save.meta_state.set_attune_refresh_level(save.meta_state.attune_state().refresh_level + 1)
|
||||
return save.write_to_disk()
|
||||
@@ -0,0 +1,176 @@
|
||||
## 存档内元进度数据件(D3 拆件——自 save_manager 收编元进度族):存档内元进度数据的读写唯一层。
|
||||
## 数据读写原语族(写口 13)+只读投影(读 6——tree/forge/attune/career 四域值拷贝出户);
|
||||
## 业务规则与写口编排归 MetaProgression,本件只承载键域读写(照单承载,零业务判定)。
|
||||
## 零 _data 副本零缓存——全部读写经注入 save 的内存数据模型单点。
|
||||
class_name MetaStateStore
|
||||
extends RefCounted
|
||||
|
||||
var _save # SaveManagerCore 注入引用(_data 唯一取数点)
|
||||
|
||||
|
||||
func _init(save_ref) -> void:
|
||||
_save = save_ref
|
||||
|
||||
|
||||
# ---- 写口原语族(13)----
|
||||
|
||||
# 钥石消费唯一口:余额不足拒绝且零副作用(钥石只增不减——消费除外,元进度验收判据)
|
||||
func spend_keystone(n: int) -> bool:
|
||||
if keystone_balance() < n:
|
||||
return false
|
||||
_save._data["meta"]["keystone_balance"] = keystone_balance() - n
|
||||
return true
|
||||
|
||||
|
||||
func unlock_item(item_id: String) -> void:
|
||||
_save._data["meta"]["item_unlocks"][item_id] = true
|
||||
|
||||
|
||||
func set_tree_level(node_id: String, level: int) -> void:
|
||||
var tn: Dictionary[Variant, Variant] = _save._data["meta"].get("tree_nodes", {}) as Dictionary
|
||||
tn[node_id] = maxi(0, level)
|
||||
_save._data["meta"]["tree_nodes"] = tn
|
||||
|
||||
|
||||
func set_fork_side(group: String, side: String) -> void:
|
||||
var fs: Dictionary[Variant, Variant] = _save._data["meta"].get("fork_sides", {}) as Dictionary
|
||||
fs[group] = side
|
||||
_save._data["meta"]["fork_sides"] = fs
|
||||
|
||||
|
||||
func set_forge_route_level(route_id: String, level: int) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var lv: Dictionary[Variant, Variant] = dom.get("levels", {}) as Dictionary
|
||||
lv[route_id] = maxi(0, level)
|
||||
dom["levels"] = lv
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func add_forge_unlock(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var un: Array[Variant] = dom.get("unlocked", []) as Array
|
||||
if not un.has(route_id):
|
||||
un.append(route_id)
|
||||
dom["unlocked"] = un
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func set_forge_weapon(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["weapon"] = route_id
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func set_forge_armor(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["armor"] = route_id
|
||||
_save._data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func set_attunement_equipped(item_ids: Array) -> void:
|
||||
var out: Array[Variant] = []
|
||||
for v in item_ids:
|
||||
out.append(String(v))
|
||||
_save._data["meta"]["attunement_equipped"] = out
|
||||
|
||||
|
||||
func set_attune_refresh_level(n: int) -> void:
|
||||
_save._data["meta"]["attune_refresh_level"] = maxi(0, n)
|
||||
|
||||
|
||||
# 真视类工具累计使用入账(写入唯一经本口冒险收束聚合,受理计数点=battle 层经冒险层聚合)
|
||||
func add_vision_used(n: int) -> void:
|
||||
_save._data["meta"]["vision_used"] = maxi(0, career_state()["vision_used"] + maxi(0, n))
|
||||
|
||||
|
||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||
# 钥石入账(summary.keystone_total——逐杀掉落已在冒险内计入,boss_drops 参数为
|
||||
# 摘要归档副本不重复入账)+ 逐关卡完成关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计
|
||||
# (同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附
|
||||
# keystone_drops 副本——复盘可归因)+ 生涯终身计数自增(总局数开局即计:
|
||||
# 三结局同经本口恰计一次;冒险成功局数按 summary.cleared 判定三关全通才计 1。终身计数写入
|
||||
# 单口=本函数,禁第二写入点)。
|
||||
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
|
||||
_save._data["meta"]["adventure_total"] = career_state()["adventure_total"] + 1
|
||||
if bool(summary.get("cleared", false)):
|
||||
_save._data["meta"]["adventure_cleared"] = career_state()["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):
|
||||
var key := str(li)
|
||||
_save._data["meta"]["level_clear_counts"][key] = int(_save._data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
||||
_save._data["meta"]["boss_first_kill"][key] = true
|
||||
_save._data["meta"]["boss_kill_count"] = career_state()["boss_kill_count"] + cleared # Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复)
|
||||
var entry: Dictionary[Variant, Variant] = summary.duplicate(true)
|
||||
entry["boss_drops"] = boss_drops.duplicate(true)
|
||||
var arr: Array[Variant] = _save._data["meta"]["recent_summaries"]
|
||||
arr.push_front(entry)
|
||||
while arr.size() > _save.SUMMARY_CAP:
|
||||
arr.pop_back()
|
||||
|
||||
|
||||
func mark_first_adventure_done() -> void:
|
||||
_save._data["meta"]["first_adventure_done"] = true
|
||||
|
||||
|
||||
# 钥石入账原语(生产消费零——apply_adventure_summary 专用;测量面经 _ 前缀直调)
|
||||
func _add_keystone(n: int) -> void:
|
||||
_save._data["meta"]["keystone_balance"] = keystone_balance() + n
|
||||
|
||||
|
||||
# ---- 只读投影族(6——值拷贝出户,禁碰内部结构契约同款)----
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return int(_save._data["meta"]["keystone_balance"])
|
||||
|
||||
|
||||
func item_unlocked(item_id: String) -> bool:
|
||||
return _save._data["meta"]["item_unlocks"].has(item_id)
|
||||
|
||||
|
||||
## 天赋树域只读投影:{nodes: node_id→已购等级, fork_sides: fork_group→"a"|"b"}
|
||||
func tree_state() -> Dictionary:
|
||||
return {
|
||||
"nodes": (_save._data["meta"].get("tree_nodes", {}) as Dictionary).duplicate(),
|
||||
"fork_sides": (_save._data["meta"].get("fork_sides", {}) as Dictionary).duplicate(),
|
||||
}
|
||||
|
||||
|
||||
## 锻造域只读投影:{levels: 六路线等级, unlocked: 直购清单, weapon/armor: 当前选中}
|
||||
## (解锁态不落档——派生判定单点在 MetaProgression.forge_route_unlocked)
|
||||
func forge_state() -> Dictionary:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
return {
|
||||
"levels": (dom.get("levels", {}) as Dictionary).duplicate(),
|
||||
"unlocked": (dom.get("unlocked", []) as Array).duplicate(),
|
||||
"weapon": String(dom.get("weapon", "sword")),
|
||||
"armor": String(dom.get("armor", "light")),
|
||||
}
|
||||
|
||||
|
||||
## 同调域只读投影:{equipped: 装配 item_id 有序数组(装配序即呈现序), refresh_level: Y 升级等级}
|
||||
func attune_state() -> Dictionary:
|
||||
return {
|
||||
"equipped": (_save._data["meta"].get("attunement_equipped", []) as Array).duplicate(),
|
||||
"refresh_level": int(_save._data["meta"].get("attune_refresh_level", 0)),
|
||||
}
|
||||
|
||||
|
||||
## 生涯域只读投影:{clear_counts, boss_first_kill, boss_kill_count, adventure_total,
|
||||
## adventure_cleared, vision_used, recent_summaries, first_adventure_done}
|
||||
func career_state() -> Dictionary:
|
||||
return {
|
||||
"clear_counts": (_save._data["meta"]["level_clear_counts"] as Dictionary).duplicate(),
|
||||
"boss_first_kill": (_save._data["meta"]["boss_first_kill"] as Dictionary).duplicate(),
|
||||
"boss_kill_count": int(_save._data["meta"].get("boss_kill_count", 0)),
|
||||
"adventure_total": int(_save._data["meta"].get("adventure_total", 0)),
|
||||
"adventure_cleared": int(_save._data["meta"].get("adventure_cleared", 0)),
|
||||
"vision_used": int(_save._data["meta"].get("vision_used", 0)),
|
||||
"recent_summaries": (_save._data["meta"]["recent_summaries"] as Array).duplicate(true),
|
||||
"first_adventure_done": bool(_save._data["meta"].get("first_adventure_done", true)),
|
||||
}
|
||||
|
||||
|
||||
# 锻造域取数(本件内部单点)
|
||||
func _forge_domain() -> Dictionary:
|
||||
return _save._data["meta"].get("forge_routes", {}) as Dictionary
|
||||
+143
-519
@@ -4,6 +4,8 @@
|
||||
## 禁 preload/import 任何玩法脚本、禁碰 ConfigDB/RngService——须可独立实例化独立验证。
|
||||
## 新增存档键先在序列化函数登记并升版本号;键域内容口径归各 GDD 文档,本类照单承载。
|
||||
## 存档键清单的事实源=本类序列化/读回代码,不在任何文档维护第二份键清单。
|
||||
## 拆件形态(D3 存档域):槽位文件域归 SlotStore(slots)、存档内元进度数据读写归 MetaStateStore
|
||||
## (meta_state)——本类留磁盘 IO+版本迁移链+断点域+门面装配,消费方经 save.slots / save.meta_state 直连。
|
||||
class_name SaveManagerCore
|
||||
extends Node
|
||||
|
||||
@@ -18,18 +20,18 @@ const SLOT_PATH_TEMPLATE := "user://save_slot_%d.json" # 槽文件名唯一派
|
||||
const LEGACY_SAVE_PATH := "user://save.json" # 旧隐式单档路径(Q1 甲:首次启动一次性字节迁入槽 1,此后无任何隐式读写)
|
||||
const LEGACY_MIGRATED_SUFFIX := ".migrated.bak" # 迁移后旧档改名留痕后缀(字节级留盘不删——处置留痕)
|
||||
|
||||
var _path: String # 当前写盘目标=激活槽文件(或 attach_path 注入路径);""=无激活态——一切写盘路径拒绝
|
||||
var _active_slot := 0 # 激活槽号 1..SLOT_COUNT;0=无激活态(注入非槽派生名路径时亦 0——写盘判定以 _path 非空为准)
|
||||
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→v6 生产迁移链现行——_init 自注册)
|
||||
var _slot_template := SLOT_PATH_TEMPLATE # 槽文件名模板(生产=常量;测试经 attach_slot_template 注入隔离命名空间,禁在呈现层/探针各自拼槽名)
|
||||
var slots: SlotStore # 槽位文件域件(槽位挂载与模板/激活与建删/摘要窥视——架构 :89)
|
||||
var meta_state: MetaStateStore # 存档内元进度数据读写唯一层(写口原语+只读投影——架构 :90)
|
||||
var _migration_fail_reason := "" # 迁移循环失败原因带出(_peek_migrated 返 null 时 load 侧落 corrupt 兜底文案单点)
|
||||
|
||||
|
||||
func _init(path: String = "") -> void:
|
||||
_path = path
|
||||
_active_slot = _slot_index_of(path)
|
||||
_data = _default_data()
|
||||
slots = SlotStore.new(self, path)
|
||||
meta_state = MetaStateStore.new(self)
|
||||
# 生产迁移链自注册(v2 起——测试钩 register_migration 仍可叠挂更早版本步骤)
|
||||
register_migration(1, _migrate_v1_v2)
|
||||
register_migration(2, _migrate_v2_v3)
|
||||
@@ -43,176 +45,103 @@ func _init(path: String = "") -> void:
|
||||
# 类名加 Core 后缀、autoload 占净名)。独立 .new() 实例(单测/工具)不入树零 _ready,
|
||||
# 读盘仍由调用方显式触发。
|
||||
# 启动态钉死(卡① sv2):autoload 启动不读任何槽——内存=默认合法空态(_init 置好)、
|
||||
# 无激活态(_path="",一切写盘路径拒绝并返回 false);玩家侧经选档界面显式进档/新档后才可写。
|
||||
# 无激活态(注入路径空,一切写盘路径拒绝并返回 false);玩家侧经选档界面显式进档/新档后才可写。
|
||||
# 旧隐式单档一次性迁入槽 1(Q1 甲,sv12)在本口执行。
|
||||
func _ready() -> void:
|
||||
_migrate_legacy_save(LEGACY_SAVE_PATH, slot_path(1), LEGACY_SAVE_PATH + LEGACY_MIGRATED_SUFFIX)
|
||||
slots._migrate_legacy_save(LEGACY_SAVE_PATH, slots.slot_path(1), LEGACY_SAVE_PATH + LEGACY_MIGRATED_SUFFIX)
|
||||
|
||||
|
||||
# 原地重置(探针隔离面,仿 RngService.set_master_seed 单例原地重置:
|
||||
# 换路径+回默认数据+重读盘;对象引用恒定,MetaProgression 持有的 save 引用零失效)。
|
||||
# 三槽化后单路径语义(契约 7)=注入路径即激活槽文件(写盘落该路径、无激活态时注入即激活);
|
||||
# 注入槽派生名时激活号同步记账,注入任意隔离路径时激活号 0(写盘仍合法——隔离面行为零变化)。
|
||||
func attach_path(path: String) -> void:
|
||||
_path = path
|
||||
# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约:
|
||||
# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v6,_init 自注册),
|
||||
# 冒烟经本口验证链式升级。
|
||||
func register_migration(from_version: int, fn: Callable) -> void:
|
||||
_migrations[from_version] = fn
|
||||
|
||||
|
||||
# ---- 读盘(进档/注入时调用;启动态不读——_ready 已钉死不隐式激活)----
|
||||
# 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。
|
||||
# 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。
|
||||
func load_from_disk() -> bool:
|
||||
_read_only = false
|
||||
_active_slot = _slot_index_of(path)
|
||||
_data = _default_data()
|
||||
load_from_disk()
|
||||
|
||||
|
||||
# 测试隔离注入口(契约 7 同源——槽文件名模板整体换注,探针/单测经此隔离到自命名空间,
|
||||
# 禁各自拼槽文件名;换注后回无激活态,槽原语全部落注入命名空间)
|
||||
func attach_slot_template(template: String) -> void:
|
||||
_slot_template = template
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_read_only = false
|
||||
_data = _default_data()
|
||||
|
||||
|
||||
# ---- 三存档位承载(卡①:隐式单档 → 3 存档位;位数与文件名口径=SLOT_COUNT/SLOT_PATH_TEMPLATE)----
|
||||
|
||||
# 槽文件路径唯一派生口(模板单点——禁第二处拼名)
|
||||
func slot_path(slot: int) -> String:
|
||||
return _slot_template % slot
|
||||
|
||||
|
||||
# 激活槽号查询(0 = 无激活态——启动态钉死后 boot 需经选档显式进档/新档,卡③ Q1 甲)
|
||||
func active_slot() -> int:
|
||||
return _active_slot
|
||||
|
||||
|
||||
# 写盘目标在位与否(激活槽或注入路径任一;无激活态一切写盘路径拒绝——sv3 判定口)
|
||||
func has_write_target() -> bool:
|
||||
return _path != ""
|
||||
|
||||
|
||||
# 进档原语(sv5,契约 6/8):激活槽 N+读盘,复用 load_from_disk 迁移/兜底链按槽成立。
|
||||
# 高版本拒读槽:激活不成立、恢复切换前三态(路径/激活号/禁写位/数据)——防误写他槽;
|
||||
# 损坏槽:兜底链接管(.corrupt.bak 改名+空态继续,激活成立)。
|
||||
func activate_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro := _read_only
|
||||
var prev_data := _data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_read_only = false
|
||||
_data = _default_data()
|
||||
if not load_from_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_read_only = prev_ro
|
||||
_data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 新档原语(sv4):空槽创建=合法白板态(_default_data 全零可玩纪律),创建即激活并落盘。
|
||||
# 生产路径只在空槽调用(有档槽开新局=先删档回空再新档,卡③ 契约 4);写盘失败回退切换前三态。
|
||||
func create_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro := _read_only
|
||||
var prev_data := _data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_read_only = false
|
||||
_data = _default_data()
|
||||
if not write_to_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_read_only = prev_ro
|
||||
_data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 删档原语(sv6,契约 8):该槽文件删除+盘面即时生效(卡内工程规定,挪用现役断点覆盖流语义)
|
||||
# +无 .tmp 残留;删激活槽时激活指针连清除(内存回默认空态,该槽不再可写——激活态生命周期终点)。
|
||||
# .corrupt.bak 损坏备份不追删(用户数据留盘可考;同槽再损坏时兜底流按槽覆盖同名备份)。
|
||||
# 删不存在文件的空槽=幂等成功;越界槽号拒绝。
|
||||
func delete_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return false
|
||||
var p := slot_path(slot)
|
||||
if FileAccess.file_exists(p) and not _remove_file(p):
|
||||
push_error("[存档序列化] 删档失败(文件删除被拒):%s" % p)
|
||||
return false
|
||||
var tmp := p + ".tmp"
|
||||
if FileAccess.file_exists(tmp):
|
||||
_remove_file(tmp) # 残留清偿尽力而为(主文件已删即达成空槽语义)
|
||||
if _active_slot == slot:
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_data = _default_data()
|
||||
return true
|
||||
|
||||
|
||||
# 摘要三项按槽窥视读器(sv7/sv8,契约 5/9):钥石余额/冒险成功局数/冒险总局数。
|
||||
# 只读零副作用:不激活、不写盘、不触发 _corrupt_fallback 改名(.corrupt.bak 只在正式进档路径产生)。
|
||||
# 返回 {"readable", "has_file", "keystone", "cleared", "total"}——readable=true=可读槽(空槽=零值合法态,
|
||||
# .tmp 残留按空槽——主文件不在即空);readable=false=不可读槽(损坏/高版本/缺版本字段/打开失败,
|
||||
# 三值无意义——卡③ Q2 裁甲:UI 按空槽呈现,进档时走正式 load 兜底/拒读链);
|
||||
# has_file=主文件在盘与否(空/有档操作集二分判定来处——卡③ T0 对齐结论;非摘要第四项,零 UI 语义)。
|
||||
# 迁移链在此只读推演(生产迁移 Callable 为纯函数,零盘面副作用)。
|
||||
func peek_slot_summary(slot: int) -> Dictionary:
|
||||
if slot < 1 or slot > SLOT_COUNT:
|
||||
return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽
|
||||
return _peek_summary_at(slot_path(slot))
|
||||
|
||||
|
||||
# 窥视注入口(契约 7 隔离面同款——测试/探针经路径注入隔离,禁各自拼槽文件名)
|
||||
func peek_summary_at(path: String) -> Dictionary:
|
||||
return _peek_summary_at(path)
|
||||
|
||||
|
||||
func _peek_summary_at(path: String) -> Dictionary:
|
||||
var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0}
|
||||
if not FileAccess.file_exists(path):
|
||||
out["readable"] = true # 空槽=可读零值合法态
|
||||
return out
|
||||
out["has_file"] = true # 主文件在盘(可读性与占档判定分离——白板档与空槽自此可区分)
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if not FileAccess.file_exists(slots._path):
|
||||
_data = _default_data() # 新档合法态
|
||||
return true
|
||||
var f := FileAccess.open(slots._path, FileAccess.READ)
|
||||
if f == null:
|
||||
return out
|
||||
return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error()))
|
||||
return _load_parsed(f)
|
||||
|
||||
|
||||
## 读盘解析域(load_from_disk 主体段提取——解析/版本/迁移/规整;corrupt_fallback 调用点随段迁移,副作用序逐字)
|
||||
func _load_parsed(f: FileAccess) -> bool:
|
||||
var text := f.get_as_text()
|
||||
f.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("version"):
|
||||
return out
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return _corrupt_fallback("JSON 解析失败或顶层结构出域(非对象)")
|
||||
if not parsed.has("version"):
|
||||
return _corrupt_fallback("缺版本字段")
|
||||
var v := int(parsed["version"])
|
||||
if v > SAVE_FORMAT_VERSION:
|
||||
return out
|
||||
while v < SAVE_FORMAT_VERSION:
|
||||
if not _migrations.has(v):
|
||||
return out
|
||||
var stepped = _migrations[v].call(parsed)
|
||||
if typeof(stepped) != TYPE_DICTIONARY:
|
||||
return out
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
var norm := _normalize(parsed)
|
||||
out["readable"] = true
|
||||
out["keystone"] = int(norm["meta"].get("keystone_balance", 0))
|
||||
out["cleared"] = int(norm["meta"].get("adventure_cleared", 0))
|
||||
out["total"] = int(norm["meta"].get("adventure_total", 0))
|
||||
return out
|
||||
_read_only = true
|
||||
_data = _default_data()
|
||||
push_error("[存档序列化] 档案版本 %d 高于当前 %d——拒读禁写(保护未来档不被覆写,内存默认新档运行)" % [v, SAVE_FORMAT_VERSION])
|
||||
return false
|
||||
var fail := []
|
||||
var migrated = _peek_migrated(parsed, fail)
|
||||
if migrated == null:
|
||||
return _corrupt_fallback(fail[0])
|
||||
_data = _normalize(migrated)
|
||||
# 老档迁移(关卡节点图生成 v0.4 布尔位——缺字段 = 已有游玩史,不重放教学脚本)
|
||||
if not _data["meta"].has("first_adventure_done"):
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
return true
|
||||
|
||||
|
||||
# 注入路径的槽号反查(命中槽派生名返回槽号,否则 0——激活号记账用,写盘判定以 _path 非空为准)
|
||||
func _slot_index_of(path: String) -> int:
|
||||
for i in range(1, SLOT_COUNT + 1):
|
||||
if path == slot_path(i):
|
||||
return i
|
||||
return 0
|
||||
# ---- 写盘(tmp + rename 原子写——写一半断电不留烂档)----
|
||||
# 无激活态一律拒绝并返回 false(sv3:启动态不隐式激活,盘面零触碰);
|
||||
# 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。
|
||||
func write_to_disk() -> bool:
|
||||
if slots._path == "":
|
||||
push_error("[存档序列化] 写盘拒绝——无激活存档位(启动态不隐式激活;经选档进档/新档后落盘)")
|
||||
return false
|
||||
if _read_only:
|
||||
push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)")
|
||||
return false
|
||||
var tmp := slots._path + ".tmp"
|
||||
var f := FileAccess.open(tmp, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())])
|
||||
return false
|
||||
f.store_string(JSON.stringify(_data, "\t", true))
|
||||
f.close()
|
||||
if not _rename_overwrite(tmp, slots._path):
|
||||
push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, slots._path])
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# ---- 冒险断点域(停点粒度续玩快照;内容装配归 AdventureRunner,本类照单承载)----
|
||||
# 空字典 = 无断点合法态。值拷贝进出(禁碰内部结构契约同款)——快照写入后调用方再改不渗入存档态。
|
||||
|
||||
func set_breakpoint(bp: Dictionary) -> void:
|
||||
_data["breakpoint"] = bp.duplicate(true)
|
||||
|
||||
|
||||
func clear_breakpoint() -> void:
|
||||
_data["breakpoint"] = {}
|
||||
|
||||
|
||||
func has_breakpoint() -> bool:
|
||||
return not (_data.get("breakpoint", {}) as Dictionary).is_empty()
|
||||
|
||||
|
||||
func breakpoint_snapshot() -> Dictionary:
|
||||
return (_data.get("breakpoint", {}) as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
# ---- 私有:迁移链 / 损坏兜底 / 规整 / 文件原语 ----
|
||||
|
||||
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
|
||||
func _migrate_v1_v2(data: Dictionary) -> Dictionary:
|
||||
if not data.has("breakpoint"):
|
||||
@@ -266,6 +195,53 @@ func _migrate_v6_v7(data: Dictionary) -> Dictionary:
|
||||
return data
|
||||
|
||||
|
||||
# 迁移循环提取(load/peek 两调用方同构共享):成功=迁移完成 Dictionary;失败=null
|
||||
# (原因经 fail 数组带出——load 侧落 corrupt 兜底文案、peek 侧保持「返回未填充 out」逐字)
|
||||
func _peek_migrated(parsed: Variant, fail: Array) -> Variant:
|
||||
var v := int(parsed["version"])
|
||||
while v < SAVE_FORMAT_VERSION:
|
||||
if not _migrations.has(v):
|
||||
fail.append("迁移链缺步骤 %d→%d" % [v, v + 1])
|
||||
return null
|
||||
var stepped = _migrations[v].call(parsed)
|
||||
if typeof(stepped) != TYPE_DICTIONARY:
|
||||
fail.append("迁移步骤 %d→%d 执行失败" % [v, v + 1])
|
||||
return null
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
return parsed
|
||||
|
||||
|
||||
## 摘要窥视内核(只读推演单点——SlotStore 两 peek 方法经槽路径派生委托至此;
|
||||
## 迁移链在此只读推演——生产迁移 Callable 为纯函数,零盘面副作用)
|
||||
func _peek_summary_at(path: String) -> Dictionary:
|
||||
var out := {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0}
|
||||
if not FileAccess.file_exists(path):
|
||||
out["readable"] = true # 空槽=可读零值合法态
|
||||
return out
|
||||
out["has_file"] = true # 主文件在盘(可读性与占档判定分离——白板档与空槽自此可区分)
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
return out
|
||||
var text := f.get_as_text()
|
||||
f.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) != TYPE_DICTIONARY or not parsed.has("version"):
|
||||
return out
|
||||
if int(parsed["version"]) > SAVE_FORMAT_VERSION:
|
||||
return out
|
||||
var fail := []
|
||||
var migrated = _peek_migrated(parsed, fail)
|
||||
if migrated == null:
|
||||
return out
|
||||
var norm := _normalize(migrated)
|
||||
out["readable"] = true
|
||||
out["keystone"] = int(norm["meta"].get("keystone_balance", 0))
|
||||
out["cleared"] = int(norm["meta"].get("adventure_cleared", 0))
|
||||
out["total"] = int(norm["meta"].get("adventure_total", 0))
|
||||
return out
|
||||
|
||||
|
||||
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
|
||||
|
||||
func _default_data() -> Dictionary[String, Variant]:
|
||||
@@ -299,368 +275,15 @@ func _default_data() -> Dictionary[String, Variant]:
|
||||
}
|
||||
|
||||
|
||||
# ---- 首次冒险布尔位(关卡节点图生成 v1.1 确定性承载)----
|
||||
# false = 本档尚无游玩史 → 首局走固定种子 42 + 参数档生成图(教学与存档复现的固定图由此承载,无兜底模板);
|
||||
# 置位只在冒险收束挂点(MetaProgression.settle_adventure)。
|
||||
# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放首局固定图)。
|
||||
# 原地重置默认态(SlotStore 槽位原语的协作口——激活/建删/换注回默认数据)
|
||||
func _reset_to_default() -> void:
|
||||
_data = _default_data()
|
||||
|
||||
func is_first_adventure_done() -> bool:
|
||||
return bool(_data["meta"].get("first_adventure_done", true))
|
||||
|
||||
|
||||
func mark_first_adventure_done() -> void:
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
|
||||
|
||||
# ---- 读盘(进档/注入时调用;启动态不读——_ready 已钉死不隐式激活)----
|
||||
# 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。
|
||||
# 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。
|
||||
func load_from_disk() -> bool:
|
||||
_read_only = false
|
||||
if not FileAccess.file_exists(_path):
|
||||
_data = _default_data() # 新档合法态
|
||||
return true
|
||||
var f := FileAccess.open(_path, FileAccess.READ)
|
||||
if f == null:
|
||||
return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error()))
|
||||
var text := f.get_as_text()
|
||||
f.close()
|
||||
var parsed = JSON.parse_string(text)
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return _corrupt_fallback("JSON 解析失败或顶层结构出域(非对象)")
|
||||
if not parsed.has("version"):
|
||||
return _corrupt_fallback("缺版本字段")
|
||||
var v := int(parsed["version"])
|
||||
if v > SAVE_FORMAT_VERSION:
|
||||
_read_only = true
|
||||
_data = _default_data()
|
||||
push_error("[存档序列化] 档案版本 %d 高于当前 %d——拒读禁写(保护未来档不被覆写,内存默认新档运行)" % [v, SAVE_FORMAT_VERSION])
|
||||
return false
|
||||
while v < SAVE_FORMAT_VERSION:
|
||||
if not _migrations.has(v):
|
||||
return _corrupt_fallback("迁移链缺步骤 %d→%d" % [v, v + 1])
|
||||
var stepped = _migrations[v].call(parsed)
|
||||
if typeof(stepped) != TYPE_DICTIONARY:
|
||||
return _corrupt_fallback("迁移步骤 %d→%d 执行失败" % [v, v + 1])
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
_data = _normalize(parsed)
|
||||
# 老档迁移(关卡节点图生成 v0.4 布尔位——缺字段 = 已有游玩史,不重放教学脚本)
|
||||
if not _data["meta"].has("first_adventure_done"):
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
return true
|
||||
|
||||
|
||||
# ---- 写盘(tmp + rename 原子写——写一半断电不留烂档)----
|
||||
# 无激活态一律拒绝并返回 false(sv3:启动态不隐式激活,盘面零触碰);
|
||||
# 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。
|
||||
func write_to_disk() -> bool:
|
||||
if _path == "":
|
||||
push_error("[存档序列化] 写盘拒绝——无激活存档位(启动态不隐式激活;经选档进档/新档后落盘)")
|
||||
return false
|
||||
if _read_only:
|
||||
push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)")
|
||||
return false
|
||||
var tmp := _path + ".tmp"
|
||||
var f := FileAccess.open(tmp, FileAccess.WRITE)
|
||||
if f == null:
|
||||
push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())])
|
||||
return false
|
||||
f.store_string(JSON.stringify(_data, "\t", true))
|
||||
f.close()
|
||||
if not _rename_overwrite(tmp, _path):
|
||||
push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, _path])
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约:
|
||||
# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v6,_init 自注册),
|
||||
# 冒烟经本口验证链式升级。
|
||||
func register_migration(from_version: int, fn: Callable) -> void:
|
||||
_migrations[from_version] = fn
|
||||
|
||||
|
||||
# ---- 类型化域访问器(消费面——只经访问器操作,禁碰内部结构)----
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return int(_data["meta"]["keystone_balance"])
|
||||
|
||||
|
||||
func add_keystone(n: int) -> void:
|
||||
_data["meta"]["keystone_balance"] = keystone_balance() + n
|
||||
|
||||
|
||||
# 钥石消费唯一口:余额不足拒绝且零副作用(钥石只增不减——消费除外,元进度验收判据)
|
||||
func spend_keystone(n: int) -> bool:
|
||||
if keystone_balance() < n:
|
||||
return false
|
||||
_data["meta"]["keystone_balance"] = keystone_balance() - n
|
||||
return true
|
||||
|
||||
|
||||
func item_unlocked(item_id: String) -> bool:
|
||||
return _data["meta"]["item_unlocks"].has(item_id)
|
||||
|
||||
|
||||
func unlock_item(item_id: String) -> void:
|
||||
_data["meta"]["item_unlocks"][item_id] = true
|
||||
|
||||
|
||||
func clear_count(level_index: int) -> int:
|
||||
return int(_data["meta"]["level_clear_counts"].get(str(level_index), 0))
|
||||
|
||||
|
||||
func boss_first_kill_marked(level_index: int) -> bool:
|
||||
return _data["meta"]["boss_first_kill"].has(str(level_index))
|
||||
|
||||
|
||||
# ---- 同调位账本域(里程碑谓词表计数源 + 装配关系 + Y 升级态)----
|
||||
|
||||
# Boss 击杀总数(含重复——底稿裁决 4 计数源;收束入账 = apply_adventure_summary 逐关卡完成关次 +1,
|
||||
# Boss 恒唯一终点故 levels_cleared 即本次冒险 Boss 击杀数)
|
||||
func boss_kill_count() -> int:
|
||||
return int(_data["meta"].get("boss_kill_count", 0))
|
||||
|
||||
|
||||
# ---- 生涯终身计数域(跨局汇总页数据源;只读面经访问器,
|
||||
# 写入唯一经 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))
|
||||
|
||||
|
||||
# 真视类工具累计使用次数(信标网络解锁行为条件;写入唯一经 add_vision_used
|
||||
# 冒险收束聚合入账,受理计数点=battle 层经冒险层聚合)
|
||||
func vision_used() -> int:
|
||||
return int(_data["meta"].get("vision_used", 0))
|
||||
|
||||
|
||||
func add_vision_used(n: int) -> void:
|
||||
_data["meta"]["vision_used"] = maxi(0, vision_used() + maxi(0, n))
|
||||
|
||||
|
||||
# ---- 天赋树域(MetaProgression 树段读写面)----
|
||||
|
||||
func tree_nodes() -> Dictionary:
|
||||
return (_data["meta"].get("tree_nodes", {}) as Dictionary).duplicate()
|
||||
|
||||
|
||||
func tree_level(node_id: String) -> int:
|
||||
return int((_data["meta"].get("tree_nodes", {}) as Dictionary).get(node_id, 0))
|
||||
|
||||
|
||||
func set_tree_level(node_id: String, level: int) -> void:
|
||||
var tn: Dictionary[Variant, Variant] = _data["meta"].get("tree_nodes", {}) as Dictionary
|
||||
tn[node_id] = maxi(0, level)
|
||||
_data["meta"]["tree_nodes"] = tn
|
||||
|
||||
|
||||
func fork_sides() -> Dictionary:
|
||||
return (_data["meta"].get("fork_sides", {}) as Dictionary).duplicate()
|
||||
|
||||
|
||||
func fork_side(group: String) -> String:
|
||||
return String((_data["meta"].get("fork_sides", {}) as Dictionary).get(group, ""))
|
||||
|
||||
|
||||
func set_fork_side(group: String, side: String) -> void:
|
||||
var fs: Dictionary[Variant, Variant] = _data["meta"].get("fork_sides", {}) as Dictionary
|
||||
fs[group] = side
|
||||
_data["meta"]["fork_sides"] = fs
|
||||
|
||||
|
||||
# ---- 锻造工坊域 forge_routes(MetaProgression 锻造写口读写面;无特效域)----
|
||||
# 解锁态不落档(派生):初始持有=表 initial_owned、练满前一档=levels[prereq]≥cap——判定单点在
|
||||
# MetaProgression.forge_route_unlocked(读表+读档);本类只承载直购清单 unlocked 与选中标记。
|
||||
# 域结构:{levels:{六路线 id→等级}, unlocked:[直购路线 id], weapon:当前武器路线 id, armor:当前甲路线 id}
|
||||
|
||||
func _forge_domain() -> Dictionary:
|
||||
return _data["meta"].get("forge_routes", {}) as Dictionary
|
||||
|
||||
|
||||
func forge_routes_snapshot() -> Dictionary:
|
||||
return _forge_domain().duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态)
|
||||
|
||||
|
||||
func forge_route_level(route_id: String) -> int:
|
||||
var lv: Dictionary[Variant, Variant] = _forge_domain().get("levels", {}) as Dictionary
|
||||
return int(lv.get(route_id, 0))
|
||||
|
||||
|
||||
func set_forge_route_level(route_id: String, level: int) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var lv: Dictionary[Variant, Variant] = dom.get("levels", {}) as Dictionary
|
||||
lv[route_id] = maxi(0, level)
|
||||
dom["levels"] = lv
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func forge_unlocked_purchases() -> Array:
|
||||
return (_forge_domain().get("unlocked", []) as Array).duplicate()
|
||||
|
||||
|
||||
func add_forge_unlock(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
var un: Array[Variant] = dom.get("unlocked", []) as Array
|
||||
if not un.has(route_id):
|
||||
un.append(route_id)
|
||||
dom["unlocked"] = un
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func forge_weapon() -> String:
|
||||
return String(_forge_domain().get("weapon", "sword"))
|
||||
|
||||
|
||||
func set_forge_weapon(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["weapon"] = route_id
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
func forge_armor() -> String:
|
||||
return String(_forge_domain().get("armor", "light"))
|
||||
|
||||
|
||||
func set_forge_armor(route_id: String) -> void:
|
||||
var dom: Dictionary[Variant, Variant] = _forge_domain()
|
||||
dom["armor"] = route_id
|
||||
_data["meta"]["forge_routes"] = dom
|
||||
|
||||
|
||||
# 装配关系(同调目录 item_id 有序数组——装配序即呈现序;跨冒险持久,冒险中途不可换装〔底稿 §8.5〕)。
|
||||
# 局外进度事实(元进度域同族),非契约 2 反面清单的「局内道具库存」——口径见 存档序列化.md 契约 2。
|
||||
func attunement_equipped() -> Array:
|
||||
return (_data["meta"].get("attunement_equipped", []) as Array).duplicate()
|
||||
|
||||
|
||||
func set_attunement_equipped(item_ids: Array) -> void:
|
||||
var out: Array[Variant] = []
|
||||
for v in item_ids:
|
||||
out.append(String(v))
|
||||
_data["meta"]["attunement_equipped"] = out
|
||||
|
||||
|
||||
# Y 刷新升级购买等级(0 = 未购 → 每冒险刷新次数 0;等级行值归营地升级行族)
|
||||
func attune_refresh_level() -> int:
|
||||
return int(_data["meta"].get("attune_refresh_level", 0))
|
||||
|
||||
|
||||
func set_attune_refresh_level(n: int) -> void:
|
||||
_data["meta"]["attune_refresh_level"] = maxi(0, n)
|
||||
|
||||
|
||||
# 同调位计数源组装(Attunement 服务取数口——clear_count = 逐关完成次数合计〔含重复关,底稿 §2.1 口径〕;
|
||||
# clear_count(level_index) 逐关访问器零改动复用)
|
||||
func recent_summaries() -> Array:
|
||||
return _data["meta"]["recent_summaries"].duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态)
|
||||
|
||||
|
||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||
# 钥石入账(summary.keystone_total——逐杀掉落已在冒险内计入,boss_drops 参数为
|
||||
# 摘要归档副本不重复入账)+ 逐关卡完成关次 clear_count +1 与 Boss 首杀标记 + Boss 击杀总数累计
|
||||
# (同调位里程碑计数源)+ 摘要头部插入(cap 10,最新在前,元素附
|
||||
# keystone_drops 副本——复盘可归因)+ 生涯终身计数自增(总局数开局即计:
|
||||
# 三结局同经本口恰计一次;冒险成功局数按 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):
|
||||
var key := str(li)
|
||||
_data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1
|
||||
_data["meta"]["boss_first_kill"][key] = true
|
||||
_data["meta"]["boss_kill_count"] = boss_kill_count() + cleared # Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复)
|
||||
var entry: Dictionary[Variant, Variant] = summary.duplicate(true)
|
||||
entry["boss_drops"] = boss_drops.duplicate(true)
|
||||
var arr: Array[Variant] = _data["meta"]["recent_summaries"]
|
||||
arr.push_front(entry)
|
||||
while arr.size() > SUMMARY_CAP:
|
||||
arr.pop_back()
|
||||
|
||||
|
||||
# ---- 冒险断点域(停点粒度续玩快照;内容装配归 AdventureRunner,本类照单承载)----
|
||||
# 空字典 = 无断点合法态。值拷贝进出(禁碰内部结构契约同款)——快照写入后调用方再改不渗入存档态。
|
||||
|
||||
func set_breakpoint(bp: Dictionary) -> void:
|
||||
_data["breakpoint"] = bp.duplicate(true)
|
||||
|
||||
|
||||
func clear_breakpoint() -> void:
|
||||
_data["breakpoint"] = {}
|
||||
|
||||
|
||||
func has_breakpoint() -> bool:
|
||||
return not (_data.get("breakpoint", {}) as Dictionary).is_empty()
|
||||
|
||||
|
||||
func breakpoint_snapshot() -> Dictionary:
|
||||
return (_data.get("breakpoint", {}) as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
# ---- 私有:损坏兜底与文件原语 ----
|
||||
|
||||
# 旧隐式单档一次性迁移(Q1 甲,sv12):src 字节级原样搬运到 dst(零解析零改造——损坏/高版本
|
||||
# 旧档字节原样落槽 1,读取时由正式进档路径的兜底/拒读链接管,不静默丢数据);
|
||||
# dst 已存在不重迁(幂等判据——判别锚④:去判据则重复迁移覆盖槽 1);迁移成功后 src 改名
|
||||
# bak 留痕(不删——处置字节留痕,防再次隐式读写)。任一步失败:源保留原地,下次启动重试。
|
||||
# tmp+rename 原子形态与 write_to_disk 同纪律。生产调用=src/dst/bak 为 旧单档/槽 1/留痕名。
|
||||
func _migrate_legacy_save(src: String, dst: String, bak: String) -> void:
|
||||
if not FileAccess.file_exists(src):
|
||||
return # 执行前置:旧档不存在=无操作
|
||||
if FileAccess.file_exists(dst):
|
||||
return # 幂等判据:槽 1 已存在不重迁
|
||||
var tmp := dst + ".tmp"
|
||||
if not _copy_file(src, tmp):
|
||||
push_warning("[存档序列化] 旧单档迁移失败(源保留,下次启动重试):%s → %s" % [src, tmp])
|
||||
return
|
||||
if not _rename_overwrite(tmp, dst):
|
||||
push_warning("[存档序列化] 旧单档迁移改名失败(tmp 保留待查,源保留重试):%s → %s" % [tmp, dst])
|
||||
return
|
||||
_rename_overwrite(src, bak)
|
||||
push_warning("[存档序列化] 旧单档已迁入 %s,原件留痕于 %s" % [dst, bak])
|
||||
|
||||
|
||||
# 字节级文件拷贝(迁移专用——buffer 直拷,零解析零改造)
|
||||
func _copy_file(from: String, to: String) -> bool:
|
||||
var src := FileAccess.open(from, FileAccess.READ)
|
||||
if src == null:
|
||||
return false
|
||||
var dst := FileAccess.open(to, FileAccess.WRITE)
|
||||
if dst == null:
|
||||
src.close()
|
||||
return false
|
||||
while not src.eof_reached():
|
||||
var chunk := src.get_buffer(65536)
|
||||
if chunk.size() > 0:
|
||||
dst.store_buffer(chunk)
|
||||
src.close()
|
||||
dst.close()
|
||||
return true
|
||||
|
||||
|
||||
# 文件删除(删档原语专用——呈现层禁 DirAccess 直删,承载面单点)
|
||||
func _remove_file(p: String) -> bool:
|
||||
var dir := DirAccess.open(p.get_base_dir())
|
||||
if dir == null:
|
||||
return false
|
||||
return dir.remove(p.get_file()) == OK
|
||||
|
||||
# 损坏兜底:坏档改名 .corrupt.bak(已存在则覆盖)→ 默认数据继续。不静默丢档(警告留痕)、不 crash。
|
||||
func _corrupt_fallback(reason: String) -> bool:
|
||||
var bak := _path + ".corrupt.bak"
|
||||
if not _rename_overwrite(_path, bak):
|
||||
var bak := slots._path + ".corrupt.bak"
|
||||
if not _rename_overwrite(slots._path, bak):
|
||||
push_warning("[存档序列化] 坏档备份改名失败(原文件保留原地):%s——%s" % [bak, reason])
|
||||
else:
|
||||
push_warning("[存档序列化] 检测到损坏档案,已备份为 %s——%s(用默认新档继续)" % [bak, reason])
|
||||
@@ -689,7 +312,8 @@ func _normalize(data: Dictionary) -> Dictionary[String, Variant]:
|
||||
return merged
|
||||
|
||||
|
||||
# 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义)
|
||||
# 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义;IO 原语单点——
|
||||
# SlotStore 迁移流经本件调用)
|
||||
func _rename_overwrite(from: String, to: String) -> bool:
|
||||
var dir := DirAccess.open(from.get_base_dir())
|
||||
if dir == null:
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
## 存档槽位文件域件(D3 拆件——自 save_manager 收编槽位族):槽位挂载与模板、激活与建删、
|
||||
## 摘要窥视(peek 族)的唯一所有者。槽位态(路径/激活号/模板)随件承载;数据重置与读写盘
|
||||
## 经注入的 save 内部协作家族(_reset_to_default/_load_parsed/_rename_overwrite)完成——
|
||||
## 本件零 _data 副本零缓存(load 整体换新不陈旧)。
|
||||
class_name SlotStore
|
||||
extends RefCounted
|
||||
|
||||
var _save # SaveManagerCore 注入引用(IO 与数据域协作单点)
|
||||
var _path: String = "" # 当前写盘目标=激活槽文件(或 attach_path 注入路径);""=无激活态——一切写盘路径拒绝
|
||||
var _active_slot := 0 # 激活槽号 1..SLOT_COUNT;0=无激活态(注入非槽派生名路径时亦 0——写盘判定以 _path 非空为准)
|
||||
var _slot_template: String # 槽文件名模板(生产=常量;测试经 attach_slot_template 注入隔离命名空间,禁在呈现层/探针各自拼槽名)
|
||||
|
||||
|
||||
func _init(save_ref, path: String = "") -> void:
|
||||
_save = save_ref
|
||||
_slot_template = _save.SLOT_PATH_TEMPLATE
|
||||
_path = path
|
||||
_active_slot = _slot_index_of(path)
|
||||
|
||||
|
||||
## 进档注入口:换路径+回默认数据+重读盘(对象引用恒定,MetaProgression 持有的 save 引用零失效)。
|
||||
## 三槽化后单路径语义(契约 7)=注入路径即激活槽文件(写盘落该路径、无激活态时注入即激活);
|
||||
## 注入槽派生名时激活号同步记账,注入任意隔离路径时激活号 0(写盘仍合法——隔离面行为零变化)。
|
||||
func attach_path(path: String) -> void:
|
||||
_path = path
|
||||
_save._read_only = false
|
||||
_active_slot = _slot_index_of(path)
|
||||
_save._reset_to_default()
|
||||
_save.load_from_disk()
|
||||
|
||||
|
||||
## 测试隔离注入口(契约 7 同源——槽文件名模板整体换注,探针/单测经此隔离到自命名空间,
|
||||
## 禁各自拼槽文件名;换注后回无激活态,槽原语全部落注入命名空间)
|
||||
func attach_slot_template(template: String) -> void:
|
||||
_slot_template = template
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_save._read_only = false
|
||||
_save._reset_to_default()
|
||||
|
||||
|
||||
# 槽文件路径唯一派生口(模板单点——禁第二处拼名)
|
||||
func slot_path(slot: int) -> String:
|
||||
return _slot_template % slot
|
||||
|
||||
|
||||
# 激活槽号查询(0 = 无激活态——启动态钉死后 boot 需经选档显式进档/新档)
|
||||
func active_slot() -> int:
|
||||
return _active_slot
|
||||
|
||||
|
||||
# 写盘目标在位与否(激活槽或注入路径任一;无激活态一切写盘路径拒绝——sv3 判定口)
|
||||
func has_write_target() -> bool:
|
||||
return _path != ""
|
||||
|
||||
|
||||
# 进档原语(sv5,契约 6/8):激活槽 N+读盘,复用 load_from_disk 迁移/兜底链按槽成立。
|
||||
# 高版本拒读槽:激活不成立、恢复切换前三态(路径/激活号/禁写位/数据)——防误写他槽;
|
||||
# 损坏槽:兜底链接管(.corrupt.bak 改名+空态继续,激活成立)。
|
||||
func activate_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro: bool = _save._read_only
|
||||
var prev_data = _save._data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_save._read_only = false
|
||||
_save._reset_to_default()
|
||||
if not _save.load_from_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_save._read_only = prev_ro
|
||||
_save._data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 新档原语(sv4):空槽创建=合法白板态(_default_data 全零可玩纪律),创建即激活并落盘。
|
||||
# 生产路径只在空槽调用(有档槽开新局=先删档回空再新档);写盘失败回退切换前三态。
|
||||
func create_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return false
|
||||
var prev_path := _path
|
||||
var prev_slot := _active_slot
|
||||
var prev_ro: bool = _save._read_only
|
||||
var prev_data = _save._data
|
||||
_path = slot_path(slot)
|
||||
_active_slot = slot
|
||||
_save._read_only = false
|
||||
_save._reset_to_default()
|
||||
if not _save.write_to_disk():
|
||||
_path = prev_path
|
||||
_active_slot = prev_slot
|
||||
_save._read_only = prev_ro
|
||||
_save._data = prev_data
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
# 删档原语(sv6,契约 8):该槽文件删除+盘面即时生效+无 .tmp 残留;删激活槽时激活指针连清除
|
||||
# (内存回默认空态,该槽不再可写——激活态生命周期终点)。
|
||||
# .corrupt.bak 损坏备份不追删(用户数据留盘可考;同槽再损坏时兜底流按槽覆盖同名备份)。
|
||||
# 删不存在文件的空槽=幂等成功;越界槽号拒绝。
|
||||
func delete_slot(slot: int) -> bool:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return false
|
||||
var p := slot_path(slot)
|
||||
if FileAccess.file_exists(p) and not _remove_file(p):
|
||||
push_error("[存档序列化] 删档失败(文件删除被拒):%s" % p)
|
||||
return false
|
||||
var tmp := p + ".tmp"
|
||||
if FileAccess.file_exists(tmp):
|
||||
_remove_file(tmp) # 残留清偿尽力而为(主文件已删即达成空槽语义)
|
||||
if _active_slot == slot:
|
||||
_path = ""
|
||||
_active_slot = 0
|
||||
_save._reset_to_default()
|
||||
return true
|
||||
|
||||
|
||||
# 摘要三项按槽窥视读器(sv7/sv8,契约 5/9):钥石余额/冒险成功局数/冒险总局数。
|
||||
# 只读零副作用:不激活、不写盘、不触发 _corrupt_fallback 改名(.corrupt.bak 只在正式进档路径产生)。
|
||||
# 迁移链在此只读推演(内核 _peek_summary_at 归 save IO 域——本方法经槽路径派生委托)。
|
||||
func peek_slot_summary(slot: int) -> Dictionary:
|
||||
if slot < 1 or slot > _save.SLOT_COUNT:
|
||||
return {"readable": false, "has_file": false, "keystone": 0, "cleared": 0, "total": 0} # 越界=非槽位,非空槽
|
||||
return _save._peek_summary_at(slot_path(slot))
|
||||
|
||||
|
||||
# 窥视注入口(契约 7 隔离面同款——测试/探针经路径注入隔离,禁各自拼槽文件名)
|
||||
func peek_summary_at(path: String) -> Dictionary:
|
||||
return _save._peek_summary_at(path)
|
||||
|
||||
|
||||
# 注入路径的槽号反查(命中槽派生名返回槽号,否则 0——激活号记账用,写盘判定以 _path 非空为准)
|
||||
func _slot_index_of(path: String) -> int:
|
||||
for i in range(1, _save.SLOT_COUNT + 1):
|
||||
if path == slot_path(i):
|
||||
return i
|
||||
return 0
|
||||
|
||||
|
||||
# 旧隐式单档一次性迁移(Q1 甲,sv12):src 字节级原样搬运到 dst(零解析零改造——损坏/高版本
|
||||
# 旧档字节原样落槽 1,读取时由正式进档路径的兜底/拒读链接管,不静默丢数据);
|
||||
# dst 已存在不重迁(幂等判据——判别锚④:去判据则重复迁移覆盖槽 1);迁移成功后 src 改名
|
||||
# bak 留痕(不删——处置字节留痕,防再次隐式读写)。任一步失败:源保留原地,下次启动重试。
|
||||
# tmp+rename 原子形态与 write_to_disk 同纪律(改名原语 _rename_overwrite 归 save IO 域单点)。生产调用=src/dst/bak 为 旧单档/槽 1/留痕名。
|
||||
func _migrate_legacy_save(src: String, dst: String, bak: String) -> void:
|
||||
if not FileAccess.file_exists(src):
|
||||
return # 执行前置:旧档不存在=无操作
|
||||
if FileAccess.file_exists(dst):
|
||||
return # 幂等判据:槽 1 已存在不重迁
|
||||
var tmp := dst + ".tmp"
|
||||
if not _copy_file(src, tmp):
|
||||
push_warning("[存档序列化] 旧单档迁移失败(源保留,下次启动重试):%s → %s" % [src, tmp])
|
||||
return
|
||||
if not _save._rename_overwrite(tmp, dst):
|
||||
push_warning("[存档序列化] 旧单档迁移改名失败(tmp 保留待查,源保留重试):%s → %s" % [tmp, dst])
|
||||
return
|
||||
_save._rename_overwrite(src, bak)
|
||||
push_warning("[存档序列化] 旧单档已迁入 %s,原件留痕于 %s" % [dst, bak])
|
||||
|
||||
|
||||
# 字节级文件拷贝(迁移专用——buffer 直拷,零解析零改造)
|
||||
func _copy_file(from: String, to: String) -> bool:
|
||||
var src := FileAccess.open(from, FileAccess.READ)
|
||||
if src == null:
|
||||
return false
|
||||
var dst := FileAccess.open(to, FileAccess.WRITE)
|
||||
if dst == null:
|
||||
src.close()
|
||||
return false
|
||||
while not src.eof_reached():
|
||||
var chunk := src.get_buffer(65536)
|
||||
if chunk.size() > 0:
|
||||
dst.store_buffer(chunk)
|
||||
src.close()
|
||||
dst.close()
|
||||
return true
|
||||
|
||||
|
||||
# 文件删除(删档原语专用——呈现层禁 DirAccess 直删,承载面单点)
|
||||
func _remove_file(p: String) -> bool:
|
||||
var dir := DirAccess.open(p.get_base_dir())
|
||||
if dir == null:
|
||||
return false
|
||||
return dir.remove(p.get_file()) == OK
|
||||
@@ -260,16 +260,16 @@ static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaP
|
||||
for i in db.level_count():
|
||||
var li: int = i + 1
|
||||
clears.append({"display_name": String(db.level_row(li).get("display_name", "")),
|
||||
"count": save_manager.clear_count(li)})
|
||||
if save_manager.boss_first_kill_marked(li):
|
||||
"count": save_manager.meta_state.career_state().clear_counts.get(str(li), 0)})
|
||||
if save_manager.meta_state.career_state().boss_first_kill.has(str(li)):
|
||||
boss_first += 1
|
||||
return {
|
||||
"total": save_manager.adventure_total(),
|
||||
"cleared": save_manager.adventure_cleared(),
|
||||
"total": save_manager.meta_state.career_state().adventure_total,
|
||||
"cleared": save_manager.meta_state.career_state().adventure_cleared,
|
||||
"clears": clears,
|
||||
"boss_first_kills": boss_first,
|
||||
"boss_first_total": db.level_count(),
|
||||
"boss_kills": save_manager.boss_kill_count(),
|
||||
"boss_kills": save_manager.meta_state.career_state().boss_kill_count,
|
||||
"keystone": meta.keystone_balance(),
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaP
|
||||
static func save_select_data(save_manager: SaveManagerCore) -> Dictionary:
|
||||
var slots: Array[Dictionary] = []
|
||||
for i in range(1, save_manager.SLOT_COUNT + 1):
|
||||
var p := save_manager.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒)
|
||||
var p := save_manager.slots.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒)
|
||||
slots.append({"slot": i,
|
||||
"occupied": bool(p.get("has_file", false)) and bool(p.get("readable", false)),
|
||||
"keystone": int(p.get("keystone", 0)),
|
||||
|
||||
@@ -312,7 +312,7 @@ func _on_confirm_accepted() -> void:
|
||||
_close_modal(confirm_dialog) # 模态栈单口
|
||||
match _confirm_consumer:
|
||||
"delete_slot":
|
||||
save_manager.delete_slot(_confirm_consumer_slot)
|
||||
save_manager.slots.delete_slot(_confirm_consumer_slot)
|
||||
save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
|
||||
_confirm_consumer = ""
|
||||
|
||||
@@ -368,7 +368,7 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref:
|
||||
"hero_override": meta_ref.hero_override() if meta_ref != null else {},
|
||||
"attack_type": meta_ref.hero_attack_type() if meta_ref != null else "",
|
||||
"armor_type": meta_ref.hero_armor_type() if meta_ref != null else "",
|
||||
"attunement": save_manager_ref.attunement_equipped() if save_manager_ref != null else [],
|
||||
"attunement": save_manager_ref.meta_state.attune_state().equipped if save_manager_ref != null else [],
|
||||
"attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0,
|
||||
"tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [],
|
||||
}
|
||||
@@ -906,6 +906,6 @@ func _seed_value() -> int:
|
||||
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费)
|
||||
if _seed_edit.text.is_valid_int():
|
||||
return int(_seed_edit.text)
|
||||
if save_manager.is_first_adventure_done():
|
||||
if save_manager.meta_state.career_state().first_adventure_done:
|
||||
return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免)
|
||||
return 42
|
||||
@@ -68,14 +68,14 @@ static func _on_save_select_closed(root: Variant) -> void:
|
||||
# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘,
|
||||
# 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。
|
||||
static func _on_save_select_new(root: Variant, slot: int) -> void:
|
||||
root.save_manager.create_slot(slot)
|
||||
root.save_manager.slots.create_slot(slot)
|
||||
root._camp_pres.open_camp()
|
||||
|
||||
|
||||
|
||||
# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。
|
||||
static func _on_save_select_load(root: Variant, slot: int) -> void:
|
||||
root.save_manager.activate_slot(slot)
|
||||
root.save_manager.slots.activate_slot(slot)
|
||||
root._camp_pres.open_camp()
|
||||
|
||||
|
||||
|
||||
@@ -104,13 +104,13 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
var iso = SaveManagerScript.new(save_path)
|
||||
iso.load_from_disk()
|
||||
if mark_first_done:
|
||||
iso.mark_first_adventure_done()
|
||||
iso.meta_state.mark_first_adventure_done()
|
||||
# --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
if swap_save:
|
||||
sm.attach_path(save_path)
|
||||
sm.slots.attach_path(save_path)
|
||||
if mark_first_done:
|
||||
sm.mark_first_adventure_done()
|
||||
sm.meta_state.mark_first_adventure_done()
|
||||
iso = sm
|
||||
else:
|
||||
main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动
|
||||
@@ -123,10 +123,10 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
# 驱动后写盘目标=隔离槽文件(持久化断言读 user://<tag>_slot_1.json)。返回 false=新档钮缺失(驱动中断)。
|
||||
static func open_camp_via_savesel(main, tag: String) -> bool:
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
sm.attach_slot_template("user://%s_slot_%%d.json" % tag)
|
||||
sm.slots.attach_slot_template("user://%s_slot_%%d.json" % tag)
|
||||
for i in range(1, 4): # 夹具清场——空槽起(陈旧槽文件会翻有档态、新档钮缺席)
|
||||
for suf in ["", ".tmp", ".corrupt.bak"]:
|
||||
var sp: String = sm.slot_path(i) + suf
|
||||
var sp: String = sm.slots.slot_path(i) + suf
|
||||
if FileAccess.file_exists(sp):
|
||||
DirAccess.remove_absolute(sp)
|
||||
main._open_save_select()
|
||||
|
||||
@@ -132,7 +132,7 @@ func _step_camp_back_to_menu() -> void:
|
||||
# 返回钮真实信号路径关闭→ESC 语境不响应(卡⑤ 契约 2 范围外——零实现即断言)。----
|
||||
func _step_save_select_open_back() -> void:
|
||||
ProbeUiUtil.isolate_save(main, "smoke_screen_flow_savesel", false, false, true)
|
||||
main.save_manager.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json")
|
||||
main.save_manager.slots.attach_slot_template("user://smoke_screen_flow_savesel_slot_%d.json")
|
||||
var esb: Button = ProbeUiUtil.find_button(main._start_overlay, "进入存档", "prefix")
|
||||
if esb == null:
|
||||
_problems.append("[savesel] mn3 进入存档钮缺失(卡② 交付物)")
|
||||
|
||||
@@ -71,7 +71,7 @@ func _ready() -> void:
|
||||
_problems.append("树淬炼柜台购买钮缺失(点击路径——四柜台重排后树=主界面)")
|
||||
else:
|
||||
# 树钮句柄用后即取(refresh_camp 随刷重建树行板——旧钮 queue_free,跨刷新句柄必失效)
|
||||
iso_c.add_keystone(9999)
|
||||
iso_c.meta_state._add_keystone(9999)
|
||||
main.camp_overlay.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var buy_btn: Button = null
|
||||
@@ -81,15 +81,15 @@ func _ready() -> void:
|
||||
if buy_btn == null:
|
||||
_problems.append("脊柱生命行缺失(点击路径)")
|
||||
else:
|
||||
var bal_before: int = iso_c.keystone_balance()
|
||||
var bal_before: int = iso_c.meta_state.keystone_balance()
|
||||
var price: int = int(main.db.tree_node("sp_hp").get("price", 0))
|
||||
buy_btn.pressed.emit() # 合成购买点击 → _on_camp_tree_buy → meta.tree_buy(写调用唯一归组合根)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.keystone_balance()) != bal_before - price:
|
||||
_problems.append("营地树购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.keystone_balance(), price])
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal_before - price:
|
||||
_problems.append("营地树购买未出账(%d→%d 应 −%d)" % [bal_before, iso_c.meta_state.keystone_balance(), price])
|
||||
var iso_chk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_chk.load_from_disk()
|
||||
if int(iso_chk.tree_level("sp_hp")) != 1:
|
||||
if int(iso_chk.meta_state.tree_state().nodes.get("sp_hp", 0)) != 1:
|
||||
_problems.append("营地树购买未持久(reload 后级 != 1)")
|
||||
# Fork 双态切换点击路径(V20——购=解锁默认 A,钮切 B;前置=同调位 1,先点亮)
|
||||
var socket_btn: Button = null
|
||||
@@ -123,7 +123,7 @@ func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
var iso_fk = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_fk.load_from_disk()
|
||||
if String(iso_fk.fork_side("fg_posture")) != "b":
|
||||
if String(iso_fk.meta_state.tree_state().fork_sides.get("fg_posture", "")) != "b":
|
||||
_problems.append("Fork 切态未持久(reload 后激活态 != b)")
|
||||
# ---- 营地六处灰字取色断言(迭代36 wc4——真实入口开营地后按文本锚取 label,
|
||||
# font_color 逐处 == STONE_DEEP;六源位 :167 图鉴锁文/:417+:476 行状态字/:509 同调形态/:513 档位标签/:525 刷新行)----
|
||||
@@ -189,7 +189,7 @@ func _ready() -> void:
|
||||
if FileAccess.file_exists(cp):
|
||||
DirAccess.remove_absolute(cp)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_savesel", true, false, true)
|
||||
main.save_manager.attach_slot_template(csel_tpl)
|
||||
main.save_manager.slots.attach_slot_template(csel_tpl)
|
||||
var f_c2 := FileAccess.open(csel_tpl % 2, FileAccess.WRITE)
|
||||
f_c2.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 100,
|
||||
"adventure_total": 3, "adventure_cleared": 1, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
|
||||
@@ -215,8 +215,8 @@ func _ready() -> void:
|
||||
_problems.append("[savesel] sl12 动线序违约:选档隐藏帧营地层未可见(先关后开=回闪形态)")
|
||||
elif samples[0][2]:
|
||||
_problems.append("[savesel] sl12 动线序违约:开始层在落点帧恢复(回闪一帧)")
|
||||
if main.save_manager.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1):
|
||||
_problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.active_slot())
|
||||
if main.save_manager.slots.active_slot() != 1 or not FileAccess.file_exists(csel_tpl % 1):
|
||||
_problems.append("[savesel] sl5 新档链失守(active=%d 应=1 且槽1 白板应落盘)" % main.save_manager.slots.active_slot())
|
||||
var occluded: Array = main._modals._occluded.get(main.camp_overlay, [])
|
||||
if not occluded.has(main.save_select_overlay):
|
||||
_problems.append("[savesel] sl12 选档层被关闭而非栈记账挂起(先关后开形态——遮挡挂起中间态钉死)")
|
||||
@@ -233,8 +233,8 @@ func _ready() -> void:
|
||||
else:
|
||||
load_btn.pressed.emit()
|
||||
await get_tree().process_frame
|
||||
if main.save_manager.active_slot() != 2:
|
||||
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.active_slot())
|
||||
if main.save_manager.slots.active_slot() != 2:
|
||||
_problems.append("[savesel] sl6 进档链失守(active=%d 应=2——激活该槽)" % main.save_manager.slots.active_slot())
|
||||
if not main.camp_overlay.visible or main.save_select_overlay.visible:
|
||||
_problems.append("[savesel] sl6 落点未开营地层或选档未隐藏")
|
||||
main._on_camp_closed()
|
||||
|
||||
@@ -475,7 +475,7 @@ func _ready() -> void:
|
||||
# attune_param_sum 只读出口→set_fog_glow_bonus〕→视野 +4 生效与卸装复原。
|
||||
# settlement.attunement 手动注入=模拟 begin_battle enc 注入后的运行态〔注入面本体归
|
||||
# 冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用)
|
||||
main.save_manager.set_attunement_equipped(["wide_scout_band"])
|
||||
main.save_manager.meta_state.set_attunement_equipped(["wide_scout_band"])
|
||||
var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta)
|
||||
if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"):
|
||||
_problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", [])))
|
||||
@@ -491,7 +491,7 @@ func _ready() -> void:
|
||||
_problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)")
|
||||
if int(bv.cells_probe()[ring_cell].fog_state) == 0:
|
||||
_problems.append("case⑮ 卸装后 dist==R+1 格未复原遮蔽")
|
||||
main.save_manager.set_attunement_equipped([])
|
||||
main.save_manager.meta_state.set_attunement_equipped([])
|
||||
# ---- case⑯ 状态面板输入锁语义(R3——开层锁棋盘、关层恢复无残留;真实 push_input
|
||||
# 路径:入口角标本体 press+release 真点击 → 状态面板开;锁期挖掘点击零穿透;「继续」关层后
|
||||
# 同格挖掘重新到达——锁语义与既有模态同款,无状态残留。置于全 case 之尾:取格若需前沿移位
|
||||
|
||||
@@ -81,7 +81,7 @@ func _ready() -> void:
|
||||
if FileAccess.file_exists(spp):
|
||||
DirAccess.remove_absolute(spp)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_savesel", true, false, true)
|
||||
main.save_manager.attach_slot_template(ssel_tpl) # 注入口(激活态回零——sl9 重启链同构态)
|
||||
main.save_manager.slots.attach_slot_template(ssel_tpl) # 注入口(激活态回零——sl9 重启链同构态)
|
||||
var f_s1 := FileAccess.open(ssel_tpl % 1, FileAccess.WRITE)
|
||||
f_s1.store_string(JSON.stringify({"version": 7, "meta": {"keystone_balance": 555,
|
||||
"adventure_total": 7, "adventure_cleared": 2, "tree_nodes": {}, "fork_sides": {}, "tree_struct": 2}}))
|
||||
@@ -89,7 +89,7 @@ func _ready() -> void:
|
||||
var f_s3 := FileAccess.open(ssel_tpl % 3, FileAccess.WRITE)
|
||||
f_s3.store_string("broken json not a save{{{") # 槽3 坏档(窥视 readable=false→按空呈现)
|
||||
f_s3.close()
|
||||
if main.save_manager.active_slot() != 0:
|
||||
if main.save_manager.slots.active_slot() != 0:
|
||||
problems.append("[savesel] 开层前激活态非零(sl9 重启链——boot/注入面后应无激活)")
|
||||
main._open_save_select()
|
||||
_settle()
|
||||
|
||||
@@ -165,11 +165,11 @@ func _ready() -> void:
|
||||
# SaveManager 是 autoload 单例——换注走原地 attach(首局态挂载+恢复隔离档);
|
||||
# --script 模式无裸 autoload 标识符——经 main 节点路径解析
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
sm.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面)
|
||||
sm.slots.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面)
|
||||
main.debug_overlay.seed_edit.text = ""
|
||||
if main._seed_value() != 42:
|
||||
problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)")
|
||||
sm.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位)
|
||||
sm.slots.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位)
|
||||
var draws: Array[int] = []
|
||||
for i in range(4):
|
||||
draws.append(main._seed_value())
|
||||
|
||||
@@ -172,13 +172,13 @@ func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) ->
|
||||
row["keystone_income"] = int(settle.total)
|
||||
row["purchases"] = _spend(meta, String(opt.spend))
|
||||
row["keystone_after"] = meta.keystone_balance()
|
||||
row["tree"] = {"sp_hp": meta.save.tree_level("sp_hp"), "sp_hit": meta.save.tree_level("sp_hit")}
|
||||
row["tree"] = {"sp_hp": meta.save.meta_state.tree_state().nodes.get("sp_hp", 0), "sp_hit": meta.save.meta_state.tree_state().nodes.get("sp_hit", 0)}
|
||||
row["new_unlocks"] = settle.new_unlocks
|
||||
_emit(row)
|
||||
counts.campaign += 1
|
||||
if (r + 1) % 10 == 0:
|
||||
print("进度 campaign %s %d/%d(树 血%d/命中%d · 钥石 %d)" % [policy, r + 1, int(opt.k),
|
||||
meta.save.tree_level("sp_hp"), meta.save.tree_level("sp_hit"),
|
||||
meta.save.meta_state.tree_state().nodes.get("sp_hp", 0), meta.save.meta_state.tree_state().nodes.get("sp_hit", 0),
|
||||
meta.keystone_balance()])
|
||||
_remove_save(save_path) # 段尾清理(与 smoke 存档纪律同源——不遗留隔离档)
|
||||
|
||||
@@ -195,7 +195,7 @@ func _spend(meta, policy_name: String) -> Array:
|
||||
for nid in order:
|
||||
var price: int = int(meta.db.tree_node(nid).get("price", 0))
|
||||
if price > 0 and meta.keystone_balance() >= price and meta.tree_buy(nid):
|
||||
out.append({"node_id": nid, "level": meta.save.tree_level(nid), "price": price})
|
||||
out.append({"node_id": nid, "level": meta.save.meta_state.tree_state().nodes.get(nid, 0), "price": price})
|
||||
bought = true
|
||||
break
|
||||
if not bought:
|
||||
|
||||
@@ -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