卡①T6(核心数据面结构收敛批): meta_progression瘦身(31→20公开=K3′封闭面定稿)+status_container/treasure_items拆分——纯转发退役2(keystone_balance/is_item_unlocked消费方直连meta.meta_state)/内联3(hero_override收编锻造加值与树淬炼flat,输出逐字)/私3(_forge_route_unlocked/_tree_spent_total/_tree_gate_text)/并组3组(forge_select(kind,rid)/hero_type_axes{attack,armor}/tree_camp_status{rows,fork_groups})/补meta_state装配成员+本类读写全改经meta_state;status_container apply_effect三臂域拆(_grant_snapshot/_apply_refresh/_apply_stack公共尾部单点+_new_instance 7→5参snap束)/treasure_items validate_use三域(_spatial_reject_reason/_window_cells/_preview_window_dead);消费方9文件改址(camp/hud/main/verifier/treasure_interact/forge/tree_requires/save_slot_isolation/theme_projection);[测试动作移交用户:套件停跑]
This commit is contained in:
1 parent
b182073941
commit
52661761c3
12 files changed
+238
-218
No files matched your search
+123
-134
@@ -7,6 +7,7 @@ extends Node
|
||||
|
||||
var db: ConfigDb
|
||||
var save: SaveManagerCore
|
||||
var meta_state: MetaStateStore # 存档内元进度数据读写唯一层(K3′ 装配成员——架构 :90 直连)
|
||||
|
||||
|
||||
# MetaProgression 是 autoload(仿 RngService 先例:类名加 Core 后缀、
|
||||
@@ -16,6 +17,8 @@ var save: SaveManagerCore
|
||||
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:
|
||||
@@ -23,31 +26,23 @@ func _ready() -> void:
|
||||
db = get_node("/root/ConfigDB")
|
||||
if save == null:
|
||||
save = get_node("/root/SaveManager")
|
||||
|
||||
|
||||
func keystone_balance() -> int:
|
||||
return save.meta_state.keystone_balance()
|
||||
meta_state = save.meta_state
|
||||
|
||||
|
||||
# ---- 秘宝解锁服务(解锁混合制,契约 6)----
|
||||
# 解锁条件单一事实源=treasure_items.tres 行内 unlock_keystone_cost/unlock_requires 谓词
|
||||
# (C1 逐件分配锁定表)。
|
||||
|
||||
# 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager)
|
||||
func is_item_unlocked(item_id: String) -> bool:
|
||||
return save.meta_state.item_unlocked(item_id)
|
||||
|
||||
# (C1 逐件分配锁定表)。钥石余额/道具解锁读数消费方直连 meta.meta_state(纯转发读已随件退役)。
|
||||
|
||||
# 解锁条件计数读数单点(谓词 key → 生涯计数——封闭枚举=TreasureItemTable.UNLOCK_COUNTER_KEYS;
|
||||
# 词表加键 = 此处增一 match 分支,阈值比较与解锁写档零改动)
|
||||
func _treasure_unlock_progress(key: String) -> int:
|
||||
match key:
|
||||
"adventure_cleared":
|
||||
return save.meta_state.career_state().adventure_cleared
|
||||
return meta_state.career_state().adventure_cleared
|
||||
"boss_kill_count":
|
||||
return save.meta_state.career_state().boss_kill_count
|
||||
return meta_state.career_state().boss_kill_count
|
||||
"vision_used":
|
||||
return save.meta_state.career_state().vision_used
|
||||
return meta_state.career_state().vision_used
|
||||
_:
|
||||
return 0 # 未知计数键恒 0 = 永不解锁(fail-safe 非致命——schema 词表校验兜底,不静默误解锁)
|
||||
|
||||
@@ -59,11 +54,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.meta_state.item_unlocked(item_id):
|
||||
if meta_state.item_unlocked(item_id):
|
||||
return false # 已解锁重复购买拒绝
|
||||
if not save.meta_state.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false # 余额不足(零副作用)
|
||||
save.meta_state.unlock_item(item_id)
|
||||
meta_state.unlock_item(item_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -73,7 +68,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.meta_state.item_unlocked(iid):
|
||||
if meta_state.item_unlocked(iid):
|
||||
continue
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array
|
||||
if ureqs.is_empty():
|
||||
@@ -85,7 +80,7 @@ func refresh_treasure_unlocks() -> Array:
|
||||
met = false
|
||||
break
|
||||
if met:
|
||||
save.meta_state.unlock_item(iid)
|
||||
meta_state.unlock_item(iid)
|
||||
new_unlocks.append(iid)
|
||||
if not new_unlocks.is_empty():
|
||||
save.write_to_disk()
|
||||
@@ -98,7 +93,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.meta_state.item_unlocked(iid)
|
||||
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:
|
||||
@@ -128,18 +123,18 @@ func treasure_shop_status() -> Array:
|
||||
# 等级语义:未解锁/未锻造=0 加值(裸体态);锻造第一级=「获得装备」=首个 +1;逐级递进。
|
||||
|
||||
# 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处)
|
||||
func forge_route_unlocked(route_id: String) -> bool:
|
||||
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.meta_state.forge_state().unlocked.has(route_id):
|
||||
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 save.meta_state.forge_state().levels.get(prereq, 0) >= int(prow.get("cap", 0)):
|
||||
if not prow.is_empty() and meta_state.forge_state().levels.get(prereq, 0) >= int(prow.get("cap", 0)):
|
||||
return true
|
||||
return false
|
||||
|
||||
@@ -148,15 +143,15 @@ func forge_route_unlocked(route_id: String) -> bool:
|
||||
# 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(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):
|
||||
if row.is_empty() or not _forge_route_unlocked(route_id):
|
||||
return false
|
||||
var lvl := save.meta_state.forge_state().levels.get(route_id, 0)
|
||||
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 save.meta_state.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.meta_state.set_forge_route_level(route_id, lvl + 1)
|
||||
meta_state.set_forge_route_level(route_id, lvl + 1)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -166,67 +161,41 @@ 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):
|
||||
if _forge_route_unlocked(route_id):
|
||||
return false
|
||||
var price := int(row.get("unlock_price", 0))
|
||||
if price < 1 or not save.meta_state.spend_keystone(price):
|
||||
if price < 1 or not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.meta_state.add_forge_unlock(route_id)
|
||||
meta_state.add_forge_unlock(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照,
|
||||
# 契约 8;冒险中无切换入口=UI 面无路径可达本口)
|
||||
func forge_select_weapon(route_id: String) -> bool:
|
||||
# 契约 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", "")) != "weapon" or not forge_route_unlocked(route_id):
|
||||
if row.is_empty() or String(row.get("kind", "")) != kind or not _forge_route_unlocked(route_id):
|
||||
return false
|
||||
save.meta_state.set_forge_weapon(route_id)
|
||||
if kind == "weapon":
|
||||
meta_state.set_forge_weapon(route_id)
|
||||
else:
|
||||
meta_state.set_forge_armor(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.meta_state.set_forge_armor(route_id)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 当前选中武器攻击加值 / 当前甲 AC 加值(路线等级×每级增量——0=裸体态/未锻造合法态)。
|
||||
# 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。
|
||||
func forge_weapon_atk_bonus() -> int:
|
||||
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.meta_state.forge_state().levels.get(rid, 0) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
func forge_armor_ac_bonus() -> int:
|
||||
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.meta_state.forge_state().levels.get(rid, 0) * int(row.get("per_level_gain", 0))
|
||||
|
||||
|
||||
# 两根类型轴当前生效值(选中路线类型轴;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
|
||||
# 两根类型轴当前生效值({attack, armor} 并组承载;选中态异常/未解锁=锻造配置默认值——契约 2 回退面)。
|
||||
# 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。
|
||||
func hero_attack_type() -> String:
|
||||
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()
|
||||
return String(row.get("type_axis", ""))
|
||||
|
||||
|
||||
func hero_armor_type() -> String:
|
||||
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()
|
||||
return String(row.get("type_axis", ""))
|
||||
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}
|
||||
|
||||
|
||||
# ---- 锻造工坊柜台只读投影(呈现层数据源:两区六路线行板;余额联动禁用归视图)----
|
||||
@@ -237,14 +206,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.meta_state.forge_state().levels.get(rid, 0)
|
||||
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 unlocked := _forge_route_unlocked(rid)
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
var plvl := save.meta_state.forge_state().levels.get(prereq, 0) if prereq != "" else 0
|
||||
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 save.meta_state.forge_state().weapon == rid) \
|
||||
or (String(row.get("kind", "")) == "armor" and save.meta_state.forge_state().armor == rid)
|
||||
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", "")),
|
||||
@@ -274,10 +243,10 @@ func forge_shop_status() -> Array:
|
||||
# apply_adventure_summary 逐关卡完成关次置位)。本函数只做标记+入账+归档。
|
||||
func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
|
||||
save.meta_state.mark_first_adventure_done()
|
||||
meta_state.mark_first_adventure_done()
|
||||
var drops: Array[Variant] = (summary.get("keystone_drops", []) as Array).duplicate(true)
|
||||
save.meta_state.apply_adventure_summary(summary, drops)
|
||||
save.meta_state.add_vision_used(int(summary.get("vision_used_delta", 0))) # 真视使用计数收束聚合入档(信标网络行为条件)
|
||||
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))
|
||||
@@ -299,19 +268,43 @@ func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。
|
||||
|
||||
func hero_override() -> Dictionary:
|
||||
# 锻造路线加值内联(原 forge_weapon_atk_bonus/forge_armor_ac_bonus 单调用点收编——输出逐字:
|
||||
# 路线等级×每级增量,选中态异常/未解锁防御回退 0)
|
||||
var fs := meta_state.forge_state()
|
||||
var atk_bonus := 0
|
||||
var wrow: Dictionary[Variant, Variant] = db.forge_route(String(fs["weapon"]))
|
||||
if not wrow.is_empty() and String(wrow.get("kind", "")) == "weapon" and _forge_route_unlocked(String(fs["weapon"])):
|
||||
atk_bonus = int(fs["levels"].get(String(fs["weapon"]), 0)) * int(wrow.get("per_level_gain", 0))
|
||||
var ac_bonus := 0
|
||||
var arow: Dictionary[Variant, Variant] = db.forge_route(String(fs["armor"]))
|
||||
if not arow.is_empty() and String(arow.get("kind", "")) == "armor" and _forge_route_unlocked(String(fs["armor"])):
|
||||
ac_bonus = int(fs["levels"].get(String(fs["armor"]), 0)) * int(arow.get("per_level_gain", 0))
|
||||
# 树淬炼 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": 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"),
|
||||
"atk": atk_bonus + tree_atk,
|
||||
"max_hp": tree_hp,
|
||||
"armor_class": ac_bonus + tree_ac,
|
||||
}
|
||||
|
||||
|
||||
# ---- 天赋树段(设计权威 整合定稿 §1.4 一树双币三脉一脊)----
|
||||
|
||||
# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」)
|
||||
func tree_spent_total() -> int:
|
||||
func _tree_spent_total() -> int:
|
||||
var total := 0
|
||||
for lvl in (save.meta_state.tree_state().nodes as Dictionary).values():
|
||||
for lvl in (meta_state.tree_state().nodes as Dictionary).values():
|
||||
total += int(lvl)
|
||||
return total
|
||||
|
||||
@@ -324,24 +317,24 @@ func tree_can_buy(node_id: String) -> bool:
|
||||
# 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红)
|
||||
if String(row.get("kind", "")) == "easter":
|
||||
return false
|
||||
var lvl := save.meta_state.tree_state().nodes.get(node_id, 0)
|
||||
var lvl := 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.meta_state.attune_state().refresh_level + 1).is_empty():
|
||||
if node_id == "ks_refresh" and db.meta_attune_refresh_row(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):
|
||||
if _tree_spent_total() < db.meta_tree.level_gate(lvl + 1):
|
||||
return false
|
||||
for req in (row.get("requires", []) as Array):
|
||||
if save.meta_state.tree_state().nodes.get(String(req), 0) < 1:
|
||||
if 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.meta_state.tree_state().nodes.get(String(req_id), 0) < int(req_lv[req_id]):
|
||||
if meta_state.tree_state().nodes.get(String(req_id), 0) < int(req_lv[req_id]):
|
||||
return false
|
||||
return true
|
||||
|
||||
@@ -357,9 +350,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.meta_state.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.meta_state.set_tree_level(node_id, save.meta_state.tree_state().nodes.get(node_id, 0) + 1)
|
||||
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()
|
||||
|
||||
@@ -369,10 +362,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.meta_state.tree_state().fork_sides.get(group, "")
|
||||
var cur := meta_state.tree_state().fork_sides.get(group, "")
|
||||
if cur == "" or cur == side:
|
||||
return false # 未解锁走购买面;同态零操作
|
||||
save.meta_state.set_fork_side(group, side)
|
||||
meta_state.set_fork_side(group, side)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
@@ -384,27 +377,17 @@ 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.meta_state.spend_keystone(price):
|
||||
if not meta_state.spend_keystone(price):
|
||||
return false
|
||||
save.meta_state.set_tree_level(node_id, 1)
|
||||
save.meta_state.set_fork_side(String(row.get("fork_group", "")), "a") # 解锁默认 A 态
|
||||
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()
|
||||
|
||||
|
||||
# 树效果投影:四线 flat 汇总 + rule 键集(激活 Fork 侧 effects 并入)
|
||||
func tree_stat_flat(key: String) -> int:
|
||||
var total := 0
|
||||
for row in db.tree_rows():
|
||||
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
|
||||
return total
|
||||
|
||||
|
||||
# 树规则键投影(激活 Fork 侧 effects 并入)
|
||||
func tree_rule_keys() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var sides: Dictionary[Variant, Variant] = save.meta_state.tree_state().fork_sides
|
||||
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
|
||||
for row in db.tree_rows():
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
@@ -418,8 +401,13 @@ func tree_rule_keys() -> Array:
|
||||
return out
|
||||
|
||||
|
||||
# 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工)
|
||||
func tree_camp_rows() -> Array:
|
||||
# 营地树面投影(并组入口——{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":
|
||||
@@ -430,31 +418,32 @@ func tree_camp_rows() -> Array:
|
||||
"vein": String(row.get("vein", "")),
|
||||
"kind": String(row.get("kind", "")),
|
||||
"cap": int(row.get("cap", 1)),
|
||||
"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)
|
||||
"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", ""))),
|
||||
"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:
|
||||
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.meta_state.tree_state().nodes.get(String(req_id), 0) < int(rl[req_id]):
|
||||
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)
|
||||
|
||||
|
||||
func tree_fork_groups() -> Array:
|
||||
## Fork 行板投影段(原 tree_fork_groups)
|
||||
func _tree_fork_groups() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
var sides: Dictionary[Variant, Variant] = save.meta_state.tree_state().fork_sides
|
||||
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
|
||||
for row in db.tree_rows():
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
@@ -464,7 +453,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.meta_state.tree_state().nodes.get(String(row.get("node_id", "")), 0) > 0,
|
||||
"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": [
|
||||
@@ -478,16 +467,16 @@ func tree_fork_groups() -> Array:
|
||||
# 彩蛋觉醒检查(特化判定——生命/命中两线拉满 且 同调位 1/2/3 全解锁 → easter_ley 免费点亮;
|
||||
# 不依赖 requires/requires_level 前置形态)
|
||||
func _check_easter_egg() -> void:
|
||||
if save.meta_state.tree_state().nodes.get("easter_ley", 0) > 0:
|
||||
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 save.meta_state.tree_state().nodes.get(nid, 0) < int(row.get("cap", 1)):
|
||||
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 save.meta_state.tree_state().nodes.get("att_socket%d" % i, 0) < 1:
|
||||
if meta_state.tree_state().nodes.get("att_socket%d" % i, 0) < 1:
|
||||
return
|
||||
save.meta_state.set_tree_level("easter_ley", 1)
|
||||
meta_state.set_tree_level("easter_ley", 1)
|
||||
|
||||
|
||||
# ---- 同调位账本(装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2)----
|
||||
@@ -498,12 +487,12 @@ func _check_easter_egg() -> void:
|
||||
# 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。
|
||||
# 装配关系写存档持久。
|
||||
func toggle_attunement(item_id: String) -> bool:
|
||||
var equipped: Array[Variant] = save.meta_state.attune_state().equipped
|
||||
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 save.meta_state.item_unlocked(item_id):
|
||||
if not meta_state.item_unlocked(item_id):
|
||||
return false # 未解锁禁装配(解锁混合制契约 6——装配以解锁为前置)
|
||||
if equipped.size() >= Attunement.slots(db, save):
|
||||
return false # 满位禁选
|
||||
@@ -513,13 +502,13 @@ func toggle_attunement(item_id: String) -> bool:
|
||||
equipped.append(item_id)
|
||||
else:
|
||||
equipped.erase(item_id)
|
||||
save.meta_state.set_attunement_equipped(equipped)
|
||||
meta_state.set_attunement_equipped(equipped)
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工)
|
||||
func attune_camp_status() -> Dictionary:
|
||||
var equipped: Array[Variant] = save.meta_state.attune_state().equipped
|
||||
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", ""))
|
||||
@@ -530,9 +519,9 @@ func attune_camp_status() -> Dictionary:
|
||||
"tier": String(row.get("tier", "")),
|
||||
"form": String(row.get("form", "")),
|
||||
"equipped": equipped.has(iid),
|
||||
"unlocked": save.meta_state.item_unlocked(iid),
|
||||
"unlocked": meta_state.item_unlocked(iid),
|
||||
})
|
||||
var lvl := save.meta_state.attune_state().refresh_level
|
||||
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),
|
||||
@@ -549,15 +538,15 @@ func attune_camp_status() -> Dictionary:
|
||||
|
||||
# 每冒险开局刷新次数 Y(当前等级行表值;0 级 = 0 次即无刷新能力——P1 甲初始 0 同构)
|
||||
func attune_refresh_charges() -> int:
|
||||
return int(db.meta_attune_refresh_row(save.meta_state.attune_state().refresh_level).get("refresh_charges", 0))
|
||||
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(save.meta_state.attune_state().refresh_level + 1)
|
||||
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 save.meta_state.spend_keystone(int(next_row.get("keystone_cost", 0))):
|
||||
if not meta_state.spend_keystone(int(next_row.get("keystone_cost", 0))):
|
||||
return false
|
||||
save.meta_state.set_attune_refresh_level(save.meta_state.attune_state().refresh_level + 1)
|
||||
meta_state.set_attune_refresh_level(meta_state.attune_state().refresh_level + 1)
|
||||
return save.write_to_disk()
|
||||
@@ -36,37 +36,51 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int
|
||||
var policy: String = row.stack_policy
|
||||
var dur: int = int(row.duration_settlements) if duration_override < 1 else duration_override
|
||||
var max_stack: int = int(row.max_stack)
|
||||
# 授予时快照(抗表重载——EC9)
|
||||
var snap_m: int = int(row.magnitude)
|
||||
if source.has("magnitude_override"):
|
||||
snap_m = int(source["magnitude_override"]) # 封印溢价贯穿(迭代14② T4——封印命中 percent 形态同乘快照;缺省路径零扰动)
|
||||
var snap_unit: String = String(row.unit)
|
||||
var snap_display: String = String(row.display_name)
|
||||
var snap := _grant_snapshot(row, source)
|
||||
match policy:
|
||||
"independent":
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
"refresh":
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
inst.remaining = dur # R ← D(取实例快照 D——同表值);L 不变;空刷新恒 true
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
return _apply_refresh(effect_id, dur, snap, source)
|
||||
"stack":
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if int(inst.layers) >= max_stack:
|
||||
return false # 满层拒绝原子——容器零写入(拒收由战斗结算记"奖励未生效"行)
|
||||
inst.layers = int(inst.layers) + 1 # stack 授予不触碰 R(spec 决议)
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display))
|
||||
return true
|
||||
return _apply_stack(effect_id, dur, snap, source, max_stack)
|
||||
push_warning("[增益状态] 未知 stack_policy")
|
||||
return false
|
||||
|
||||
|
||||
func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, m: int, unit: String, display: String) -> Dictionary:
|
||||
## 授予时快照域(抗表重载——EC9;封印溢价贯穿——迭代14② T4 percent 形态同乘快照、缺省路径零扰动)
|
||||
func _grant_snapshot(row: Dictionary, source: Dictionary) -> Dictionary:
|
||||
var snap_m: int = int(row.magnitude)
|
||||
if source.has("magnitude_override"):
|
||||
snap_m = int(source["magnitude_override"])
|
||||
return {"m": snap_m, "unit": String(row.unit), "display": String(row.display_name)}
|
||||
|
||||
|
||||
## refresh 臂:命中改 R 未命中追加(R ← D 取实例快照 D——同表值;L 不变;空刷新恒 true)
|
||||
func _apply_refresh(effect_id: String, dur: int, snap: Dictionary, source: Dictionary) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
inst.remaining = dur
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
|
||||
|
||||
## stack 臂:满层拒零写入·未满加层·未命中追加(stack 授予不触碰 R——spec 决议;
|
||||
## 满层拒绝原子——容器零写入,拒收由战斗结算记"奖励未生效"行)
|
||||
func _apply_stack(effect_id: String, dur: int, snap: Dictionary, source: Dictionary, max_stack: int) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if int(inst.layers) >= max_stack:
|
||||
return false
|
||||
inst.layers = int(inst.layers) + 1
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
|
||||
|
||||
func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, snap: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"effect_id": effect_id,
|
||||
"layers": layers,
|
||||
@@ -74,9 +88,9 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary,
|
||||
"source_class": String(source.get("source_class", "")),
|
||||
"origin_cell": source.get("origin_cell", Vector2i(-1, -1)),
|
||||
"round_index": int(source.get("round_index", 0)),
|
||||
"snap_m": m,
|
||||
"snap_unit": unit,
|
||||
"snap_display": display,
|
||||
"snap_m": snap["m"],
|
||||
"snap_unit": snap["unit"],
|
||||
"snap_display": snap["display"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
+40
-23
@@ -97,31 +97,16 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre
|
||||
var kinds: Array[Variant] = row.get("effect_kinds", [])
|
||||
var resolved: Array[Variant] = []
|
||||
if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"):
|
||||
# 空间效果(真视 reveal_tiles/天眼复合 safe_dig 入空间解析;
|
||||
# 真视窄域护栏 REVEAL_KINDS 同守):
|
||||
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
|
||||
return _reject("目标越板")
|
||||
if String(row.get("target", "")) == "cell":
|
||||
# 单格档(线香真视单格;≤3×3 窄域契约内的点目标)
|
||||
resolved.append(target)
|
||||
else:
|
||||
# 3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
var c := target + Vector2i(dx, dy)
|
||||
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
|
||||
resolved.append(c)
|
||||
# 空间效果(真视 reveal_tiles/天眼复合 safe_dig 入空间解析;真视窄域护栏 REVEAL_KINDS 同守)
|
||||
var spatial := _spatial_reject_reason(row, target, board_size)
|
||||
if spatial != "":
|
||||
return _reject(spatial)
|
||||
resolved = _window_cells(row, target, board_size)
|
||||
# 点击级零信息:纯预读行窗口全已开 = 没东西可看(拒绝零消耗);
|
||||
# 真视恒可用(零雷窗口=「这 9 格没雷」信息非空——GDD 裁定)
|
||||
if kinds.has("reward_preview") and not kinds.has("mine_reveal"):
|
||||
var any_unrevealed := false
|
||||
if cell_unrevealed_cb.is_valid():
|
||||
for c in resolved:
|
||||
if bool(cell_unrevealed_cb.call(c)):
|
||||
any_unrevealed = true
|
||||
break
|
||||
if not any_unrevealed:
|
||||
return _reject("窗口内无可预读的未开格")
|
||||
if kinds.has("reward_preview") and not kinds.has("mine_reveal") \
|
||||
and _preview_window_dead(resolved, cell_unrevealed_cb):
|
||||
return _reject("窗口内无可预读的未开格")
|
||||
return {
|
||||
"ok": true,
|
||||
"reason": "",
|
||||
@@ -132,6 +117,38 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre
|
||||
}
|
||||
|
||||
|
||||
## 空间解析域:目标越板拒(cell/3×3 两档共用板界)——返回拒绝原因,空串=通过
|
||||
func _spatial_reject_reason(row: Dictionary, target: Vector2i, board_size: Vector2i) -> String:
|
||||
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
|
||||
return "目标越板"
|
||||
return ""
|
||||
|
||||
|
||||
## 预读窗口域:单格档=点目标(线香真视单格;≤3×3 窄域契约内);其余=3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
|
||||
func _window_cells(row: Dictionary, target: Vector2i, board_size: Vector2i) -> Array:
|
||||
var resolved: Array[Variant] = []
|
||||
if String(row.get("target", "")) == "cell":
|
||||
resolved.append(target)
|
||||
else:
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
var c := target + Vector2i(dx, dy)
|
||||
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
|
||||
resolved.append(c)
|
||||
return resolved
|
||||
|
||||
|
||||
## 预读死窗判定:纯预读行窗口全已开 = 没东西可看(cell_unrevealed_cb = 排雷玩法 只读面)
|
||||
func _preview_window_dead(resolved: Array, cell_unrevealed_cb: Callable) -> bool:
|
||||
var any_unrevealed := false
|
||||
if cell_unrevealed_cb.is_valid():
|
||||
for c in resolved:
|
||||
if bool(cell_unrevealed_cb.call(c)):
|
||||
any_unrevealed = true
|
||||
break
|
||||
return not any_unrevealed
|
||||
|
||||
|
||||
# ---- 叠加层(战斗结算逐 kind 应用——空间先、ap 后的序在编排侧钉死)----
|
||||
|
||||
func apply_overlay(kind: String, cells: Array) -> void:
|
||||
|
||||
@@ -59,12 +59,12 @@ func on_camp_closed() -> void:
|
||||
func camp_data() -> Dictionary:
|
||||
var meta = _meta_get.call()
|
||||
return {
|
||||
"balance": meta.keystone_balance() if meta != null else 0,
|
||||
"balance": meta.meta_state.keystone_balance() if meta != null else 0,
|
||||
"workshop": meta.treasure_shop_status() if meta != null else [], # 键名沿 workshop(视图消费键不变——行内容=秘宝匣 12 件解锁投影)
|
||||
"forge": meta.forge_shop_status() if meta != null else [], # 锻造工坊两区六路线投影
|
||||
"attune": meta.attune_camp_status() if meta != null else {}, # 同调装配面投影
|
||||
"tree": meta.tree_camp_rows() if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行)
|
||||
"fork_groups": meta.tree_fork_groups() if meta != null else [],
|
||||
"tree": meta.tree_camp_status()["rows"] if meta != null else [], # 树面投影(含 forge_workshop/ks_workshop 钥匙门判据行)
|
||||
"fork_groups": meta.tree_camp_status()["fork_groups"] if meta != null else [],
|
||||
"recent": _save_get.call().recent_summaries() if _save_get.call() != null else [],
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ func on_camp_forge_select(route_id: String) -> void:
|
||||
var meta = _meta_get.call()
|
||||
if meta == null:
|
||||
return
|
||||
var ok: bool = meta.forge_select_weapon(route_id) if String(db.forge_route(route_id).get("kind", "")) == "weapon" \
|
||||
else meta.forge_select_armor(route_id)
|
||||
var ok: bool = meta.forge_select("weapon", route_id) if String(db.forge_route(route_id).get("kind", "")) == "weapon" \
|
||||
else meta.forge_select("armor", route_id)
|
||||
if not ok:
|
||||
toast.show_toast("切换被拒——路线未解锁", "warn")
|
||||
return
|
||||
|
||||
@@ -229,7 +229,7 @@ static func _p90(a: Array) -> float:
|
||||
# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链)
|
||||
static func status_data(db: ConfigDb, meta: MetaProgressionCore, runner: Variant, treasure_ui: Variant) -> Dictionary:
|
||||
return {
|
||||
"balance": meta.keystone_balance(),
|
||||
"balance": meta.meta_state.keystone_balance(),
|
||||
"hero_hp": int(runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命)
|
||||
"max_hp": runner.effective_max_hp(),
|
||||
"atk": runner.effective_atk(),
|
||||
@@ -270,7 +270,7 @@ static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaP
|
||||
"boss_first_kills": boss_first,
|
||||
"boss_first_total": db.level_count(),
|
||||
"boss_kills": save_manager.meta_state.career_state().boss_kill_count,
|
||||
"keystone": meta.keystone_balance(),
|
||||
"keystone": meta.meta_state.keystone_balance(),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -366,8 +366,8 @@ func _wire_adventure() -> void:
|
||||
static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary:
|
||||
var params: Dictionary[Variant, Variant] = {
|
||||
"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 "",
|
||||
"attack_type": meta_ref.hero_type_axes()["attack"] if meta_ref != null else "",
|
||||
"armor_type": meta_ref.hero_type_axes()["armor"] if meta_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 [],
|
||||
|
||||
@@ -46,7 +46,7 @@ func unlocked_set() -> Dictionary:
|
||||
return out
|
||||
for row in db.treasure_transitional_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if meta.is_item_unlocked(iid):
|
||||
if meta.meta_state.item_unlocked(iid):
|
||||
out[iid] = true
|
||||
return out
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) ->
|
||||
for r in range(int(opt.k)):
|
||||
var seed_val := _next_seed(int(opt.seed0))
|
||||
var ov: Dictionary = meta.hero_override()
|
||||
var keystone_before: int = meta.keystone_balance()
|
||||
var keystone_before: int = meta.meta_state.keystone_balance()
|
||||
var st: Dictionary = autoplay.run_adventure(db, seed_val, 0, 0, false, logger, policy, "min", ov)
|
||||
var row := _adventure_row(st, policy, seed_val)
|
||||
row["seg"] = "campaign"
|
||||
@@ -171,7 +171,7 @@ func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) ->
|
||||
var settle: Dictionary = meta.settle_adventure(st.summary)
|
||||
row["keystone_income"] = int(settle.total)
|
||||
row["purchases"] = _spend(meta, String(opt.spend))
|
||||
row["keystone_after"] = meta.keystone_balance()
|
||||
row["keystone_after"] = meta.meta_state.keystone_balance()
|
||||
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)
|
||||
@@ -179,7 +179,7 @@ func _seg_campaign(db, autoplay, logger, opt: Dictionary, counts: Dictionary) ->
|
||||
if (r + 1) % 10 == 0:
|
||||
print("进度 campaign %s %d/%d(树 血%d/命中%d · 钥石 %d)" % [policy, r + 1, int(opt.k),
|
||||
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()])
|
||||
meta.meta_state.keystone_balance()])
|
||||
_remove_save(save_path) # 段尾清理(与 smoke 存档纪律同源——不遗留隔离档)
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ func _spend(meta, policy_name: String) -> Array:
|
||||
var bought := false
|
||||
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):
|
||||
if price > 0 and meta.meta_state.keystone_balance() >= price and meta.tree_buy(nid):
|
||||
out.append({"node_id": nid, "level": meta.save.meta_state.tree_state().nodes.get(nid, 0), "price": price})
|
||||
bought = true
|
||||
break
|
||||
|
||||
@@ -430,8 +430,8 @@ func _smoke_forge_axes() -> void:
|
||||
not mp2.forge_buy_level("sword") and not mp2d.forge_buy_level("spear"))
|
||||
# V3① 通道一:练满前一档(长剑 cap)→ 长枪/法杖派生解锁(零收费);轻甲未满→甲线仍锁
|
||||
Kit.check("通道一:长剑练满→长枪/法杖派生解锁;轻甲未练满→重甲/魔法护甲仍锁〔锻造工坊批 V3〕",
|
||||
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"))
|
||||
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.meta_state.forge_state().levels.get("spear", 0) == 1
|
||||
and sv2.meta_state.keystone_balance() == 30)
|
||||
@@ -440,18 +440,18 @@ func _smoke_forge_axes() -> void:
|
||||
var mp3 = load("res://core/meta_progression.gd").new(_db, sv3)
|
||||
sv3.meta_state._add_keystone(500)
|
||||
Kit.check("通道二:魔法护甲一次性解锁价 200 直购开(轻甲未练满也可买)〔锻造工坊批 V3〕",
|
||||
not mp3.forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
|
||||
and sv3.meta_state.keystone_balance() == 300 and mp3.forge_route_unlocked("ward"))
|
||||
not mp3._forge_route_unlocked("ward") and mp3.forge_buy_unlock("ward")
|
||||
and sv3.meta_state.keystone_balance() == 300 and mp3._forge_route_unlocked("ward"))
|
||||
# V3③ 直购后练满不重复收(练满通道=派生零收费;已解锁再直购=拒绝)
|
||||
sv3.meta_state.set_forge_route_level("light", 5)
|
||||
Kit.check("直购后练满不重复收(已解锁=拒绝零收费——两通道互不影响)〔锻造工坊批 V3〕",
|
||||
mp3.forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
|
||||
mp3._forge_route_unlocked("ward") and not mp3.forge_buy_unlock("ward")
|
||||
and sv3.meta_state.keystone_balance() == 300)
|
||||
# V3④ 不设串联门槛(法杖直购开——长枪零状态不阻;dry 档充值走直购)
|
||||
sv2d.meta_state._add_keystone(500)
|
||||
Kit.check("不设串联门槛(法杖直购开——长枪零状态不阻)〔锻造工坊批 V3〕",
|
||||
mp2d.forge_buy_unlock("staff") and mp2d.forge_route_unlocked("staff")
|
||||
and sv2d.meta_state.forge_state().levels.get("spear", 0) == 0 and not mp2d.forge_route_unlocked("spear"))
|
||||
mp2d.forge_buy_unlock("staff") and mp2d._forge_route_unlocked("staff")
|
||||
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.meta_state.keystone_balance()
|
||||
Kit.check("两币分工:购买只扣钥石(余额差=当级价 20——GP 零触碰、无材料计数)〔锻造工坊批 V4〕",
|
||||
@@ -460,17 +460,17 @@ func _smoke_forge_axes() -> void:
|
||||
# V6①:营地切换=写存档域+免费(余额零动);旧路线练度保留(只不生效)
|
||||
var ks6: int = sv3.meta_state.keystone_balance()
|
||||
Kit.check("选中切换免费(余额零动——写存档域即盘)〔锻造工坊批 V6〕",
|
||||
mp3.forge_select_armor("ward") and sv3.meta_state.forge_state().armor == "ward"
|
||||
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.meta_state.forge_state().levels.get("light", 0) == 5)
|
||||
# V6②:未解锁路线拒选零副作用(dry 档:长枪未解锁)
|
||||
Kit.check("未解锁路线拒选零副作用〔锻造工坊批 V6〕",
|
||||
not mp2d.forge_select_weapon("spear") and sv2d.meta_state.forge_state().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"
|
||||
and mp2d.hero_armor_type() == "light")
|
||||
mp3.hero_type_axes()["armor"] == "ward" and mp2d.hero_type_axes()["attack"] == "normal"
|
||||
and mp2d.hero_type_axes()["armor"] == "light")
|
||||
|
||||
# ---- T4 注入接线(V5 后半 · V6 锁死 · V7 公式对拍 · V10 树零攻防复核)----
|
||||
# V5:build_adventure_params 注入默认值(新档=树钥匙未点亮)+点亮后读存档选中覆盖
|
||||
@@ -482,7 +482,7 @@ func _smoke_forge_axes() -> void:
|
||||
and String(bp_params.get("armor_type", "")) == "light")
|
||||
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"))
|
||||
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)
|
||||
Kit.check("build_adventure_params 读存档选中覆盖(选中长枪→pierce)〔锻造工坊批 V5〕",
|
||||
String(bp_sel.get("attack_type", "")) == "pierce"
|
||||
@@ -497,13 +497,13 @@ func _smoke_forge_axes() -> void:
|
||||
Kit.check("换当前武器→攻击加值随切换(长剑 3 级待命不生效——只认选中)〔锻造工坊批 V7〕",
|
||||
int(mp4.hero_override().get("atk", -1)) == 1)
|
||||
Kit.check("切回长剑→加值=3 级量(旧路线练度保留且生效)〔锻造工坊批 V7〕",
|
||||
mp4.forge_select_weapon("sword") and int(mp4.hero_override().get("atk", -1)) == 3)
|
||||
mp4.forge_select("weapon", "sword") and int(mp4.hero_override().get("atk", -1)) == 3)
|
||||
# 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.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"))
|
||||
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))
|
||||
var p5: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv5, mp5)
|
||||
Kit.check("注入参数冒险启动(两轴+override 同面)", adr5.start_adventure(77, p5))
|
||||
@@ -514,7 +514,7 @@ func _smoke_forge_axes() -> void:
|
||||
adr5.stats().attack_type == "normal" and adr5.stats().armor_type == "heavy")
|
||||
# V6 锁死:开冒险后改存档选中→参数面/注入值不变(快照冻结;light=初始持有已解锁可切)
|
||||
Kit.check("冒险开始后锁死:改存档选中不影响参数面快照与生效值(契约 8)〔锻造工坊批 V6〕",
|
||||
mp5.forge_select_armor("light") and adr5.stats().armor_type == "heavy"
|
||||
mp5.forge_select("armor", "light") and adr5.stats().armor_type == "heavy"
|
||||
and int(r5.effective_ac()) == 12 + 1)
|
||||
# V5/V7 末端:enc 两轴直达 spawn 英雄快照(hero_attack 行携带选中类型键)
|
||||
var run_ax = _run_battle(1, 0, {"attack_type": "pierce", "armor_type": "ward"})
|
||||
|
||||
@@ -92,9 +92,9 @@ func _test_metaprogression_reference_identity() -> void:
|
||||
_write_v7_keystone(TPL % 2, 644)
|
||||
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,
|
||||
Kit.check_quiet(meta.save == t and meta.meta_state.keystone_balance() == 533,
|
||||
"sv10 宿主链取数走动态分发(meta.save 同一实例、读槽 1)〔卡① sv10〕")
|
||||
Kit.check_quiet(t.slots.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.meta_state.keystone_balance() == 644,
|
||||
"sv10 切槽后引用同一性保持(零换实例)且读数随槽(644)〔卡① sv10〕")
|
||||
meta.save.meta_state._add_keystone(6)
|
||||
Kit.check_quiet(t.write_to_disk() and t.meta_state.keystone_balance() == 650,
|
||||
|
||||
@@ -68,12 +68,12 @@ func _init() -> void:
|
||||
print("[r1] FAIL 前置 Lv2 达阈值应可购")
|
||||
# ⑧ 投影:gate_text 未达标拼门槛文案、达标清空
|
||||
sv.meta_state.set_tree_level("gate_pre", 1)
|
||||
var gate: String = mp.tree_gate_text("gate_dep")
|
||||
var gate: String = mp._tree_gate_text("gate_dep")
|
||||
if gate != "需 门槛前置 Lv2":
|
||||
failures += 1
|
||||
print("[r1] FAIL 未达标 gate_text 应=「需 门槛前置 Lv2」实得「%s」" % gate)
|
||||
sv.meta_state.set_tree_level("gate_pre", 2)
|
||||
if mp.tree_gate_text("gate_dep") != "":
|
||||
if mp._tree_gate_text("gate_dep") != "":
|
||||
failures += 1
|
||||
print("[r1] FAIL 达标后 gate_text 应为空串")
|
||||
if failures == 0:
|
||||
|
||||
@@ -840,7 +840,7 @@ func _career_overlay_projection() -> void:
|
||||
# 投影拼装(HudPresenter.career_data 同构)+ 视图喂入
|
||||
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.meta_state.career_state().boss_kill_count, "keystone": mp.keystone_balance()}
|
||||
"boss_kills": sv.meta_state.career_state().boss_kill_count, "keystone": mp.meta_state.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", "")),
|
||||
@@ -862,7 +862,7 @@ func _career_overlay_projection() -> void:
|
||||
Kit.check("Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" % [co._boss_first_label.text, co._boss_kills_label.text],
|
||||
String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次")
|
||||
Kit.check("钥石余额徽投影 = keystone_balance() 读数(%s)" % co._keystone_badge.value_label.text,
|
||||
String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance())
|
||||
String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.meta_state.keystone_balance())
|
||||
# 口径换代(2026-10-26):行标签换代钉子+生涯页零「通关」残留(负断言兜全页)
|
||||
var career_label_texts: Array[String] = []
|
||||
for career_lbl: Label in _all_labels(co):
|
||||
@@ -885,7 +885,7 @@ func _career_overlay_projection() -> void:
|
||||
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
|
||||
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.meta_state.career_state().boss_kill_count, "keystone": mp_v5.keystone_balance()}
|
||||
"boss_kills": v5.meta_state.career_state().boss_kill_count, "keystone": mp_v5.meta_state.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.meta_state.career_state().clear_counts.get(str(i + 1), 0)})
|
||||
|
||||
Reference in new issue
Block a user