From 52661761c348c9d712774d0c86d14a6c3fa2c9aa Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 28 Oct 2026 08:22:37 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=A1=E2=91=A0T6(=E6=A0=B8=E5=BF=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E9=9D=A2=E7=BB=93=E6=9E=84=E6=94=B6=E6=95=9B?= =?UTF-8?q?=E6=89=B9):=20meta=5Fprogression=E7=98=A6=E8=BA=AB(31=E2=86=922?= =?UTF-8?q?0=E5=85=AC=E5=BC=80=3DK3=E2=80=B2=E5=B0=81=E9=97=AD=E9=9D=A2?= =?UTF-8?q?=E5=AE=9A=E7=A8=BF)+status=5Fcontainer/treasure=5Fitems?= =?UTF-8?q?=E6=8B=86=E5=88=86=E2=80=94=E2=80=94=E7=BA=AF=E8=BD=AC=E5=8F=91?= =?UTF-8?q?=E9=80=80=E5=BD=B92(keystone=5Fbalance/is=5Fitem=5Funlocked?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E6=96=B9=E7=9B=B4=E8=BF=9Emeta.meta=5Fstate)?= =?UTF-8?q?/=E5=86=85=E8=81=943(hero=5Foverride=E6=94=B6=E7=BC=96=E9=94=BB?= =?UTF-8?q?=E9=80=A0=E5=8A=A0=E5=80=BC=E4=B8=8E=E6=A0=91=E6=B7=AC=E7=82=BC?= =?UTF-8?q?flat,=E8=BE=93=E5=87=BA=E9=80=90=E5=AD=97)/=E7=A7=813(=5Fforge?= =?UTF-8?q?=5Froute=5Funlocked/=5Ftree=5Fspent=5Ftotal/=5Ftree=5Fgate=5Fte?= =?UTF-8?q?xt)/=E5=B9=B6=E7=BB=843=E7=BB=84(forge=5Fselect(kind,rid)/hero?= =?UTF-8?q?=5Ftype=5Faxes{attack,armor}/tree=5Fcamp=5Fstatus{rows,fork=5Fg?= =?UTF-8?q?roups})/=E8=A1=A5meta=5Fstate=E8=A3=85=E9=85=8D=E6=88=90?= =?UTF-8?q?=E5=91=98+=E6=9C=AC=E7=B1=BB=E8=AF=BB=E5=86=99=E5=85=A8?= =?UTF-8?q?=E6=94=B9=E7=BB=8Fmeta=5Fstate;status=5Fcontainer=20apply=5Feff?= =?UTF-8?q?ect=E4=B8=89=E8=87=82=E5=9F=9F=E6=8B=86(=5Fgrant=5Fsnapshot/=5F?= =?UTF-8?q?apply=5Frefresh/=5Fapply=5Fstack=E5=85=AC=E5=85=B1=E5=B0=BE?= =?UTF-8?q?=E9=83=A8=E5=8D=95=E7=82=B9+=5Fnew=5Finstance=207=E2=86=925?= =?UTF-8?q?=E5=8F=82snap=E6=9D=9F)/treasure=5Fitems=20validate=5Fuse?= =?UTF-8?q?=E4=B8=89=E5=9F=9F(=5Fspatial=5Freject=5Freason/=5Fwindow=5Fcel?= =?UTF-8?q?ls/=5Fpreview=5Fwindow=5Fdead);=E6=B6=88=E8=B4=B9=E6=96=B99?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=94=B9=E5=9D=80(camp/hud/main/verifier/tre?= =?UTF-8?q?asure=5Finteract/forge/tree=5Frequires/save=5Fslot=5Fisolation/?= =?UTF-8?q?theme=5Fprojection);[=E6=B5=8B=E8=AF=95=E5=8A=A8=E4=BD=9C?= =?UTF-8?q?=E7=A7=BB=E4=BA=A4=E7=94=A8=E6=88=B7:=E5=A5=97=E4=BB=B6?= =?UTF-8?q?=E5=81=9C=E8=B7=91]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/meta_progression.gd | 257 +++++++++--------- game/core/status_container.gd | 64 +++-- game/core/treasure_items.gd | 63 +++-- game/presentation/camp_shop_presenter.gd | 10 +- game/presentation/hud_presenter.gd | 4 +- game/presentation/main.gd | 4 +- .../treasure_interact_controller.gd | 2 +- game/sim/verifier_batch.gd | 8 +- .../unit/core/meta_progression_forge_test.gd | 30 +- .../unit/core/save_slot_isolation_test.gd | 4 +- .../unit/core/tree_requires_level_test.gd | 4 +- .../presentation/theme_projection_test.gd | 6 +- 12 files changed, 238 insertions(+), 218 deletions(-) diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 6e7162a7..b3c16df5 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -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() diff --git a/game/core/status_container.gd b/game/core/status_container.gd index e1433e10..896ee8b0 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -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"], } diff --git a/game/core/treasure_items.gd b/game/core/treasure_items.gd index fb0ab1b9..786a60ec 100644 --- a/game/core/treasure_items.gd +++ b/game/core/treasure_items.gd @@ -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: diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index 86fcc5ec..a85bf399 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -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 diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index ee43f1fc..e57b7f97 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -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(), } diff --git a/game/presentation/main.gd b/game/presentation/main.gd index f9d18da5..729df063 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -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 [], diff --git a/game/presentation/treasure_interact_controller.gd b/game/presentation/treasure_interact_controller.gd index 7deab97d..759be778 100644 --- a/game/presentation/treasure_interact_controller.gd +++ b/game/presentation/treasure_interact_controller.gd @@ -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 diff --git a/game/sim/verifier_batch.gd b/game/sim/verifier_batch.gd index 53756c9e..ed732439 100644 --- a/game/sim/verifier_batch.gd +++ b/game/sim/verifier_batch.gd @@ -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 diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index 4bdfe10f..fa2bc037 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -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"}) diff --git a/game/tests/unit/core/save_slot_isolation_test.gd b/game/tests/unit/core/save_slot_isolation_test.gd index 3dc1d017..9d7070c8 100644 --- a/game/tests/unit/core/save_slot_isolation_test.gd +++ b/game/tests/unit/core/save_slot_isolation_test.gd @@ -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, diff --git a/game/tests/unit/core/tree_requires_level_test.gd b/game/tests/unit/core/tree_requires_level_test.gd index 732f8dcd..8879e59a 100644 --- a/game/tests/unit/core/tree_requires_level_test.gd +++ b/game/tests/unit/core/tree_requires_level_test.gd @@ -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: diff --git a/game/tests/unit/presentation/theme_projection_test.gd b/game/tests/unit/presentation/theme_projection_test.gd index 90f7966a..f8a9403a 100644 --- a/game/tests/unit/presentation/theme_projection_test.gd +++ b/game/tests/unit/presentation/theme_projection_test.gd @@ -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)})