From 3b9ee9f134270874ffce7477d0d9b578a12184e3 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 4 Oct 2026 03:02:03 +0800 Subject: [PATCH] =?UTF-8?q?T8+T9+T10=E6=89=B9=E6=AC=A1=E4=BA=8C=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=EF=BC=88=E8=BF=AD=E4=BB=A312=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=A4=A9=E8=B5=8B=E6=A0=91+=E5=AD=98=E6=A1=A3v5+=E8=90=A5?= =?UTF-8?q?=E5=9C=B0=E6=A0=91UI+A/B=E9=AA=8C=E8=AF=81=EF=BC=89=EF=BC=9AT8?= =?UTF-8?q?=E6=A0=91=E7=B3=BB=E7=BB=9F=3DMetaTreeTable=20schema=EF=BC=88ve?= =?UTF-8?q?in=E5=9B=9B=E8=84=89/kind=E5=85=AD=E7=B1=BB/=E5=8F=8C=E5=B8=81c?= =?UTF-8?q?urrency/row-gating=20SPINE=5FROW=5FGATE=3D4/effects=E5=9B=9B?= =?UTF-8?q?=E7=BA=BF+rule=E9=94=AE=E7=99=BD=E5=90=8D=E5=8D=95/=E9=9B=B6arc?= =?UTF-8?q?hetype=E5=B1=82=E7=BA=A7=E6=8A=A4=E6=A0=8F/Fork=E4=BA=92?= =?UTF-8?q?=E6=96=A5=E5=AF=B9=E7=BB=93=E6=9E=84/=E5=BD=A9=E8=9B=8B?= =?UTF-8?q?=E2=89=A43=EF=BC=89+meta=5Ftree.tres=2033=E8=8A=82=E7=82=B9?= =?UTF-8?q?=EF=BC=88=E8=84=8A=E6=9F=B14=E8=A1=8C=C3=974=E7=BA=BFGP?= =?UTF-8?q?=E4=BB=B710=C3=97row/=E9=94=BB=E9=80=A0=E8=84=893=E6=94=B6?= =?UTF-8?q?=E7=BC=96forge=5Flevels=E9=92=A5=E7=9F=B3=E4=BB=98=E4=BB=B7?= =?UTF-8?q?=E9=9B=B6=E7=97=9B/=E9=92=A5=E7=9F=B3=E8=84=89workshop=E6=94=B6?= =?UTF-8?q?=E7=BC=96+Y=E5=8D=87=E7=BA=A7=E7=A7=BB=E6=A4=8D/=E5=90=8C?= =?UTF-8?q?=E8=B0=83socket3+Fork4=E5=AF=B9=E9=9B=86=E4=B8=AD=E5=90=8C?= =?UTF-8?q?=E8=B0=83=E8=84=89/=E5=BD=A9=E8=9B=8B1=E9=94=BB=E9=80=A0?= =?UTF-8?q?=E6=BB=A1=E6=8E=92=E8=A7=89=E9=86=92=EF=BC=89+MetaProgression?= =?UTF-8?q?=E6=A0=91=E6=AE=B5=EF=BC=88tree=5Fspent=5Ftotal/tree=5Fcan=5Fbu?= =?UTF-8?q?y=20row-gating+=E5=89=8D=E7=BD=AE+=E5=B8=81+cap/tree=5Fbuy?= =?UTF-8?q?=E5=8D=95=E5=B8=81=E9=92=A5=E7=9F=B3=E6=89=BF=E8=BD=BD=E6=8A=AB?= =?UTF-8?q?=E9=9C=B2/tree=5Fbuy=5Ffork=E8=B4=AD=E5=8D=B3=E6=BF=80=E6=B4=BB?= =?UTF-8?q?/tree=5Ftoggle=5Ffork=E5=85=8D=E8=B4=B9=E5=88=87=E6=8D=A2/tree?= =?UTF-8?q?=5Fstat=5Fflat+rule=5Fkeys=E6=8A=95=E5=BD=B1/=5Fcheck=5Feaster?= =?UTF-8?q?=5Fegg=E8=A7=89=E9=86=92=EF=BC=89+rule=E9=94=AE=E6=B6=88?= =?UTF-8?q?=E8=B4=B9=E9=9D=A2=EF=BC=88posture=5Ffirst=5Fstrike=3D=E9=A6=96?= =?UTF-8?q?=E8=BD=AEAP+1/posture=5Fbulwark=3D=E5=8F=97=E5=87=BB=E5=87=8F1?= =?UTF-8?q?=E2=80=94=E2=80=94enc.tree=5Frules=E9=80=8F=E4=BC=A0=E9=93=BE?= =?UTF-8?q?=EF=BC=89=EF=BC=9BT9=E5=AD=98=E6=A1=A3v5=EF=BC=88SAVE=5FFORMAT?= =?UTF-8?q?=5FVERSION=3D5=E6=A0=91=E5=9F=9Ftree=5Fnodes/fork=5Fsides?= =?UTF-8?q?=E4=B8=A4=E6=96=B0=E5=9F=9F+v4=E2=86=92v5=E8=BF=81=E7=A7=BB+?= =?UTF-8?q?=E4=B8=89=E4=BD=93=E7=B3=BB=E8=BF=81=E7=A7=BBmigrate=5Ftree=3Df?= =?UTF-8?q?orge=E5=8E=9F=E6=A0=B7=E6=98=A0=E5=B0=84=E9=9B=B6=E4=BA=8F?= =?UTF-8?q?=E6=8D=9F/item=5Funlocks=E2=86=92ks=5Fworkshop/=E5=90=8C?= =?UTF-8?q?=E8=B0=83Slots=E2=86=92socket=E7=82=B9=E4=BA=AE+=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E5=B9=82=E7=AD=89+=E6=96=AD=E7=82=B9=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=96=AD=E8=A8=804=E2=86=925=E6=8D=A2=E4=BB=A3?= =?UTF-8?q?=EF=BC=89+=E8=90=A5=E5=9C=B0=E6=A0=91UI=EF=BC=88=E6=A0=91?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=3D=E5=9B=BE=E9=89=B4=E6=9F=9C=E5=8F=B0?= =?UTF-8?q?=E4=B8=8B=E6=AE=B5=E7=A8=8B=E5=BA=8F=E5=8D=A0=E4=BD=8D=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=B8=89=E6=9F=9C=E5=8F=B0=E2=86=92=E4=B8=80=E6=A0=91?= =?UTF-8?q?=E5=AD=97=E9=9D=A2=E9=87=8D=E6=9E=84=E5=BB=B6=E5=90=8E=E6=8A=AB?= =?UTF-8?q?=E9=9C=B2+=E4=BF=A1=E5=8F=B7tree=5Fbuy=5Frequested/tree=5Ffork?= =?UTF-8?q?=5Ftoggled+main=E6=8E=A5=E7=BA=BF+=E6=8A=95=E5=BD=B1=E4=B8=A4?= =?UTF-8?q?=E5=87=BD=E6=95=B0=EF=BC=89=EF=BC=9BT10=3DMC=E7=AC=AC7=E5=8F=82?= =?UTF-8?q?=E6=89=A9ab=5Fa/ab=5Fb+A/B=E4=B8=A4=E4=BE=A7=E9=94=9AN=3D20?= =?UTF-8?q?=E9=85=8D=E5=AF=B9=EF=BC=88=E5=85=88=E6=89=8B=E5=8E=8B=E5=88=B6?= =?UTF-8?q?/=E7=A8=B3=E5=81=A5=E6=8B=93=E8=8D=92rule=E9=94=AE=E9=9B=86?= =?UTF-8?q?=E9=80=8F=E4=BC=A0=EF=BC=89=E2=80=94=E2=80=94=E4=B8=A4=E4=BE=A7?= =?UTF-8?q?=E5=B7=AE0.0pp<<3pp=E9=AA=8C=E6=94=B6=E8=BF=87=EF=BC=88A-4?= =?UTF-8?q?=E5=8F=8D=E4=BE=8B=E4=B8=8D=E8=A7=A6=E5=8F=91=EF=BC=89+taken=20?= =?UTF-8?q?25.0/22.9=E5=87=8F=E4=BC=A4=E6=96=B9=E5=90=91=E5=85=91=E7=8E=B0?= =?UTF-8?q?=E4=BD=90=E8=AF=81=EF=BC=9B=E4=BF=AE=E7=BC=A9=E8=BF=9B=E5=9D=91?= =?UTF-8?q?=E5=A4=8D=E5=8F=91=EF=BC=88adventure=5Frunner:826=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=90=8C=E5=9D=91=E4=B8=89=E7=8A=AF=E7=95=99=E7=97=95?= =?UTF-8?q?=EF=BC=89+=E8=BF=81=E7=A7=BBunlock=E6=A3=80=E6=9F=A5=E5=85=A8?= =?UTF-8?q?=E8=A1=8C=E9=9B=86=EF=BC=88aux=5Flive=5Fids=E7=B3=BB=E8=BF=87?= =?UTF-8?q?=E6=B8=A1=E5=8F=A3=E5=BE=84=E5=90=8C=E8=B0=83=E8=A1=8C=E4=B8=8D?= =?UTF-8?q?=E5=9C=A8=E2=80=94=E2=80=94=E8=BF=94=E5=B7=A5=E7=95=99=E7=97=95?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E5=86=92=E7=83=9F=E6=A0=91=E6=AE=B518?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E2=86=92714/0+run=5Fall=2015+=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E7=BB=BF+doc=E4=B8=89=E9=97=B8=E5=9F=BA=E7=BA=BF?= =?UTF-8?q?=E5=90=8C=E6=AC=BE+=E7=86=B5=E5=A2=9EPASS=EF=BC=9BT10=E5=87=BA?= =?UTF-8?q?=E5=8F=A3=E5=8C=85design/qa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qa/m4iter12-t10-batch2-exit-2026-10-04.md | 34 ++ game/core/adventure_runner.gd | 3 + game/core/battle_orchestrator.gd | 7 + game/core/config_db.gd | 12 + game/core/damage_controller.gd | 3 + game/core/level_runner.gd | 9 + game/core/meta_progression.gd | 203 ++++++++- game/core/save_manager.gd | 51 ++- game/data/schemas/meta_tree_table.gd | 118 +++++ game/data/schemas/meta_tree_table.gd.uid | 1 + game/data/tables/meta_tree.tres | 415 ++++++++++++++++++ game/presentation/main.gd | 27 ++ game/presentation/views/camp_overlay_view.gd | 62 ++- game/smoke/adventure_monte_carlo.gd | 11 +- game/smoke/level_autoplay.gd | 3 + game/smoke/smoke_all.gd | 60 ++- 16 files changed, 1005 insertions(+), 14 deletions(-) create mode 100644 design/qa/m4iter12-t10-batch2-exit-2026-10-04.md create mode 100644 game/data/schemas/meta_tree_table.gd create mode 100644 game/data/schemas/meta_tree_table.gd.uid create mode 100644 game/data/tables/meta_tree.tres diff --git a/design/qa/m4iter12-t10-batch2-exit-2026-10-04.md b/design/qa/m4iter12-t10-batch2-exit-2026-10-04.md new file mode 100644 index 00000000..f3317776 --- /dev/null +++ b/design/qa/m4iter12-t10-batch2-exit-2026-10-04.md @@ -0,0 +1,34 @@ +# 迭代 12 批次二 T10 出口包(2026-10-04) + +- 锚件:`runsum_m4iter12_t10aba_n20_20261004.jsonl`(A 侧)/ `runsum_m4iter12_t10abb_n20_20261004.jsonl`(B 侧)——N=20/侧,seed0=90000 同种子配对,normal 档 +- 采集档位:**二档·数值修改验证**(本触发点总量 40=每侧 20×2 贴边合规;T7 批次一触发点 40 独立计) +- A/B 口径:Fork posture 对两侧 rule 键集透传(A=先手压制 posture_first_strike=战斗首轮 AP+1 / B=稳健拓荒 posture_bulwark=受击减 1)——「两侧不同强非强弱」验收载体 + +## A/B 面两侧差读数(验收 <~3pp) + +| 指标 | A 先手压制 | B 稳健拓荒 | Δ | +|---|---|---|---| +| 全通率 | 0.0% | 0.0% | **0.0pp** ✓ | +| L1 通关率 | 5.0% | 5.0% | **0.0pp** ✓ | +| L2 通关率 | 0.0% | 0.0% | **0.0pp** ✓ | +| 局均承伤 | 25.0 | 22.9 | B 侧减伤方向兑现(rule 键真实生效佐证)| + +**验收判定:过**(两侧差 0.0pp << ~3pp 阈——A-4 反例条件不触发)。 + +## 批末闸全量留痕 + +冒烟 **714/0** 三闸 PASS(批次一 696+树段 18 断言)+ run_all **15 套全绿** + 探针(ui_input/ui_probe/ui_layout/fx/t17_defer 五绿+click 存量红零新增)+ doc 三闸基线同款(gdd_lint 2 存量/doc_term 4 存量/doc_link 0)+ 熵增 PASS。迁移幂等断言在冒烟树段(重复迁移零变化)。MC 触发点全程:TP0 20+T7 40+T10 40=100 局(三触发点各自 ≤40 合规)。 + +## 完成定义对账(任务书 §一五条) + +| # | 定义 | 证据 | 判定 | +|---|---|---|---| +| ① | 七流派各 ≥3 件入局内随机渠道+引擎判成型 | relics.tres 27 件(4/4/4/4/4/3/4);RelicTable.validate 扫雷端铁律;档位激活引擎冒烟断言(T1/T4) | ✅ | +| ② | 七流派成型态 MC 读数可判+红线自检①全流派过 | T7 off/formed 配对锚(L1 5→75% 分岔方向);红线①方向判定过(T7 报告) | ✅ | +| ③ | 天赋树可购可切可迁移+存档往返幂等 | tree_buy/tree_buy_fork/tree_toggle_fork/migrate_tree 冒烟断言(row-gating/互斥/免费切换/幂等);save v5 迁移链 v1→v5 全链 | ✅ | +| ④ | 文档正主七处吸收落文 | 随落籍批提交(node-reward/冒险结构/registry/06 于 TP0+本批元进度/树新节——「吸收即死」退役面随批执行) | ✅*(正文七处=定稿 §文档面:元进度/node-reward/战斗结算/行动经济/地形变异/魔法道具/商店 spec——战斗结算/行动经济/地形变异/魔法道具四正主本批零语义变更(relic 挂点均为既有段槽消费),落文面=元进度树节+node-reward v2.4+冒险结构 v0.14+registry 三处+06 两条——披露差异待用户追认) | +| ⑤ | 批末闸零新增 | 本报告上节 | ✅ | + +## provisional 披露(批次二随批代拍板——人工检查清单母本) + +存档 v4→v5(任务书「v3→v4」系同调批未落时行文顺延披露);树 GP 节点钥石单币承载(GP 局外余额域缺位);「三柜台→一树」字面重构延后(树面板=图鉴柜台下段程序占位,锻造柜台保留一版周期);Fork 首期两 rule 键真实接线(posture 对)余三组 stat 面 provisional;hit 线不入 override 三字段通道(命中折扣档消费归数值批);迁移差额补偿=原样映射零亏损(钥石付价不变,无亏损面故无补偿金)。 diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index a9f9f49e..f5f0019f 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -82,6 +82,7 @@ var _resume_bp := {} # 续玩快照暂存(resume_adventur var _bonus_taken := {} # 件获取次数计数(kind 维度派生账本——断点附随件与摘要读数承载;池库存正主=_bonus_deck) var _bonus_deck: Array = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账;迭代 12 T1 起机制件带=relic 令牌随表追加) var _relics: Array[String] = [] # 流派件持有集(迭代 12 T1——局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树) +var _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) var _bonus_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点 var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付) var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗) @@ -130,6 +131,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: # 流派件开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派 # 各 2 件档一成型——测量面专用;真实局流派件只走局内随机渠道,红线②层级纪律不破〕) var relic_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else [] + _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # 迭代 12 T10——A/B 面 rule 键集 if not relic_seed.is_empty(): _relics = [] for r in relic_seed: @@ -825,6 +827,7 @@ func _start_current_level() -> bool: _runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算) _runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入) _runner.set_relics(_relics) # 迭代 12 T1——流派件持有集下传(begin_battle enc.relics 求值面) + _runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费) if _shop_mode == "pool": # 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动) _runner.set_shop_pool(_bonus_deck, Callable(self, "_consume_pool_purchase")) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 82629ae3..bb966600 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -253,6 +253,13 @@ func begin_battle(enc: Dictionary) -> bool: _relic_combo = 0 _relic_deep_dug = 0 _relic_magma_layers = 0 + # 树 rule 键消费(迭代 12 T10——Fork 激活侧 effects;真实接线首期两键:先手压制=战斗首轮 + # AP+1(queue_ap_delta——先手压制语义)/稳健拓荒=敌方攻击减 1(伤害控制器 flags bulwark); + # 其余 rule 键=stat 面投影不入战斗(T8 表内容 provisional 口径披露)) + var tree_rules: Array = enc.get("tree_rules", []) as Array + if tree_rules.has("posture_first_strike"): + action_economy.queue_ap_delta(1) + _damage.relic_flags["bulwark"] = 1 if tree_rules.has("posture_bulwark") else 0 # 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现 # 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后 # 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节) diff --git a/game/core/config_db.gd b/game/core/config_db.gd index e07da475..22a2acae 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -23,6 +23,7 @@ const TABLE_MANIFEST := { "difficulty": "difficulty.tres", "shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费) "meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍) + "meta_tree": "meta_tree.tres", # 迭代 12 T8 双层 BD 天赋树(MetaTreeTable 定义表 Rule 4 豁免——R1 甲融合案 33 节点) "aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注) "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) "attunement_growth": "attunement_growth.tres", # 迭代 11 第①批 同调实施批 T1(AttunementGrowthTable 单行全局——同调位里程碑谓词表;单行先例 Rule 4 豁免) @@ -45,6 +46,7 @@ var difficulty: DifficultyTable var shop: ShopTable = null # 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题) var meta: MetaTable +var meta_tree: MetaTreeTable # 迭代 12 T8 类型化持用(消费面走 tree_node 访问器) var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器) var adventure: AdventureTable var attunement_growth: AttunementGrowthTable # 迭代 11 第①批 T1 类型化持用(消费面走 attunement_* 访问器) @@ -141,6 +143,7 @@ func load_tables(root: String, collect_only: bool) -> bool: adventure = loaded.get("adventure") attunement_growth = loaded.get("attunement_growth") relics = loaded.get("relics") + meta_tree = loaded.get("meta_tree") is_ready = not has_errors return not has_errors @@ -156,6 +159,15 @@ func relic_rows() -> Array: return relics.rows if relics != null else [] +# 天赋树节点行(迭代 12 T8——MetaProgression 树段/营地树 UI 取数口) +func tree_node(node_id: String) -> Dictionary: + return meta_tree.row_by_id(node_id) if meta_tree != null else {} + + +func tree_rows() -> Array: + return meta_tree.rows if meta_tree != null else [] + + # 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 2026-10-02 修复批迁出: # ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名零变化。 func _cross_validate(loaded: Dictionary) -> void: diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 8ba32c79..f2ec3a43 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -310,6 +310,9 @@ func _resolve_enemy_skill(t: Dictionary) -> void: var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区 # 铁壁整备(迭代 12 T5 蓄力流——每层下轮开场 S 点减伤;减后过下限保底) var shield := int(relic_flags.get("shield_per_layer", 0)) * int(relic_flags.get("charge_layers", 0)) + # 稳健拓荒(迭代 12 T10——Fork posture B 侧 rule 键:受击减 1;两侧对称弱值=「不同强非强弱」) + if int(relic_flags.get("bulwark", 0)) > 0: + shield += 1 if shield > 0: total = maxi(int(db.settlement.damage_floor), total - shield) units.apply_damage(hero, total) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 97c020ef..1d164437 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -479,6 +479,7 @@ func _start_battle(node: Dictionary) -> bool: "attunement": _attunement, "attune_charges": _attune_charges, "relics": _relics, + "tree_rules": _tree_rules, "node_type": String(node.get("type", ""))}): push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1))) return false @@ -598,6 +599,14 @@ func set_relics(ids: Array) -> void: _relics = ids +# 树 rule 键集注入(迭代 12 T10——begin_battle enc.tree_rules 携带;Fork 激活侧 effects) +var _tree_rules: Array = [] + + +func set_tree_rules(rules: Array) -> void: + _tree_rules = rules.duplicate() + + # 同调主动件充能账本注入(迭代 11 第①批 T6——共享 dict 引用直改;能力件骑库存面注入: # 差额注入防续玩双份——resume 路径 restore_inventory 已含断点内 attune 记录) func set_attune_charges(charges: Dictionary) -> void: diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 03ac6604..a89ade6e 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -142,16 +142,211 @@ func settle_adventure(summary: Dictionary) -> Dictionary: } -# ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档)---- +# ---- 跨局注入(冒险开始参数面——字段清单随柜台扩展,现 = 属性锻造三档+树四线之三条)---- # 全零 = 白板合法态(无解锁不得改变任何局内规则);override 禁写表(表运行期只读)。 +# 迭代 12 T8:树四线 flat 并入(脊柱 stat+锻造脉+彩蛋——同一覆盖值通道;hit 线消费面= +# 命中折扣档归数值批,本批不入 override 三字段通道——披露)。 func hero_override() -> Dictionary: return { - "atk": forge_flat("atk"), - "max_hp": forge_flat("max_hp"), - "armor_class": forge_flat("armor_class"), + "atk": forge_flat("atk") + tree_stat_flat("atk"), + "max_hp": forge_flat("max_hp") + tree_stat_flat("max_hp"), + "armor_class": forge_flat("armor_class") + tree_stat_flat("armor_class"), } +# ---- 天赋树段(迭代 12 T8——R1 甲融合案;设计权威 整合定稿 §1.4 一树双币三脉一脊)---- + +# 全树已购总等级(row-gating 输入——「投入点数累计解锁新行」) +func tree_spent_total() -> int: + var total := 0 + for lvl in (save.tree_nodes() as Dictionary).values(): + total += int(lvl) + return total + + +# 节点可购校验(row-gating + 显式前置 + 币 + cap;free 节点=迁移/彩蛋不经购买) +func tree_can_buy(node_id: String) -> bool: + var row: Dictionary = db.tree_node(node_id) + if row.is_empty(): + return false + var lvl := save.tree_level(node_id) + if lvl >= int(row.get("cap", 1)): + return false + var row_num := int(row.get("row", 0)) + if row_num > 0 and tree_spent_total() < db.meta_tree.row_gate(row_num): + return false + for req in (row.get("requires", []) as Array): + if save.tree_level(String(req)) < 1: + return false + var fg := String(row.get("fork_group", "")) + if fg != "" and save.fork_side(fg) != "": + return false # 同组已激活一侧——互斥(切另侧走 tree_toggle_fork 免费切换) + return true + + +# 节点购买(营地树 UI 唯一写口):**单币=钥石先行**(双币铁律中 GP 需局外余额承载域——GP 冒险 +# 收束清零=局内死钱不跨局,GP 余额域缺位,脊柱 GP 节点暂走钥石消费——表 currency 字段保留语义, +# T9/数值批补 GP 余额域后切回——台账披露待追认)。free=迁移点亮/彩蛋觉醒零币。 +func tree_buy(node_id: String) -> bool: + var row: Dictionary = db.tree_node(node_id) + if row.is_empty() or not tree_can_buy(node_id): + return false + var price := int(row.get("price", 0)) + var currency := String(row.get("currency", "gp")) + if currency == "gp" or currency == "keystone": + if not save.spend_keystone(price): + return false # T8 骨架单币承载(GP 余额域缺位——披露待追认) + save.set_tree_level(node_id, save.tree_level(node_id) + 1) + _check_easter_egg() + return save.write_to_disk() + + +# Fork 激活/切换(营地树 UI——同组换侧零币=免费切换裁定;未激活组首次激活=消费购买面 tree_buy +# 已购该侧后此处激活;同侧重复=零操作) +func tree_toggle_fork(group: String, side: String) -> bool: + if not db.meta_tree.FORK_GROUPS.has(group) or not (side == "a" or side == "b"): + return false + var cur := save.fork_side(group) + if cur == side: + return false # 同侧零操作 + if cur == "": + return false # 未激活组走购买面(tree_buy 激活侧节点后由激活写入——见下) + save.set_fork_side(group, side) + return save.write_to_disk() + + +# Fork 侧节点购买入口(购节点即激活该侧——组内互斥由 can_buy 拦截另侧) +func tree_buy_fork(node_id: String) -> bool: + var row: Dictionary = db.tree_node(node_id) + if row.is_empty() or String(row.get("kind", "")) != "fork": + return tree_buy(node_id) + if not tree_can_buy(node_id): + return false + var price := int(row.get("price", 0)) + if not save.spend_keystone(price): + return false + save.set_tree_level(node_id, 1) + save.set_fork_side(String(row.get("fork_group", "")), String(row.get("fork_side", ""))) + return save.write_to_disk() + + +# 树效果投影:四线 flat 汇总 + rule 键集(激活 Fork 侧 effects 并入) +func tree_stat_flat(key: String) -> int: + var total := 0 + for row in db.tree_rows(): + var lvl := save.tree_level(String(row.get("node_id", ""))) + if lvl <= 0: + continue + total += int((row.get("effects", {}) as Dictionary).get(key, 0)) * lvl + return total + + +func tree_rule_keys() -> Array: + var out: Array = [] + var sides: Dictionary = save.fork_sides() + for row in db.tree_rows(): + if String(row.get("kind", "")) != "fork": + continue + var fg := String(row.get("fork_group", "")) + if String(sides.get(fg, "")) != String(row.get("fork_side", "")): + continue + for k in (row.get("effects", {}) as Dictionary).keys(): + if not out.has(String(k)): + out.append(String(k)) + return out + + +# 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工) +func tree_camp_rows() -> Array: + var out: Array = [] + for row in db.tree_rows(): + if String(row.get("kind", "")) == "fork": + continue # Fork 行由 fork_groups 段承载 + out.append({ + "node_id": String(row.get("node_id", "")), + "display_name": String(row.get("display_name", "")), + "vein": String(row.get("vein", "")), + "kind": String(row.get("kind", "")), + "cap": int(row.get("cap", 1)), + "level": save.tree_level(String(row.get("node_id", ""))), + "price": int(row.get("price", 0)), + "currency": String(row.get("currency", "gp")), + "can_buy": tree_can_buy(String(row.get("node_id", ""))), + }) + return out + + +func tree_fork_groups() -> Array: + var out: Array = [] + var sides: Dictionary = save.fork_sides() + for fg in db.meta_tree.FORK_GROUPS: + var group_rows: Array = [] + for row in db.tree_rows(): + if String(row.get("fork_group", "")) == String(fg): + group_rows.append(row) + if group_rows.is_empty(): + continue + var sides_out: Array = [] + for row in group_rows: + sides_out.append({ + "side": String(row.get("fork_side", "")), + "node_id": String(row.get("node_id", "")), + "display_name": String(row.get("display_name", "")), + "owned": save.tree_level(String(row.get("node_id", ""))) > 0, + }) + out.append({ + "group": String(fg), + "display_name": String(group_rows[0].get("display_name", String(fg))).split("·")[0], + "active_side": String(sides.get(String(fg), "")), + "sides": sides_out, + }) + return out + + +# 彩蛋觉醒检查(锻造脉三节点全满→easter_ley 免费点亮——发现率监控 T10 呈报) +func _check_easter_egg() -> void: + var row: Dictionary = db.tree_node("easter_ley") + if row.is_empty() or save.tree_level("easter_ley") > 0: + return + for req in (row.get("requires", []) as Array): + var req_row: Dictionary = db.tree_node(String(req)) + if save.tree_level(String(req)) < int(req_row.get("cap", 1)): + return + save.set_tree_level("easter_ley", 1) + + +# 三体系迁移(迭代 12 T9——v4→v5 旧域只读→树域点亮;幂等=重复迁移零变化): +# forge_levels 等级→锻造脉节点树级原样映射(钥石付价不变零亏损——「宁多勿少」以零亏损满足); +# item_unlocks 任一真→ks_workshop 点亮;同调槽位解锁态→socket 点亮(谓词表 Slots 求值≥i)。 +func migrate_tree() -> Dictionary: + var migrated := {"forge": 0, "unlock": 0, "socket": 0} + for pair in [["forge_atk", "atk"], ["forge_hp", "max_hp"], ["forge_ac", "armor_class"]]: + var lvl := save.forge_level(String(pair[1])) + if lvl > 0 and save.tree_level(String(pair[0])) < lvl: + save.set_tree_level(String(pair[0]), lvl) + migrated["forge"] += 1 + var any_unlock := false + for iid in db.aux_live_ids().keys(): + if save.item_unlocked(String(iid)): + any_unlock = true + break + if not any_unlock: + for row in db.aux_attunement_rows(): + if save.item_unlocked(String(row.get("item_id", ""))): + any_unlock = true + break + if any_unlock and save.tree_level("ks_workshop") < 1: + save.set_tree_level("ks_workshop", 1) + migrated["unlock"] += 1 + var slots := Attunement.slots(db, save) + for i in range(1, slots + 1): + if save.tree_level("att_socket%d" % i) < 1: + save.set_tree_level("att_socket%d" % i, 1) + migrated["socket"] += 1 + save.write_to_disk() + return migrated + + # ---- 同调位账本(迭代 11 第①批 T4——装配关系/同调位求值/Y 刷新升级;设计权威 proposal §2 + 任务书 P1 甲)---- # 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选; diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 4b042cad..363a525e 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -36,7 +36,7 @@ class_name SaveManager extends RefCounted -const SAVE_FORMAT_VERSION := 4 # registry schema_version:v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 +const SAVE_FORMAT_VERSION := 5 # registry schema_version:v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;任务书「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台) @@ -54,6 +54,7 @@ func _init(path: String = "user://save.json") -> void: register_migration(1, _migrate_v1_v2) register_migration(2, _migrate_v2_v3) register_migration(3, _migrate_v3_v4) + register_migration(4, _migrate_v4_v5) # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) @@ -80,6 +81,22 @@ func _migrate_v3_v4(data: Dictionary) -> Dictionary: return data +# v4→v5:天赋树域+三体系迁移(迭代 12 T9——R1 甲融合案;差额补偿原则=宁多勿少): +# ①tree_nodes/fork_sides 两新域(缺键默认补齐);②forge_levels 已购等级→锻造脉节点等效点亮 +# (原样映射零亏损——钥石付价不变);③item_unlocks 已解锁→ks_workshop Unlock 节点点亮; +# ④attunement 槽位解锁态→同调 socket 点亮(已拥有道具/装配关系不动——装配交互不变裁定); +# ⑤迁移幂等由 MetaProgression.migrate_tree 侧承载(重复迁移零变化——T10 断言面)。 +# 旧三域只读保留一版周期(forge_levels/item_unlocks 读取面维持——回滚窗口)。 +func _migrate_v4_v5(data: Dictionary) -> Dictionary: + var meta: Dictionary = data.get("meta", {}) as Dictionary + if not meta.has("tree_nodes"): + meta["tree_nodes"] = {} + if not meta.has("fork_sides"): + meta["fork_sides"] = {} + data["version"] = SAVE_FORMAT_VERSION + return data + + # ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)---- func _default_data() -> Dictionary[String, Variant]: @@ -94,6 +111,8 @@ func _default_data() -> Dictionary[String, Variant]: "boss_kill_count": 0, "attunement_equipped": [], "attune_refresh_level": 0, + "tree_nodes": {}, # 天赋树已购等级(node_id→int——迭代 12 T9 树域) + "fork_sides": {}, # A/B 激活侧(fork_group→"a"|"b"——营地免费切换承载) "recent_summaries": [], "first_adventure_done": false, }, @@ -232,6 +251,36 @@ func boss_kill_count() -> int: return int(_data["meta"].get("boss_kill_count", 0)) +# ---- 天赋树域(迭代 12 T9——MetaProgression 树段读写面)---- + +func tree_nodes() -> Dictionary: + return (_data["meta"].get("tree_nodes", {}) as Dictionary).duplicate() + + +func tree_level(node_id: String) -> int: + return int((_data["meta"].get("tree_nodes", {}) as Dictionary).get(node_id, 0)) + + +func set_tree_level(node_id: String, level: int) -> void: + var tn: Dictionary = _data["meta"].get("tree_nodes", {}) as Dictionary + tn[node_id] = maxi(0, level) + _data["meta"]["tree_nodes"] = tn + + +func fork_sides() -> Dictionary: + return (_data["meta"].get("fork_sides", {}) as Dictionary).duplicate() + + +func fork_side(group: String) -> String: + return String((_data["meta"].get("fork_sides", {}) as Dictionary).get(group, "")) + + +func set_fork_side(group: String, side: String) -> void: + var fs: Dictionary = _data["meta"].get("fork_sides", {}) as Dictionary + fs[group] = side + _data["meta"]["fork_sides"] = fs + + # 装配关系(同调目录 item_id 有序数组——装配序即呈现序;跨冒险持久,冒险中途不可换装〔底稿 §8.5〕)。 # 局外进度事实(元进度域同族),非契约 2 反面清单的「局内道具库存」——存档序列化.md 契约 2 随批注记。 func attunement_equipped() -> Array: diff --git a/game/data/schemas/meta_tree_table.gd b/game/data/schemas/meta_tree_table.gd new file mode 100644 index 00000000..6560f672 --- /dev/null +++ b/game/data/schemas/meta_tree_table.gd @@ -0,0 +1,118 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Validation Question: 三旧体系(锻造/钥石/同调)收敛为一棵营地天赋树后,双币消费、row-gating、 +# A/B 免费切换、彩蛋觉醒能否机械承载(P0——迭代 12 批次二 T8,R1 甲融合案) +# Date: 2026-10-04(双层 BD 实施批 T8;设计权威 双层BD体系-整合定稿-2026-10-03 §1.4) +# 数据配置 MetaTreeTable schema(元进度 回填 owner 批末落文;行 = Dictionary 键 node_id,Rule 4 豁免 +# 定义表——RelicTable 先例)。 +# 行字段:node_id / display_name / vein ∈ VEINS(spine=主干脊柱 row-gating,GP 买深度;forge=锻造脉 +# 数值专精;keystone=钥石脉内容解锁;attune=同调脉槽位与规则)/ kind ∈ KINDS(stat=四线数值/ +# rule=全局规则/unlock=内容解锁/socket=同调位/fork=A/B 互斥对/easter=彩蛋觉醒)/ +# currency ∈ {gp, keystone, free}(双币铁律:GP 喂脊柱数值、钥石喂 Unlock/Socket/Fork——哈迪斯镜内 +# 双币同构;free=迁移点亮与彩蛋)/ price [0,9999] / cap [1,9](等级上限;fork/unlock 恒 1)/ +# row(脊柱行号 1..N——row-gating:行 N 可购条件 = 全树已购等级总和 ≥ SPINE_ROW_GATE×(N-1))/ +# requires Array[node_id](显式前置——父开子脉内链)/ effects Dictionary(stat 四线 flat: +# atk/max_hp/hit/armor_class;rule 键封闭 RULE_KEYS)/ fork_group/fork_side(a/b——同组互斥、 +# 营地免费切换)。 +# 🔴 层级纪律(任务书红线 2):局外树只放全局规则件——effects 禁流派标签(relic 流派件永不入树, +# archetype 键出域=加载致命);流派件只走局内随机渠道。 +# A/B 只放规则面(定稿 1.3——全树互斥面 ≤10-15% 首期 4 组);两侧「不同强非强弱」MC 验差 <~3pp(T10)。 +# 彩蛋 ≤3(定稿 1.4——点满某脉某排免费觉醒;发现率 <5% 改灰色预告——T10 追认单呈报)。 +class_name MetaTreeTable +extends Resource + +@export var rows: Array = [] # Array[Dictionary] 键 node_id + +const VEINS := ["spine", "forge", "keystone", "attune"] +const KINDS := ["stat", "rule", "unlock", "socket", "fork", "easter"] +const CURRENCIES := ["gp", "keystone", "free"] +const STAT_KEYS := ["atk", "max_hp", "hit", "armor_class"] +## 规则键封闭词表(零裸 flat 同族护栏——effects.rule 键出域=加载致命;首期两键=T8 Fork 面真实接线) +const RULE_KEYS := ["posture_first_strike", "posture_bulwark"] +const KEY_WHITELIST := ["node_id", "display_name", "vein", "kind", "currency", "price", "cap", + "row", "requires", "effects", "fork_group", "fork_side", "note"] +## 脊柱行门槛(row-gating:行 N 可购需全树已购总等级 ≥ GATE×(N−1)——「投入点数累计解锁新行」) +const SPINE_ROW_GATE := 4 + +## A/B 组词表(同组恰两侧 a/b——互斥对;营地免费切换承载面) +const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy", "fg_mastery"] + + +func validate() -> Array: + var errs: Array = [] + if rows.is_empty(): + return errs # 空表合法态(T8 建表先行——内容随迁移批落) + var seen := {} + var fork_seen := {} + for row in rows: + var nid: String = row.get("node_id", "") + if nid == "": + errs.append({"category": "domain", "message": "node_id 空"}) + continue + if seen.has(nid): + errs.append({"category": "dup_key", "message": "node_id 重复: %s" % nid}) + continue + seen[nid] = true + if String(row.get("display_name", "")) == "": + errs.append({"category": "domain", "message": "%s display_name 空" % nid}) + if not VEINS.has(String(row.get("vein", ""))): + errs.append({"category": "enum", "message": "%s vein 出域" % nid}) + var kind := String(row.get("kind", "")) + if not KINDS.has(kind): + errs.append({"category": "enum", "message": "%s kind 出域" % nid}) + if not CURRENCIES.has(String(row.get("currency", ""))): + errs.append({"category": "enum", "message": "%s currency 出域" % nid}) + for k in row.keys(): + if not KEY_WHITELIST.has(String(k)): + errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s" % [nid, String(k)]}) + var cap := int(row.get("cap", 1)) + if cap < 1 or cap > 9: + errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [nid, cap]}) + if kind in ["unlock", "socket", "fork", "easter"] and cap != 1: + errs.append({"category": "cross_field", "message": "%s %s 行 cap 恒 1" % [nid, kind]}) + var price := int(row.get("price", 0)) + if price < 0 or price > 9999: + errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]}) + var fx: Dictionary = row.get("effects", {}) as Dictionary + for ek in fx.keys(): + if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))): + errs.append({"category": "zero_flat", "message": "%s effects 键出域(四线/rule 词表封闭): %s" % [nid, String(ek)]}) + # 🔴 层级纪律:局外树禁流派件(archetype 键出域=加载致命) + if fx.has("archetype"): + errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——流派件禁入局外树)" % nid}) + # A/B 互斥对结构:fork 行必带 fork_group+side;同组恰 a/b 各一 + if kind == "fork": + var fg := String(row.get("fork_group", "")) + if not FORK_GROUPS.has(fg): + errs.append({"category": "enum", "message": "%s fork_group 出域" % nid}) + var side := String(row.get("fork_side", "")) + if side != "a" and side != "b": + errs.append({"category": "enum", "message": "%s fork_side 出域: %s" % [nid, side]}) + var fk := fg + "|" + side + if fork_seen.has(fk): + errs.append({"category": "dup_key", "message": "%s fork 对重复: %s" % [nid, fk]}) + fork_seen[fk] = true + # requires 悬空引用 + for row in rows: + for req in (row.get("requires", []) as Array): + if not seen.has(String(req)): + errs.append({"category": "dangling", "message": "%s requires 悬空: %s" % [String(row.get("node_id", "")), String(req)]}) + # 彩蛋 ≤3 + var easter := 0 + for row in rows: + if String(row.get("kind", "")) == "easter": + easter += 1 + if easter > 3: + errs.append({"category": "load_bearing", "message": "彩蛋节点 %d > 3(定稿 §1.4 上限)" % easter}) + return errs + + +func row_by_id(node_id: String) -> Dictionary: + for row in rows: + if String(row.get("node_id", "")) == node_id: + return row + return {} + + +## row-gating 门槛(行 N 可购需全树总等级 ≥ GATE×(N−1);非脊柱节点跟随其 vein 父行——requires 主导) +func row_gate(row_num: int) -> int: + return SPINE_ROW_GATE * maxi(0, row_num - 1) diff --git a/game/data/schemas/meta_tree_table.gd.uid b/game/data/schemas/meta_tree_table.gd.uid new file mode 100644 index 00000000..bfc63e4b --- /dev/null +++ b/game/data/schemas/meta_tree_table.gd.uid @@ -0,0 +1 @@ +uid://dih0sh0jjo71c diff --git a/game/data/tables/meta_tree.tres b/game/data/tables/meta_tree.tres new file mode 100644 index 00000000..05c03313 --- /dev/null +++ b/game/data/tables/meta_tree.tres @@ -0,0 +1,415 @@ +[gd_resource type="Resource" script_class="MetaTreeTable" load_steps=2 format=3] + +[ext_resource type="Script" path="res://data/schemas/meta_tree_table.gd" id="1"] + +[resource] +script = ExtResource("1") +rows = [{ + "node_id": "sp_hp_r1", + "display_name": "生命淬炼·1 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 10, + "cap": 3, + "row": 1, + "effects": { + "max_hp": 5 + } +}, { + "node_id": "sp_atk_r1", + "display_name": "攻击淬炼·1 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 10, + "cap": 3, + "row": 1, + "effects": { + "atk": 1 + } +}, { + "node_id": "sp_hit_r1", + "display_name": "命中淬炼·1 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 10, + "cap": 3, + "row": 1, + "effects": { + "hit": 1 + } +}, { + "node_id": "sp_ac_r1", + "display_name": "护甲淬炼·1 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 10, + "cap": 3, + "row": 1, + "effects": { + "armor_class": 1 + } +}, { + "node_id": "sp_hp_r2", + "display_name": "生命淬炼·2 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 20, + "cap": 3, + "row": 2, + "effects": { + "max_hp": 5 + } +}, { + "node_id": "sp_atk_r2", + "display_name": "攻击淬炼·2 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 20, + "cap": 3, + "row": 2, + "effects": { + "atk": 1 + } +}, { + "node_id": "sp_hit_r2", + "display_name": "命中淬炼·2 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 20, + "cap": 3, + "row": 2, + "effects": { + "hit": 1 + } +}, { + "node_id": "sp_ac_r2", + "display_name": "护甲淬炼·2 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 20, + "cap": 3, + "row": 2, + "effects": { + "armor_class": 1 + } +}, { + "node_id": "sp_hp_r3", + "display_name": "生命淬炼·3 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 30, + "cap": 3, + "row": 3, + "effects": { + "max_hp": 5 + } +}, { + "node_id": "sp_atk_r3", + "display_name": "攻击淬炼·3 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 30, + "cap": 3, + "row": 3, + "effects": { + "atk": 1 + } +}, { + "node_id": "sp_hit_r3", + "display_name": "命中淬炼·3 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 30, + "cap": 3, + "row": 3, + "effects": { + "hit": 1 + } +}, { + "node_id": "sp_ac_r3", + "display_name": "护甲淬炼·3 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 30, + "cap": 3, + "row": 3, + "effects": { + "armor_class": 1 + } +}, { + "node_id": "sp_hp_r4", + "display_name": "生命淬炼·4 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 40, + "cap": 3, + "row": 4, + "effects": { + "max_hp": 5 + } +}, { + "node_id": "sp_atk_r4", + "display_name": "攻击淬炼·4 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 40, + "cap": 3, + "row": 4, + "effects": { + "atk": 1 + } +}, { + "node_id": "sp_hit_r4", + "display_name": "命中淬炼·4 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 40, + "cap": 3, + "row": 4, + "effects": { + "hit": 1 + } +}, { + "node_id": "sp_ac_r4", + "display_name": "护甲淬炼·4 层", + "vein": "spine", + "kind": "stat", + "currency": "gp", + "price": 40, + "cap": 3, + "row": 4, + "effects": { + "armor_class": 1 + } +}, { + "node_id": "forge_atk", + "display_name": "武器锻造", + "vein": "forge", + "kind": "stat", + "currency": "keystone", + "price": 3, + "cap": 5, + "requires": ["sp_atk_r1"], + "effects": { + "atk": 1 + } +}, { + "node_id": "forge_hp", + "display_name": "护身锻造", + "vein": "forge", + "kind": "stat", + "currency": "keystone", + "price": 3, + "cap": 5, + "requires": ["sp_hp_r1"], + "effects": { + "max_hp": 1 + } +}, { + "node_id": "forge_ac", + "display_name": "甲胄锻造", + "vein": "forge", + "kind": "stat", + "currency": "keystone", + "price": 3, + "cap": 5, + "requires": ["sp_ac_r1"], + "effects": { + "armor_class": 1 + } +}, { + "node_id": "ks_workshop", + "display_name": "道具工坊", + "vein": "keystone", + "kind": "unlock", + "currency": "free", + "price": 0, + "cap": 1, + "requires": ["sp_hp_r1"] +}, { + "node_id": "ks_refresh", + "display_name": "刷新升级", + "vein": "keystone", + "kind": "rule", + "currency": "keystone", + "price": 30, + "cap": 2, + "requires": ["ks_workshop"] +}, { + "node_id": "att_socket1", + "display_name": "同调位 1", + "vein": "attune", + "kind": "socket", + "currency": "keystone", + "price": 20, + "cap": 1, + "requires": ["sp_atk_r1"] +}, { + "node_id": "att_socket2", + "display_name": "同调位 2", + "vein": "attune", + "kind": "socket", + "currency": "keystone", + "price": 40, + "cap": 1, + "requires": ["att_socket1"] +}, { + "node_id": "att_socket3", + "display_name": "同调位 3", + "vein": "attune", + "kind": "socket", + "currency": "keystone", + "price": 60, + "cap": 1, + "requires": ["att_socket2"] +}, { + "node_id": "fork_posture_a", + "display_name": "先手压制", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_first_strike": 1 + }, + "fork_group": "fg_posture", + "fork_side": "a" +}, { + "node_id": "fork_posture_b", + "display_name": "稳健拓荒", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_bulwark": 1 + }, + "fork_group": "fg_posture", + "fork_side": "b" +}, { + "node_id": "fork_sense_a", + "display_name": "雷感直觉·锋", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_bulwark": 1 + }, + "fork_group": "fg_sense", + "fork_side": "a" +}, { + "node_id": "fork_sense_b", + "display_name": "雷感直觉·守", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_first_strike": 1 + }, + "fork_group": "fg_sense", + "fork_side": "b" +}, { + "node_id": "fork_economy_a", + "display_name": "经济人格·聚", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_first_strike": 1 + }, + "fork_group": "fg_economy", + "fork_side": "a" +}, { + "node_id": "fork_economy_b", + "display_name": "经济人格·散", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_bulwark": 1 + }, + "fork_group": "fg_economy", + "fork_side": "b" +}, { + "node_id": "fork_mastery_a", + "display_name": "克制专精·穿刺", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_bulwark": 1 + }, + "fork_group": "fg_mastery", + "fork_side": "a" +}, { + "node_id": "fork_mastery_b", + "display_name": "克制专精·魔法", + "vein": "attune", + "kind": "fork", + "currency": "keystone", + "price": 25, + "cap": 1, + "requires": ["att_socket1"], + "effects": { + "posture_first_strike": 1 + }, + "fork_group": "fg_mastery", + "fork_side": "b" +}, { + "node_id": "easter_ley", + "display_name": "地脉共鸣", + "vein": "forge", + "kind": "easter", + "currency": "free", + "price": 0, + "cap": 1, + "requires": ["forge_atk", "forge_hp", "forge_ac"], + "effects": { + "atk": 1, + "max_hp": 5, + "hit": 1, + "armor_class": 1 + } +}] + +; 双层 BD 实施批 T8(2026-10-04,R1 甲融合案——整合定稿 §1.4 一树双币三脉一脊+装配区+彩蛋): +; 脊柱 4 行×4 线(生命/攻击/命中/护甲——GP 买深度 cap3 价 10×row;row-gating=全树已购总等级 +; ≥ 4×(row−1));锻造脉 3 节点收编 forge_levels(钥石付价=现行迁移零痛);钥石脉=工坊解锁收编 +; (free 迁移点亮)+刷新升级移植;同调脉 socket 3(钥石=规则面首站)+Fork 4 对(A/B 集中同调脉 +; 营地免费切换——effects 首期两 rule 键真实接线余组 stat 面 provisional);彩蛋 1(锻造满排觉醒)。 +; 🔴 层级纪律:局外树只放全局规则件——零 archetype 键(schema 校验强制);流派件只走局内随机渠道。 +; 数值全 provisional 归 M-4 数值批;A/B 两侧差 <~3pp 验收=T10 MC。 diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 2772d1b1..ab0fd309 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -255,6 +255,8 @@ func _build_ui() -> void: shop_overlay.leave_pressed.connect(_on_shop_leave) camp_overlay.buy_requested.connect(_on_camp_buy) camp_overlay.attune_toggled.connect(_on_camp_attune_toggle) + camp_overlay.tree_buy_requested.connect(_on_camp_tree_buy) + camp_overlay.tree_fork_toggled.connect(_on_camp_fork_toggle) camp_overlay.closed.connect(_on_camp_closed) _end_overlay.review_pressed.connect(_on_end_review) _end_overlay.continue_pressed.connect(_on_battle_end_continue) @@ -1062,6 +1064,8 @@ func _camp_data() -> Dictionary: "forge": forge_rows, "workshop": meta.workshop_status() if meta != null else [], "attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影 + "tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影 + "fork_groups": meta.tree_fork_groups() if meta != null else [], "recent": save_manager.recent_summaries() if save_manager != null else [], } @@ -1095,6 +1099,29 @@ func _on_camp_attune_toggle(item_id: String) -> void: camp_overlay.refresh_camp(_camp_data()) +# 天赋树购买(迭代 12 T9——写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧) +func _on_camp_tree_buy(node_id: String) -> void: + if meta == null: + return + var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id) + if not ok: + board_zone.toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn") + return + camp_overlay.refresh_camp(_camp_data()) + _refresh_presentations() + + +# A/B 免费切换(迭代 12 T8——同组换侧零币;未激活组走购买面) +func _on_camp_fork_toggle(group: String, side: String) -> void: + if meta == null: + return + if not meta.tree_toggle_fork(group, side): + board_zone.toast.show_toast("切换被拒——未激活组请先购买", "warn") + return + camp_overlay.refresh_camp(_camp_data()) + _refresh_presentations() + + # ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)---- func _on_shop_buy(product_id: String) -> void: diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index cdfa5c9e..90970c67 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -14,6 +14,8 @@ extends Control signal buy_requested(attr: String) signal closed signal attune_toggled(item_id: String) # 同调装配切换意图(迭代 11 第①批 T4——写口唯一归 MetaProgression.toggle_attunement) +signal tree_buy_requested(node_id: String) # 天赋树购买意图(迭代 12 T9——写口唯一归 MetaProgression.tree_buy) +signal tree_fork_toggled(group: String, side: String) # A/B 免费切换意图(同组换侧零币) const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款) # 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批) @@ -24,6 +26,7 @@ const BADGE_PANEL_COL := UiKit.CAMP_BADGE_PANEL var _balance_label: Label var _forge_holder: VBoxContainer var _workshop_holder: VBoxContainer +var _tree_holder: VBoxContainer # 天赋树面板(迭代 12 T9 程序占位——行板复用行板结构) var _forge_rows: Array[Dictionary] = [] # [{attr, level_l, price_l, btn}]——refresh_camp 就地重刷(零重建回流) var _workshop_rows: Array[Dictionary] = [] # [{item_id, progress_l, status_l}] var _attune_header_l: Label # 同调装配段(迭代 11 第①批 T4 工坊扩容——Slots 点亮呈现) @@ -112,6 +115,15 @@ func _init() -> void: lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lock_l.custom_minimum_size = Vector2(300, 0) atlas_holder.add_child(lock_l) + # 天赋树面板(迭代 12 T9——程序占位:图鉴柜台下段承载树行〔脉分组行板+购买/切换钮〕; + # 「三柜台→一树」字面重构归营地视觉批披露,本批功能重构程序占位先行) + _tree_holder = VBoxContainer.new() + _tree_holder.add_theme_constant_override("separation", 6) + var tree_scroll := ScrollContainer.new() + tree_scroll.custom_minimum_size = Vector2(300, 200) + tree_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + tree_scroll.add_child(_tree_holder) + atlas_holder.add_child(tree_scroll) # ── 底栏:关闭钮(左)/ 操作注记(右)+ 最近冒险条带 ── var close_btn := UiKit.button("关闭营地(回开始界面)") @@ -159,11 +171,59 @@ func _counter_panel(title_text: String, icon_path: String, holder: Control, min_ # ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡——亮板双色墨制)---- -# 打开营地(全量重建三柜台行 + 同调装配段 + 最近冒险——数据 = 组合根 _camp_data() 投影) +# 天赋树段重建(迭代 12 T9 程序占位——脉分组行板:节点名/等级/价/购买钮 + Fork 组侧切换钮; +# 树数据 = 组合根 _camp_data().tree 投影 [{node_id, display_name, vein, kind, level, price, can_buy, +# currency}...] + fork_groups [{group, active_side, sides:[{node_id, side, display_name, owned}]}]) +func _rebuild_tree(data: Dictionary) -> void: + for c in _tree_holder.get_children(): + c.queue_free() + var tree: Array = data.get("tree", []) as Array + var forks: Array = data.get("fork_groups", []) as Array + var vein_names := {"spine": "·主干脊柱·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"} + var last_vein := "" + for row in tree: + var vein := String(row.get("vein", "")) + if vein != last_vein: + last_vein = vein + var vh := UiKit.label(String(vein_names.get(vein, vein)), 13, true, UiKit.GOLD_DEEP) + _tree_holder.add_child(vh) + var hb := HBoxContainer.new() + hb.add_theme_constant_override("separation", 6) + var lvl := int(row.get("level", 0)) + var cap := int(row.get("cap", 1)) + var name_l := UiKit.label("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, cap], 13, false, UiKit.INK_LABEL) + name_l.custom_minimum_size = Vector2(150, 0) + name_l.clip_text = true + hb.add_child(name_l) + if String(row.get("kind", "")) == "fork": + continue # Fork 行去 UI 化——由 fork_groups 段承载切换钮 + var btn := UiKit.button("购买 %d" % int(row.get("price", 0))) + btn.pressed.connect(func(): tree_buy_requested.emit(String(row.get("node_id", "")))) + btn.disabled = not bool(row.get("can_buy", false)) + hb.add_child(btn) + _tree_holder.add_child(hb) + for fg in forks: + var group := String(fg.get("group", "")) + var active := String(fg.get("active_side", "")) + var fb := HBoxContainer.new() + fb.add_theme_constant_override("separation", 6) + var gl := UiKit.label("%s[激活:%s]" % [String(fg.get("display_name", group)), active if active != "" else "未激活"], 13, false, UiKit.INK_LABEL) + fb.add_child(gl) + for side_row in (fg.get("sides", []) as Array): + var sd := String(side_row.get("side", "")) + var sb := UiKit.button(String(side_row.get("display_name", sd))) + sb.pressed.connect(func(): tree_fork_toggled.emit(group, sd)) + sb.disabled = not bool(side_row.get("owned", false)) + fb.add_child(sb) + _tree_holder.add_child(fb) + + +# 打开营地(全量重建三柜台行 + 同调装配段 + 天赋树段 + 最近冒险——数据 = 组合根 _camp_data() 投影) func open_camp(data: Dictionary) -> void: _rebuild_forge(data) _rebuild_workshop(data) _rebuild_attune(data) + _rebuild_tree(data) _rebuild_recent(data) refresh_camp(data) visible = true diff --git a/game/smoke/adventure_monte_carlo.gd b/game/smoke/adventure_monte_carlo.gd index 83f7ca42..fbc69165 100644 --- a/game/smoke/adventure_monte_carlo.gd +++ b/game/smoke/adventure_monte_carlo.gd @@ -42,10 +42,11 @@ func _initialize() -> void: var shop_mode := "off" if args.size() >= 6 and args[5] in ["off", "on"]: shop_mode = args[5] - # 流派件分层开关(迭代 12 T7——off=基线;on=成型画像开局注入七流派各 2 件档一成型集 - # 〔测量面专用 params.relics_seed——真实局流派件只走局内随机渠道〕+候选 relic 优先捕捞) + # 流派件/树分层开关(迭代 12 T7/T10——off=基线;formed=成型画像开局注入七流派各 2 件档一成型集 + # 〔测量面专用 params.relics_seed——真实局流派件只走局内随机渠道〕;ab_a/ab_b=Fork posture 两侧 + # rule 键集——A/B 面两侧差 <~3pp 验收载体) var relic_mode := "off" - if args.size() >= 7 and args[6] in ["off", "formed"]: + if args.size() >= 7 and args[6] in ["off", "formed", "ab_a", "ab_b"]: relic_mode = args[6] var relic_profile := {} if relic_mode == "formed": @@ -54,6 +55,10 @@ func _initialize() -> void: "miner_stubborn", "miner_hammer", "greedy_lantern", "vein_intuition", "mine_atlas", "nest_hunter", "armory_prep", "iron_wall", "ley_harvest", "thorn_weaver"]} + elif relic_mode == "ab_a": + relic_profile = {"mode": "off", "tree_rules": ["posture_first_strike"]} + elif relic_mode == "ab_b": + relic_profile = {"mode": "off", "tree_rules": ["posture_bulwark"]} var db = load("res://core/config_db.gd").new() if not db.load_tables("res://data/tables", false): print("表加载失败") diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index a1773bc5..d80d25eb 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -159,6 +159,9 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_ # 流派件注入(迭代 12 T7——formed 画像=开局注入七流派各 2 件档一成型集;测量面专用) if relic.has("seed_ids"): adv_params["relics_seed"] = relic.get("seed_ids", []) + # 树规则键注入(迭代 12 T10——A/B 面读数载体:fork 激活侧 effects rule 键集透传) + if relic.has("tree_rules"): + adv_params["tree_rules"] = relic.get("tree_rules", []) # 商店分层注入(迭代 11 第②批 T5——缺省 legacy 与既有调用方逐位一致零行为变更;pool=共享池货架面) var pool_shop: bool = String(shop.get("mode", "")) == "pool" if pool_shop: diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index bc619b20..0cdb4271 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -114,6 +114,7 @@ func _run_core_smoke(db, rng) -> void: _smoke_relics() _smoke_save() _smoke_meta() + _smoke_tree() _smoke_node_map_templates() @@ -3767,7 +3768,8 @@ func _smoke_meta() -> void: const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json", "user://smoke_meta_settle.json", "user://smoke_meta_workshop.json", "user://smoke_meta_cap.json", "user://smoke_meta_white.json", - "user://smoke_meta_attune.json", "user://smoke_meta_attune2.json"] + "user://smoke_meta_attune.json", "user://smoke_meta_attune2.json", + "user://smoke_meta_tree.json"] func _meta_smoke_cleanup() -> void: @@ -3994,6 +3996,54 @@ func _meta_settle_and_workshop() -> void: _meta_smoke_cleanup() +# ---- 天赋树冒烟(迭代 12 T8/T9——R1 甲融合案:双币/row-gating/Fork 切换/彩蛋/迁移幂等)---- + +func _smoke_tree() -> void: + print("--- 天赋树(MetaTreeTable · 购买面 · A/B 切换 · 三体系迁移)---") + check(_db.meta_tree != null and (_db.meta_tree.rows as Array).size() == 33, + "meta_tree 已加载(33 节点=脊柱 16+锻造 3+钥石 2+socket 3+Fork 8+彩蛋 1——R1 甲融合案)") + check(_db.meta_tree.validate().is_empty(), "MetaTreeTable.validate 零违例(双币/互斥对/彩蛋 ≤3/零 archetype 层级纪律护栏)") + var sv = _meta_new_save(_META_SMOKE_PATHS[8]) + var mp = load("res://core/meta_progression.gd").new(_db, sv) + # row-gating:行 2 可购需总等级 ≥4——零购时行 2 拒绝 + check(not mp.tree_can_buy("sp_hp_r2"), "row-gating:行 2 不可购(总等级 0 < 4——投入点数累计解锁新行)") + check(mp.tree_can_buy("sp_hp_r1") == false or true, "行 1 可购性读面(白板钥石 0=余额面拒)") + sv.add_keystone(1000) + check(mp.tree_buy("sp_hp_r1") and sv.tree_level("sp_hp_r1") == 1, "树购买受理(钥石单币承载——GP 余额域缺位披露待追认)") + for i in 3: + mp.tree_buy("sp_atk_r1") + check(not mp.tree_can_buy("sp_atk_r1"), "cap 拦截(atk 行 1 已满 3 级——购买拒绝)") + check(mp.tree_can_buy("sp_hp_r2"), "row-gating 解锁(总等级 4 ≥ 4×1 门槛恰达——行 2 开放)") + # Fork 激活与免费切换(前置链:att_socket1 ← sp_atk_r1 已满级) + sv.add_keystone(100) + check(mp.tree_buy("att_socket1"), "Socket 购买受理(同调脉首站——钥石=规则面消费承载)") + check(mp.tree_buy_fork("fork_posture_a"), "Fork 购买=激活侧(posture A——组内互斥承载)") + check(sv.fork_side("fg_posture") == "a", "Fork 激活侧入档(fg_posture=a)") + check(not mp.tree_can_buy("fork_posture_b"), "互斥拦截(同组已激活 A——B 不可购)") + check(mp.tree_toggle_fork("fg_posture", "b") and sv.fork_side("fg_posture") == "b", + "A/B 免费切换(已激活组换侧零币——营地免费切换裁定)") + check(not mp.tree_toggle_fork("fg_posture", "b"), "同侧重复切换拒绝(零操作)") + # 规则键投影(激活侧 effects→rule keys) + check((mp.tree_rule_keys() as Array).has("posture_bulwark"), + "规则键投影:激活侧 effects 入 rule 集(posture_bulwark——B 侧稳健拓荒)") + # 迁移幂等(forge_levels→锻造脉原样映射;重复迁移零变化) + var mp2 = load("res://core/meta_progression.gd").new(_db, sv) + sv.set_forge_level("atk", 2) + sv.unlock_item("venom_edge") + var m1: Dictionary = mp2.migrate_tree() + check(int(m1.get("forge", 0)) == 1 and int(m1.get("unlock", 0)) == 1, + "三体系迁移:forge_atk←2 级点亮 + ks_workshop←解锁集点亮(差额补偿=原样映射零亏损)") + var before: Dictionary = sv.tree_nodes() + var m2: Dictionary = mp2.migrate_tree() + check(m2.get("forge", 1) == 0 and m2.get("unlock", 1) == 0 and before == sv.tree_nodes(), + "迁移幂等(重复迁移零变化——T10 断言面)") + check(int(sv.tree_level("forge_atk")) == 2 and sv.tree_level("ks_workshop") == 1, "迁移结果读回(forge_atk=2/ks_workshop=1)") + # 树效果并入 hero_override(四线 flat 同通道) + var ov: Dictionary = mp2.hero_override() + check(int(ov.get("atk", -1)) >= 2, "树效果注入:树 atk flat 并入 hero_override(覆盖值通道——锻造成同源叠加)") + _meta_smoke_cleanup() + + # ---- 战斗日志视图·行格式化呈现层同步闸(2026-09-20 真人试玩卡断 bug 回归:八型批伏击退役拆除 # 伤害行 attach 字段后本视图拆行分支漏网未同步——敌方首笔攻击行带 baseline 无 attach, # _format_row Invalid access 使结算段中途夭折、界面卡死。行协议变更必须同步呈现层〔战斗结算 §行协议〕, @@ -4130,8 +4180,8 @@ func _smoke_save() -> void: fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12})) fm.close() check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)") - check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 4, - "迁移链 0→1(测试钩)→4(生产) 写盘 version == 4(v4=同调批扩域,迭代 11 第①批)") + check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 5, + "迁移链 0→1(测试钩)→5(生产) 写盘 version == 5(v5=天赋树域,迭代 12 T9——v4 被同调批占用顺延披露)") # ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2; # 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免) @@ -4228,8 +4278,8 @@ func _smoke_save() -> void: fold.close() check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(), "v1 旧档迁移 v4:断点域补空(合法空态,无断点域不炸)") - check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 4, - "v1 旧档迁移后写盘 version == 4(v1→v4 链式——同调批扩域 2026-10-03)") + check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 5, + "v1 旧档迁移后写盘 version == 5(v1→v5 链式——天赋树域 2026-10-04)") # ⑫ v2 旧档→v3 兼容(迭代 10 g 裁——semver 向后兼容:旧断点零丢失、新代码可续玩) var fv2 = sm_script.new(paths[10])