## VERTICAL SLICE - NOT FOR PRODUCTION ## 元进度账本服务:钥石收束入账、营地消费定价与秘宝解锁判定的纯被动服务,零信号零外发(垂直切片验证:钥石跨局换成永久强化)。 ## 数值零字面量——价格/档位/阈值只读 db 访问器;存档只经 SaveManager 访问器禁读内部结构;行集禁自扫表。 ## hero_override() 现算不写表(全零=白板合法态)——生效注入只走冒险开始参数面显式传参。 class_name MetaProgressionCore extends Node var db: ConfigDb var save: SaveManagerCore # 迭代 27 T1 裁点①——MetaProgression 升格 autoload(RngService 先例:类名加 Core 后缀、 # autoload 占净名)。_init 参数可缺省兼容双形态:autoload 实例化走 .new() 零参 + _ready 从 # autoload 链取依赖(工程序 ConfigDB→RngService→SaveManager→MetaProgression,_ready 时序 # 已就绪);单测/工具直呼 .new(db, save) 形态不变(不入树零 _ready)。 func _init(db_ref: ConfigDb = null, save_ref: SaveManagerCore = null) -> void: db = db_ref save = save_ref func _ready() -> void: if db == null: db = get_node("/root/ConfigDB") if save == null: save = get_node("/root/SaveManager") func keystone_balance() -> int: return save.keystone_balance() # ---- 秘宝解锁服务(迭代 22 T2 换代——解锁混合制,契约 6;旧工坊渠道面随批删除)---- # 〔属性锻造柜台退役——迭代 21 T4/T5:buy_forge/forge_flat/forge_level/forge_price 与 meta_forge # 表族随攻防所有权切分删除。〕〔道具工坊解锁渠道面退役——迭代 22 T2:旧「总通关次数达阈值」 # 行族与 _workshop_progress/workshop_status/refresh_workshop_unlocks 删除,解锁条件单一事实源= # treasure_items.tres 行内 unlock_keystone_cost/unlock_requires 谓词(C1 逐件分配锁定表)。〕 # 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager) func is_item_unlocked(item_id: String) -> bool: return save.item_unlocked(item_id) # 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS; # 词表加键 = 此处增一 match 分支,阈值比较与解锁写档零改动) func _treasure_unlock_progress(key: String) -> int: match key: "adventure_cleared": return save.adventure_cleared() "boss_kill_count": return save.boss_kill_count() "vision_used": return save.vision_used() _: return 0 # 未知计数键恒 0 = 永不解锁(fail-safe 非致命——schema 词表校验兜底,不静默误解锁) # 钥石购买解锁(营地秘宝匣唯一购买写口):仅购买件(行带 unlock_keystone_cost)可购; # 已解锁/余额不足/条件件误购 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币)。 func buy_treasure_unlock(item_id: String) -> bool: var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id) 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): return false # 已解锁重复购买拒绝 if not save.spend_keystone(price): return false # 余额不足(零副作用) save.unlock_item(item_id) return save.write_to_disk() # 条件解锁刷新(达标未解锁 → unlock_item;返回新解锁 item_id 列——冒险收束后与营地打开时各调一次)。 # 全部谓词达标才解锁(多谓词=AND——天眼双条件);幂等:已解锁跳过,重复刷新零重复入账。 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): continue var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array if ureqs.is_empty(): continue # 购买件不入条件刷新面 var met := true for req_v in ureqs: var req: Dictionary[Variant, Variant] = req_v as Dictionary if _treasure_unlock_progress(String(req.get("key", ""))) < int(req.get("threshold", 999)): met = false break if met: save.unlock_item(iid) new_unlocks.append(iid) if not new_unlocks.is_empty(): save.write_to_disk() return new_unlocks # 秘宝匣柜台只读投影(呈现层数据源——12 件逐行解锁态/价格/条件进度): # {item_id, display_name, desc, tier, unlocked, mode(keystone|condition|owned), price, reqs:[{key,threshold,progress}]} 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 ureqs: Array[Variant] = row.get("unlock_requires", []) as Array var req_view: Array[Variant] = [] for req_v in ureqs: var req: Dictionary[Variant, Variant] = req_v as Dictionary req_view.append({ "key": String(req.get("key", "")), "threshold": int(req.get("threshold", 0)), "progress": _treasure_unlock_progress(String(req.get("key", ""))), }) out.append({ "item_id": iid, "display_name": String(row.get("display_name", "")), "desc": String(row.get("desc", "")), "tier": String(row.get("tier", "")), "unlocked": unlocked, "mode": "owned" if unlocked else ("condition" if ureqs.size() > 0 else "keystone"), "price": int(row.get("unlock_keystone_cost", 0)), "reqs": req_view, }) return out # ---- 锻造工坊写口族(迭代 23 T3——契约 9:扣币/升级/解锁/选中/写盘只经本组;UI 只发意图信号)---- # 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:钥石单币(契约 3——GP 局内一次性货币 # 永不互兑)、并联双通道解锁(4b-1——练满前一档 OR 一次性解锁价,互不影响不设串联)、 # 一次冒险一武器一甲(契约 8——营地免费切换、冒险开始锁死由参数面快照保证)。 # 等级语义(Q1):未解锁/未锻造=0 加值(裸体态);锻造第一级=「获得装备」=首个 +1;逐级递进。 # 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处) func forge_route_unlocked(route_id: String) -> bool: var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty(): return false if bool(row.get("initial_owned", false)): return true if save.forge_unlocked_purchases().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)): return true return false # 路线购买(几何价=基础价×150%^已购级数——锚表 20/30/45/67/101;等级+1 即时写盘)。 # 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币不写盘)。 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) if lvl >= int(row.get("cap", 0)): return false var price := ForgeTable.level_price(row, lvl) if not save.spend_keystone(price): return false save.set_forge_route_level(route_id, lvl + 1) return save.write_to_disk() # 一次性解锁价直购(并联双通道②)。已解锁(含练满前一档派生)= 拒绝零收费——两通道互不影响: # 未练满也可直购;直购后练满不重复收(练满通道本就是派生、零收费)。初始持有行无解锁面。 func forge_buy_unlock(route_id: String) -> bool: var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty() or bool(row.get("initial_owned", false)): return false 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): return false save.add_forge_unlock(route_id) return save.write_to_disk() # 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照, # 契约 8;冒险中无切换入口=UI 面无路径可达本口) 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) return save.write_to_disk() 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) return save.write_to_disk() # 当前选中武器攻击加值 / 当前甲 AC 加值(路线等级×每级增量——0=裸体态/未锻造合法态)。 # 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。 func forge_weapon_atk_bonus() -> int: var rid := save.forge_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)) func forge_armor_ac_bonus() -> int: var rid := save.forge_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)) # 两根类型轴当前生效值(选中路线类型轴;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。 # 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。 func hero_attack_type() -> String: var rid := save.forge_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() return String(row.get("type_axis", "")) func hero_armor_type() -> String: var rid := save.forge_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() return String(row.get("type_axis", "")) # ---- 锻造工坊柜台只读投影(迭代 23 T6——呈现层数据源:两区六路线行板;余额联动禁用归视图)---- # 每行 {route_id, display_name, kind, type_axis, level, cap, unlocked, initial_owned, selected, # next_price(未满级已解锁=下级几何价/否则 0), unlock_price, prereq_route, prereq_name, # prereq_level, prereq_cap, prereq_maxed}——纯结构面,禁嵌余额判断(视图按 balance 禁用,同秘宝匣先例)。 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 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 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) out.append({ "route_id": rid, "display_name": String(row.get("display_name", "")), "kind": String(row.get("kind", "")), "type_axis": String(row.get("type_axis", "")), "level": lvl, "cap": cap_v, "unlocked": unlocked, "initial_owned": bool(row.get("initial_owned", false)), "selected": selected, "next_price": ForgeTable.level_price(row, lvl) if (unlocked and lvl < cap_v) else 0, "unlock_price": int(row.get("unlock_price", 0)), "prereq_route": prereq, "prereq_name": String(db.forge_route(prereq).get("display_name", "")) if prereq != "" else "", "prereq_level": plvl, "prereq_cap": pcap, "prereq_maxed": prereq != "" and plvl >= pcap, }) return out # ---- Boss 两档与收束入账(结算屏文案数据源)---- # 明细 {big_reward, boss_drops, total, new_unlocks};入账与标记经 save.apply_adventure_summary # (keystone_drops 副本随摘要入 recent_summaries)。 # 迭代 10 奖励重构批:钥石改 Boss 逐杀掉落——掉落金额已在冒险内逐杀入账(summary.keystone_total # 含全部掉落;首杀/复杀裁决在 AdventureRunner._boss_keystone_drop,首杀标记仍在下方 # apply_adventure_summary 逐通关关次置位)。本函数不再派生掉落金额,只做标记+入账+归档。 func settle_adventure(summary: Dictionary) -> Dictionary: # 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束) save.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))) # 迭代 22 T3——真视使用计数收束聚合入档(信标网络行为条件) var new_unlocks := refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役) save.write_to_disk() var total := int(summary.get("keystone_total", 0)) return { "big_reward": total, "boss_drops": drops, "total": total, "new_unlocks": new_unlocks, } # ---- 跨局注入(冒险开始参数面)---- # 迭代 23 T4 注入公式换代(锻造工坊实装批——契约 1 攻防所有权切分落地):基础值 + 树生命/命中淬炼 # flat(命中线在账未注入——契约 6,消费面归数值批)+ **锻造路线加值**(当前选中武器攻击加值 + 当前甲 # AC 加值——读存档 forge_routes 域+路线表)+ 局内加算区(bonus 区域不经本面)。入战 AC 三层可加: # 甲型基础 AC(forge.tres 行 base_ac)+ 甲路线等级加值(本面 armor_class 分量)+ 局内加算区。 # 旧属性柜台 forge_flat 项退役移除(V7/V12);攻/AC 树来源随所有权切分砍除(契约 9——树零攻防来源, # 现表零 atk/armor_class 行,tree_stat_flat 二轴恒 0 保留通式)。原 FORGE_ROUTE_BONUS 预留常量随本批 # 退役(公式不再出现——V7/V12)。 # 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。 func hero_override() -> Dictionary: return { "atk": forge_weapon_atk_bonus() + tree_stat_flat("atk"), "max_hp": tree_stat_flat("max_hp"), "armor_class": forge_armor_ac_bonus() + tree_stat_flat("armor_class"), } # ---- 天赋树段(迭代 12 T8——R1 甲融合案;设计权威 整合定稿 §1.4 一树双币三脉一脊)---- # 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」) func tree_spent_total() -> int: var total := 0 for lvl in (save.tree_nodes() as Dictionary).values(): total += int(lvl) return total # 节点可购校验(行门槛 + 显式前置 + 币 + cap;free 节点=钥匙不经购买,彩蛋=觉醒不经购买) func tree_can_buy(node_id: String) -> bool: var row: Dictionary[Variant, Variant] = db.tree_node(node_id) if row.is_empty(): return false # 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红) if String(row.get("kind", "")) == "easter": return false var lvl := save.tree_level(node_id) 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(): return false # 行门槛(一线一节点:脊柱节点第 N 级需全树总投入 ≥4×(N−1)——迭代 21 主语平移,row 字段退役) 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: return false # 等级阈值前置(R1 机制位——requires_level {node_id: n}:前置节点等级 ≥ n 才可购; # 2026-10-13 用户推翻裁撤案随批重立,现役表零配置=空转零行为影响) 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]): return false return true # 节点购买(营地树 UI 唯一写口):树全钥石单币(2026-10-07 用户裁甲)。free=工作间钥匙零币点亮 # (price 0,零额消费无副作用);彩蛋不经购买(can_buy 已拒——觉醒唯一入口=_check_easter_egg)。 # 刷新升级节点走同账路由(Q1 裁乙——树上+秘宝匣装配段双入口写同一 attune_refresh_level 账本, # 价格/上限单一事实源=Y 行表值 keystone_cost;V15)。 func tree_buy(node_id: String) -> bool: if node_id == "ks_refresh": return buy_attune_refresh() var row: Dictionary[Variant, Variant] = db.tree_node(node_id) 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): return false save.set_tree_level(node_id, save.tree_level(node_id) + 1) _check_easter_egg() return save.write_to_disk() # Fork 切换(营地树 UI——一节点双态:购买解锁组默认 A 态,换态走本口零币;同态重复=零操作; # 未解锁组切换=拒绝——解锁唯一入口=购买面 tree_buy_fork) 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) if cur == "" or cur == side: return false # 未解锁走购买面;同态零操作 save.set_fork_side(group, side) return save.write_to_disk() # Fork 节点购买入口(一节点双态——购=解锁开关并置默认 A 态;重复购买由 cap 经 can_buy 拦截) func tree_buy_fork(node_id: String) -> bool: var row: Dictionary[Variant, Variant] = db.tree_node(node_id) if row.is_empty() or String(row.get("kind", "")) != "fork": return tree_buy(node_id) if not tree_can_buy(node_id): return false var price := int(row.get("price", 0)) if not save.spend_keystone(price): return false save.set_tree_level(node_id, 1) save.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态 return save.write_to_disk() # 树效果投影:四线 flat 汇总 + rule 键集(激活 Fork 侧 effects 并入) 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", ""))) if lvl <= 0: continue total += int((row.get("effects", {}) as Dictionary).get(key, 0)) * lvl return total func tree_rule_keys() -> Array: var out: Array[Variant] = [] var sides: Dictionary[Variant, Variant] = save.fork_sides() for row in db.tree_rows(): if String(row.get("kind", "")) != "fork": continue var active := String(sides.get(String(row.get("fork_group", "")), "")) if active == "": continue # 未解锁组零规则输出 var fx: Dictionary[Variant, Variant] = row.get("effects_" + active, {}) as Dictionary for k in fx.keys(): if not out.has(String(k)): out.append(String(k)) return out # 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工) func tree_camp_rows() -> Array: var out: Array[Variant] = [] for row in db.tree_rows(): if String(row.get("kind", "")) == "fork": continue # Fork 行由 fork_groups 段承载 out.append({ "node_id": String(row.get("node_id", "")), "display_name": String(row.get("display_name", "")), "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) "price": int(row.get("price", 0)), "currency": String(row.get("currency", "keystone")), "can_buy": tree_can_buy(String(row.get("node_id", ""))), "gate_text": tree_gate_text(String(row.get("node_id", ""))), }) return out # 行板门槛文案(R1 机制位呈现——requires_level 未达标项拼「需 {前置名} Lv{n}」;达标/未配置=空串。 # 只呈现未达标项:达标后锁标消失,不做全量前置罗列) func tree_gate_text(node_id: String) -> String: var row: Dictionary[Variant, Variant] = db.tree_node(node_id) 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]): 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) func tree_fork_groups() -> Array: var out: Array[Variant] = [] var sides: Dictionary[Variant, Variant] = save.fork_sides() for row in db.tree_rows(): if String(row.get("kind", "")) != "fork": continue var fg := String(row.get("fork_group", "")) out.append({ "group": fg, "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, "can_buy": tree_can_buy(String(row.get("node_id", ""))), "price": int(row.get("price", 0)), "sides": [ {"side": "a", "display_name": String(row.get("name_a", "")), "effects": row.get("effects_a", {}) as Dictionary}, {"side": "b", "display_name": String(row.get("name_b", "")), "effects": row.get("effects_b", {}) as Dictionary}, ], }) return out # 彩蛋觉醒检查(特化判定——生命/命中两线拉满 且 同调位 1/2/3 全解锁 → easter_ley 免费点亮; # 迭代 21 T2 换代:不依赖 requires/requires_level 前置形态,发现率监控呈报面沿袭) func _check_easter_egg() -> void: if save.tree_level("easter_ley") > 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)): return for i in range(1, 4): if save.tree_level("att_socket%d" % i) < 1: return save.set_tree_level("easter_ley", 1) # 三体系迁移函数已删除(迭代 21 T5——旧档破坏性废弃〔Q3 用户裁:开发阶段无迁移〕;换代前结构 # 由 SaveManager._normalize 的 tree_struct 标记检测废弃重置,禁迁移禁折算)。 # ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 需求卡片 P1 甲)---- # 装配切换(营地装配覆盖层唯一写口——冒险开始前配置;冒险中零写口=装配不可变契约 1,本口只服务营地): # 已装 → 卸下恒可(卸下后其余件前移位序——效果面只读装配集不受格位影响);未装 → 非同调行拒绝 + # 满位禁选 + 档位格校验(迭代 22 T2 契约 2:装入件 append 至末位 k=已装数+1,件档 rank ≤ k—— # 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。 # 装配关系写存档持久。 func toggle_attunement(item_id: String) -> bool: var equipped: Array[Variant] = save.attunement_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): return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置) if equipped.size() >= Attunement.slots(db, save): return false # 满位禁选 var rank: int = int(db.treasure_items.TIER_RANK.get(String(arow.get("tier", "")), 99)) if rank > equipped.size() + 1: return false # 件档高于将占格档(第 k 格档=k——普通/优秀/稀有递增) equipped.append(item_id) else: equipped.erase(item_id) save.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 items: Array[Dictionary] = [] for row in db.treasure_attunement_rows(): var iid := String(row.get("item_id", "")) items.append({ "item_id": iid, "display_name": String(row.get("display_name", "")), "desc": String(row.get("desc", "")), "tier": String(row.get("tier", "")), "form": String(row.get("form", "")), "equipped": equipped.has(iid), "unlocked": save.item_unlocked(iid), }) var lvl := save.attune_refresh_level() var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(lvl + 1) return { "slots": Attunement.slots(db, save), "equipped": equipped, "items": items, "refresh": { "level": lvl, "charges": attune_refresh_charges(), "price": int(next_row.get("keystone_cost", 0)), "maxed": next_row.is_empty(), }, } # 每冒险开局刷新次数 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)) # 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) if next_row.is_empty(): return false if not save.spend_keystone(int(next_row.get("keystone_cost", 0))): return false save.set_attune_refresh_level(save.attune_refresh_level() + 1) return save.write_to_disk()