Files
gplan-2/game/core/meta_progression.gd

554 lines
27 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 元进度账本服务:钥石收束入账、营地消费定价与秘宝解锁判定的纯被动服务,零信号零外发(垂直切片验证:钥石跨局换成永久强化)。
## 数值零字面量——价格/档位/阈值只读 db 访问器;存档只经 SaveManager 访问器禁读内部结构;行集禁自扫表。
## hero_override() 现算不写表(全零=白板合法态)——生效注入只走冒险开始参数面显式传参。
class_name MetaProgressionCore
extends Node
var db: ConfigDb
var save: SaveManagerCore
var meta_state: MetaStateStore # 存档内元进度数据读写唯一层(K3′ 装配成员——架构 :90 直连)
# 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
if save != null:
meta_state = save.meta_state
func _ready() -> void:
if db == null:
db = get_node("/root/ConfigDB")
if save == null:
save = get_node("/root/SaveManager")
meta_state = save.meta_state
# ---- 秘宝解锁服务(解锁混合制,契约 6)----
# 解锁条件单一事实源=treasure_items.tres 行内 unlock_keystone_cost/unlock_requires 谓词
# (C1 逐件分配锁定表)。钥石余额/道具解锁读数消费方直连 meta.meta_state(纯转发读已随件退役)。
# 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS;
# 词表加键 = 此处增一 match 分支,阈值比较与解锁写档零改动)
func _treasure_unlock_progress(key: String) -> int:
match key:
"adventure_cleared":
return meta_state.career_state().adventure_cleared
"boss_kill_count":
return meta_state.career_state().boss_kill_count
"vision_used":
return meta_state.career_state().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.crossrun.treasure_items.row_by_id(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 meta_state.item_unlocked(item_id):
return false # 已解锁重复购买拒绝
if not meta_state.spend_keystone(price):
return false # 余额不足(零副作用)
meta_state.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.crossrun.treasure_items.attunement_rows():
var iid := String(row.get("item_id", ""))
if meta_state.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:
meta_state.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.crossrun.treasure_items.attunement_rows():
var iid := String(row.get("item_id", ""))
var unlocked := 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:
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
# ---- 锻造工坊写口族(契约 9:扣币/升级/解锁/选中/写盘只经本组;UI 只发意图信号)----
# 设计权威 design/gdd/systems/跨局循环/锻造工坊.md v0.6:钥石单币(契约 3——GP 局内一次性货币
# 永不互兑)、并联双通道解锁(4b-1——练满前一档 OR 一次性解锁价,互不影响不设串联)、
# 一次冒险一武器一甲(契约 8——营地免费切换、冒险开始锁死由参数面快照保证)。
# 等级语义:未解锁/未锻造=0 加值(裸体态);锻造第一级=「获得装备」=首个 +1;逐级递进。
# 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处)
func _forge_route_unlocked(route_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
if row.is_empty():
return false
if bool(row.get("initial_owned", false)):
return true
if 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.crossrun.forge.route(prereq)
if not prow.is_empty() and meta_state.forge_state().levels.get(prereq, 0) >= 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.crossrun.forge.route(route_id)
if row.is_empty() or not _forge_route_unlocked(route_id):
return false
var lvl: int = 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 meta_state.spend_keystone(price):
return false
meta_state.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.crossrun.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 meta_state.spend_keystone(price):
return false
meta_state.add_forge_unlock(route_id)
return save.write_to_disk()
# 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照,
# 契约 8;冒险中无切换入口=UI 面无路径可达本口)。并组入口:kind ∈ {weapon, armor}。
func forge_select(kind: String, route_id: String) -> bool:
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(route_id)
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(route_id):
return false
if kind == "weapon":
meta_state.set_forge_weapon(route_id)
else:
meta_state.set_forge_armor(route_id)
return save.write_to_disk()
# 两根类型轴当前生效值({attack, armor} 并组承载;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。
func hero_type_axes() -> Dictionary:
var fs := meta_state.forge_state()
var wrow: Dictionary[Variant, Variant] = db.crossrun.forge.route(String(fs["weapon"]))
var attack := db.crossrun.forge.default_attack_type
if not wrow.is_empty() and String(wrow.get("kind", "")) == "weapon" and _forge_route_unlocked(String(fs["weapon"])):
attack = String(wrow.get("type_axis", ""))
var arow: Dictionary[Variant, Variant] = db.crossrun.forge.route(String(fs["armor"]))
var armor := db.crossrun.forge.default_armor_type
if not arow.is_empty() and String(arow.get("kind", "")) == "armor" and _forge_route_unlocked(String(fs["armor"])):
armor = String(arow.get("type_axis", ""))
return {"attack": attack, "armor": armor}
# ---- 锻造工坊柜台只读投影(呈现层数据源:两区六路线行板;余额联动禁用归视图)----
# 每行 {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.crossrun.forge.route_rows():
var rid := String(row.get("route_id", ""))
var lvl: int = 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: int = meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0
var pcap := int(db.crossrun.forge.route(prereq).get("cap", 0)) if prereq != "" else 0
var selected: bool = (String(row.get("kind", "")) == "weapon" and meta_state.forge_state().weapon == rid) \
or (String(row.get("kind", "")) == "armor" and meta_state.forge_state().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.crossrun.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)。
# 钥石为 Boss 逐杀掉落——掉落金额已在冒险内逐杀入账(summary.keystone_total
# 含全部掉落;首杀/复杀判定在 AdventureRunner._boss_keystone_drop,首杀标记在下方
# apply_adventure_summary 逐关卡完成关次置位)。本函数只做标记+入账+归档。
func settle_adventure(summary: Dictionary) -> Dictionary:
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
meta_state.mark_first_adventure_done()
var drops: Array[Variant] = (summary.get("keystone_drops", []) as Array).duplicate(true)
meta_state.apply_adventure_summary(summary, drops)
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))
return {
"big_reward": total,
"boss_drops": drops,
"total": total,
"new_unlocks": new_unlocks,
}
# ---- 跨局注入(冒险开始参数面)----
# 注入公式(契约 1 攻防所有权切分):基础值 + 树生命/命中淬炼
# flat(命中线在账未注入——契约 6)+ **锻造路线加值**(当前选中武器攻击加值 + 当前甲
# AC 加值——读存档 forge_routes 域+路线表)+ 局内加算区(bonus 区域不经本面)。入战 AC 三层可加:
# 甲型基础 AC(forge.tres 行 base_ac)+ 甲路线等级加值(本面 armor_class 分量)+ 局内加算区。
# 攻/AC 树来源按所有权切分砍除(契约 9——树零攻防来源,
# 现表零 atk/armor_class 行,tree_stat_flat 二轴恒 0 保留通式)。
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
func hero_override() -> Dictionary:
# 锻造路线加值内联(原 forge_weapon_atk_bonus/forge_armor_ac_bonus 单调用点收编——输出逐字:
# 路线等级×每级增量,选中态异常/未解锁防御回退 0)
var fs := meta_state.forge_state()
var atk_bonus := _forge_axis_bonus(fs, "weapon", "weapon")
var ac_bonus := _forge_axis_bonus(fs, "armor", "armor")
# 树淬炼 flat 内联(原 tree_stat_flat 单调用点收编——四线 flat 汇总同轮扫描,三轴输出逐字等价)
var tree_atk := 0
var tree_hp := 0
var tree_ac := 0
var nodes: Dictionary[Variant, Variant] = meta_state.tree_state()["nodes"]
for row in db.crossrun.meta_tree.rows:
var lvl := int(nodes.get(String(row.get("node_id", "")), 0))
if lvl <= 0:
continue
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
tree_atk += int(fx.get("atk", 0)) * lvl
tree_hp += int(fx.get("max_hp", 0)) * lvl
tree_ac += int(fx.get("armor_class", 0)) * lvl
return {
"atk": atk_bonus + tree_atk,
"max_hp": tree_hp,
"armor_class": ac_bonus + tree_ac,
}
# 锻造轴加值原语(选中路线等级×每级增量;选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态)
func _forge_axis_bonus(fs: Dictionary, sel_key: String, kind: String) -> int:
var rid := String(fs[sel_key])
var row: Dictionary[Variant, Variant] = db.crossrun.forge.route(rid)
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(rid):
return 0
return int(fs["levels"].get(rid, 0)) * int(row.get("per_level_gain", 0))
# ---- 天赋树段(设计权威 整合定稿 §1.4 一树双币三脉一脊)----
# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」)
func _tree_spent_total() -> int:
var total := 0
for lvl in (meta_state.tree_state().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.crossrun.meta_tree.row_by_id(node_id)
# 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红)
if row.is_empty() or String(row.get("kind", "")) == "easter":
return false
var nodes: Dictionary[Variant, Variant] = meta_state.tree_state()["nodes"]
var lvl := int(nodes.get(node_id, 0))
var blocked := lvl >= int(row.get("cap", 1))
# 刷新升级节点:cap 主语=Y 账本(单一事实源——树上入口与装配段同 cap 同价,防双账漂移)
if node_id == "ks_refresh" and db.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level + 1).is_empty():
blocked = true
# 行门槛(一线一节点:脊柱节点第 N 级需全树总投入 ≥4×(N−1))
if String(row.get("vein", "")) == "spine" and String(row.get("kind", "")) == "stat" and _tree_spent_total() < db.crossrun.meta_tree.level_gate(lvl + 1):
blocked = true
for req in (row.get("requires", []) as Array):
if int(nodes.get(String(req), 0)) < 1:
blocked = true
# 等级阈值前置(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 int(nodes.get(String(req_id), 0)) < int(req_lv[req_id]):
blocked = true
return not blocked
# 节点购买(营地树 UI 唯一写口):树全钥石单币。free=工作间钥匙零币点亮
# (price 0,零额消费无副作用);彩蛋不经购买(can_buy 已拒——觉醒唯一入口=_check_easter_egg)。
# 刷新升级节点走同账路由(树上+秘宝匣装配段双入口写同一 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.crossrun.meta_tree.row_by_id(node_id)
if row.is_empty() or not tree_can_buy(node_id):
return false
var price := int(row.get("price", 0))
if not meta_state.spend_keystone(price):
return false
meta_state.set_tree_level(node_id, meta_state.tree_state().nodes.get(node_id, 0) + 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.crossrun.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
return false
var cur: String = meta_state.tree_state().fork_sides.get(group, "")
if cur == "" or cur == side:
return false # 未解锁走购买面;同态零操作
meta_state.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.crossrun.meta_tree.row_by_id(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 meta_state.spend_keystone(price):
return false
meta_state.set_tree_level(node_id, 1)
meta_state.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
return save.write_to_disk()
# 树规则参数字典投影(激活 Fork 侧 effects_* 参数字典并入——键=规则键,值=该键参数字典原样透传;
# 首遇优先去重语义保持)
func tree_rules() -> Dictionary[Variant, Variant]:
var out: Dictionary[Variant, Variant] = {}
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
for row in db.crossrun.meta_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[String(k)] = fx[k]
return out
# 营地树面投影(并组入口——{rows, fork_groups};CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工)
func tree_camp_status() -> Dictionary:
return {"rows": _tree_camp_rows(), "fork_groups": _tree_fork_groups()}
## 树行板投影段(原 tree_camp_rows)
func _tree_camp_rows() -> Array:
var out: Array[Variant] = []
for row in db.crossrun.meta_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": meta_state.attune_state().refresh_level if String(row.get("node_id", "")) == "ks_refresh" else 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", ""))),
"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.crossrun.meta_tree.row_by_id(node_id)
var rl: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary
var parts: Array[String] = []
for req_id in rl.keys():
if meta_state.tree_state().nodes.get(String(req_id), 0) < int(rl[req_id]):
var req_row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(String(req_id))
parts.append("需 %s Lv%d" % [String(req_row.get("display_name", req_id)), int(rl[req_id])])
return "、".join(parts)
## Fork 行板投影段(原 tree_fork_groups)
func _tree_fork_groups() -> Array:
var out: Array[Variant] = []
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
for row in db.crossrun.meta_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": 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": [
{"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 免费点亮;
# 不依赖 requires/requires_level 前置形态)
func _check_easter_egg() -> void:
if meta_state.tree_state().nodes.get("easter_ley", 0) > 0:
return
for nid in ["sp_hp", "sp_hit"]:
var row: Dictionary[Variant, Variant] = db.crossrun.meta_tree.row_by_id(nid)
if row.is_empty() or meta_state.tree_state().nodes.get(nid, 0) < int(row.get("cap", 1)):
return
for i in range(1, 4):
if meta_state.tree_state().nodes.get("att_socket%d" % i, 0) < 1:
return
meta_state.set_tree_level("easter_ley", 1)
# ---- 同调位账本(装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2)----
# 装配切换(营地装配覆盖层唯一写口——冒险开始前配置;冒险中零写口=装配不可变契约 1,本口只服务营地):
# 已装 → 卸下恒可(卸下后其余件前移位序——效果面只读装配集不受格位影响);未装 → 非同调行拒绝 +
# 满位禁选 + 档位格校验(契约 2:装入件 append 至末位 k=已装数+1,件档 rank ≤ k——
# 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。
# 装配关系写存档持久。
func toggle_attunement(item_id: String) -> bool:
var equipped: Array[Variant] = meta_state.attune_state().equipped
if not equipped.has(item_id):
var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(item_id)
if arow.is_empty() or String(arow.get("form", "")) == "":
return false # 非同调行(过渡/退役行)禁装配
if not meta_state.item_unlocked(item_id):
return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置)
if equipped.size() >= Attunement.slots(db, save):
return false # 满位禁选
var rank: int = int(db.crossrun.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)
meta_state.set_attunement_equipped(equipped)
return save.write_to_disk()
# 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工)
func attune_camp_status() -> Dictionary:
var equipped: Array[Variant] = meta_state.attune_state().equipped
var items: Array[Dictionary] = []
for row in db.crossrun.treasure_items.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": meta_state.item_unlocked(iid),
})
var lvl: int = meta_state.attune_state().refresh_level
var next_row: Dictionary[Variant, Variant] = db.crossrun.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.crossrun.meta.attune_refresh_row(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.crossrun.meta.attune_refresh_row(meta_state.attune_state().refresh_level + 1)
if next_row.is_empty():
return false
if not meta_state.spend_keystone(int(next_row.get("keystone_cost", 0))):
return false
meta_state.set_attune_refresh_level(meta_state.attune_state().refresh_level + 1)
return save.write_to_disk()