553 lines
26 KiB
GDScript
553 lines
26 KiB
GDScript
## 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.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 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.treasure_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.treasure_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.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.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.forge_route(route_id)
|
||
if row.is_empty() or not _forge_route_unlocked(route_id):
|
||
return false
|
||
var lvl := 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.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.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.forge_route(String(fs["weapon"]))
|
||
var attack := db.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.forge_route(String(fs["armor"]))
|
||
var armor := db.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.forge_routes():
|
||
var rid := String(row.get("route_id", ""))
|
||
var lvl := 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 := 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 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.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.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.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.tree_node(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.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.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.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 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.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"):
|
||
return false
|
||
var cur := 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.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 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_rule_keys() -> Array:
|
||
var out: Array[Variant] = []
|
||
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().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
|
||
|
||
|
||
# 营地树面投影(并组入口——{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.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.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 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)
|
||
|
||
|
||
## 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.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.tree_node(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.treasure_item_row(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.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.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": meta_state.item_unlocked(iid),
|
||
})
|
||
var lvl := 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),
|
||
"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(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(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()
|