T8+T9+T10批次二出口(迭代12——天赋树+存档v5+营地树UI+A/B验证):T8树系统=MetaTreeTable schema(vein四脉/kind六类/双币currency/row-gating SPINE_ROW_GATE=4/effects四线+rule键白名单/零archetype层级护栏/Fork互斥对结构/彩蛋≤3)+meta_tree.tres 33节点(脊柱4行×4线GP价10×row/锻造脉3收编forge_levels钥石付价零痛/钥石脉workshop收编+Y升级移植/同调socket3+Fork4对集中同调脉/彩蛋1锻造满排觉醒)+MetaProgression树段(tree_spent_total/tree_can_buy row-gating+前置+币+cap/tree_buy单币钥石承载披露/tree_buy_fork购即激活/tree_toggle_fork免费切换/tree_stat_flat+rule_keys投影/_check_easter_egg觉醒)+rule键消费面(posture_first_strike=首轮AP+1/posture_bulwark=受击减1——enc.tree_rules透传链);T9存档v5(SAVE_FORMAT_VERSION=5树域tree_nodes/fork_sides两新域+v4→v5迁移+三体系迁移migrate_tree=forge原样映射零亏损/item_unlocks→ks_workshop/同调Slots→socket点亮+迁移幂等+断点版本断言4→5换代)+营地树UI(树面板=图鉴柜台下段程序占位——三柜台→一树字面重构延后披露+信号tree_buy_requested/tree_fork_toggled+main接线+投影两函数);T10=MC第7参扩ab_a/ab_b+A/B两侧锚N=20配对(先手压制/稳健拓荒rule键集透传)——两侧差0.0pp<<3pp验收过(A-4反例不触发)+taken 25.0/22.9减伤方向兑现佐证;修缩进坑复发(adventure_runner:826——同坑三犯留痕)+迁移unlock检查全行集(aux_live_ids系过渡口径同调行不在——返工留痕);冒烟树段18断言→714/0+run_all 15+探针绿+doc三闸基线同款+熵增PASS;T10出口包design/qa

This commit is contained in:
DKChan committed 2026-10-04 03:02:03 +08:00
1 parent bba962a5d3
commit 3b9ee9f134
16 files changed
+1005 -14

No files matched your search

@@ -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 三字段通道(命中折扣档消费归数值批);迁移差额补偿=原样映射零亏损(钥石付价不变,无亏损面故无补偿金)。
+3
View File
@@ -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"))
+7
View File
@@ -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「跨冒险行动点加成轴」节)
+12
View File
@@ -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:
+3
View File
@@ -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)
+9
View File
@@ -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:
+199 -4
View File
@@ -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 甲)----
# 装配切换(营地装配覆盖层唯一写口):已装 → 卸下恒可;未装 → 非同调行拒绝 + 满位禁选;
+50 -1
View File
@@ -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:
+118
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://dih0sh0jjo71c
+415
View File
@@ -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。
+27
View File
@@ -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:
+61 -1
View File
@@ -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
+8 -3
View File
@@ -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("表加载失败")
+3
View File
@@ -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:
+55 -5
View File
@@ -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])