T1 schema 换代:MetaTreeTable FORK_GROUPS 4→3(fg_mastery 退役)/RULE_KEYS 2→6(sense/economy 四键扩签)/ fork 单行双态(effects_a/b+name_a/b、每组恰一校验)/row 字段退役(行门槛主语=节点等级 level_gate)/ requires 只留点亮前置(R1 裁撤案执行);meta_tree.tres 33→12 行重排(脊柱2/锻造2含彩蛋/钥石2/同调6, att_socket1 改根节点无前置,easter_ley 摘 atk/armor_class 分量=契约9) T2 购买与彩蛋面:tree_can_buy easter 拒购+行门槛换轴;tree_buy_fork=解锁默认A;tree_toggle_fork 零币切态; tree_rule_keys 按激活态 effects_a/b 投影;_check_easter_egg 特化判定(两线拉满+socket全解锁);营地树行板双态钮 T3 六键接线:EVENTS+flag_missed/FACTS+battle_index(L2 扩签两处);rebuild 六键合成订阅 (posture 窗=battle_index&&round_index 双闸/sense 两组=counter 闸+记账双行 domain:adventure 限次2次 provisional); level_runner battle_index 注入(visited battle 型计数);_judge_flag 去重闸后 flag_missed 发射(契约12); 聚=_roll_gp_drop 入账前×1.5 provisional(escrow 不乘);散=_spend_discounted ×80%(current_price/pool_price 双计价点,刷新价不折=§11②定义边界);_start_current_level 限次按关清零(_reset_tree_limited_counters 可测口) T4 注入换代:hero_override=树淬炼flat+FORGE_ROUTE_BONUS预留0(命中不注入=契约6);Attunement.slots 判定源切 树点亮数(谓词表退役);tree_buy(ks_refresh) 同账路由 buy_attune_refresh(V15 双入口) T5 旧档废弃与删除:save_manager tree_struct=2 结构版本标记+_normalize 白名单重建+树域废弃重置(禁迁移折算); migrate_tree/buy_forge/forge_flat/forge_level/forge_price/meta_forge_row/forge_products 行族/ attunement_growth 表+访问器/生涯页三投影/career forge_total 全删;treasure_item_table UNLOCK_COUNTER_KEYS 内联; verifier_batch 花费策略换代树节点;MC slots 画像常量3 T6 营地统一:四柜台并排横滚(树淬炼主界面/锻造工坊钥匙门+零交互占位面板/秘宝匣钥匙门+内容迁入/图鉴常开); 暂停层锻造段连带删;camp data forge 投影删 存量红修复:_flush_pending_reveals 三处空 source_class 补 fx_reveal+类级豁免(a57489e9 落地件,冒烟51行) 测试随批:smoke 树段全重写(V1/V2/V3/V8/V9/V16)+_smoke_tree_rules V10 行为段(窗/限次/守侧五场景编排rig/聚散 同种子对拍)+单元 tree_rules_v11_test(六键合成行闭集校验)+run_all 22 套;ui_probe/ui_click_probe 换代树点击路径 轻闸全绿:冒烟886/0+run_all 22套+probe --click 全绿
133 lines
8.0 KiB
GDScript
133 lines
8.0 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 数据配置 MetaTreeTable schema(迭代 21 天赋树实装批 T1 换代——设计权威
|
||
# design/gdd/systems/跨局循环/天赋树.md v0.8,2026-10-11 三轮换代终态)。
|
||
# 结构:12 节点 = 脊柱 2(生命淬炼/命中淬炼——一线一节点 cap4,2026-10-11 用户裁:一个内容一个
|
||
# 节点、等级在节点内)+ 锻造脉 2(锻造工坊钥匙 free 无前置 + 彩蛋地脉共鸣——彩蛋含在此 2 内)
|
||
# + 钥石脉 2(秘宝匣/刷新升级)+ 同调脉 6(同调位 1/2/3 + Fork 3 双态节点)。
|
||
# 行字段:node_id / display_name / vein ∈ VEINS(spine=主干脊柱;forge=锻造脉;keystone=钥石脉内容
|
||
# 解锁;attune=同调脉)/ kind ∈ KINDS(stat=数值线/rule=全局规则/unlock=内容解锁/socket=同调位/
|
||
# fork=一节点双态开关/easter=彩蛋觉醒)/ currency ∈ {keystone, free}(树全钥石单币——2026-10-07
|
||
# 用户裁甲;free=工作间钥匙与彩蛋)/ price [0,9999] / cap [1,9](等级上限;unlock/socket/fork/
|
||
# easter 恒 1)/ requires Array[node_id](点亮前置——前置节点 tree_level>=1 即满足;
|
||
# 「等级阈值前置」新形态经 R1 裁撤〔2026-10-12 卡片 v1.7 默认案执行〕,schema 只保留点亮前置)/
|
||
# effects Dictionary(stat 线 flat:max_hp/hit;rule 键封闭 RULE_KEYS)。
|
||
# Fork 一节点双态(2026-10-11 用户裁,原 A/B 双行结构收编):fork_group ∈ FORK_GROUPS 每组恰一节点;
|
||
# 行持 effects_a/effects_b 双效果键(A/B 只放规则面——键 ⊆ RULE_KEYS)+ name_a/name_b 双态呈现名;
|
||
# 购买解锁默认 A 态、营地零币切换(tree_toggle_fork),效果按激活态(save.fork_sides)取值。
|
||
# 行门槛(一线一节点换代):脊柱节点第 N 级可购需全树已购总等级 ≥ SPINE_ROW_GATE×(N−1)(总等级口径
|
||
# =tree_nodes 全部节点等级之和,免费点亮与 Fork 解锁计入——2026-10-11 歧义扫描裁甲);
|
||
# row 字段随层行退役。
|
||
# 🔴 层级纪律(需求卡片红线 2):局外树只放全局规则件——effects 禁流派标签(archetype 键出域=加载
|
||
# 致命);规则改写型特性只走局内随机渠道,永不入树。
|
||
# 彩蛋 ≤3(schema 校验);觉醒判定=MetaProgression._check_easter_egg 特化(生命/命中两线拉满+同调位
|
||
# 全解锁),不依赖 requires 点亮形态。
|
||
# 数值全 provisional 归 M4 数值批(契约 7);两侧「不同强非强弱」MC 验差 <~3pp(V19)。
|
||
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 := ["keystone", "free"] # 树全钥石单币(2026-10-07 用户裁甲——原 gp 枚举值退役,字段保留为呈现元数据)
|
||
const STAT_KEYS := ["atk", "max_hp", "hit", "armor_class"]
|
||
## 规则键封闭词表(零裸 flat 同族护栏——effects.rule 键出域=加载致命;六键=三组 Fork 双态真实接线,
|
||
## 迭代 21 T1 扩签:posture 两键现役沿袭,sense/economy 四键随危险直觉/财富之道两组建效面新增)
|
||
const RULE_KEYS := ["posture_first_strike", "posture_bulwark", "sense_sharp", "sense_guard", "economy_hoard", "economy_spend"]
|
||
const KEY_WHITELIST := ["node_id", "display_name", "vein", "kind", "currency", "price", "cap",
|
||
"requires", "effects", "effects_a", "effects_b", "name_a", "name_b", "fork_group", "note"]
|
||
## 脊柱行门槛(一线一节点:节点第 N 级可购需全树总等级 ≥ GATE×(N−1)——「投入点数累计解锁深度」)
|
||
const SPINE_ROW_GATE := 4
|
||
|
||
## Fork 双态组词表(每组恰一节点——互斥由单态承载;营地零币切换承载面;
|
||
## fg_mastery 克制专精整组砍除——2026-10-11 裁,类型表达归锻造工坊)
|
||
const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy"]
|
||
|
||
|
||
func validate() -> Array:
|
||
var errs: Array = []
|
||
if rows.is_empty():
|
||
return errs # 空表合法态(建表先行——内容随换代批落)
|
||
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 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]})
|
||
# 🔴 层级纪律:局外树禁规则改写型特性(archetype 键出域=加载致命)
|
||
if fx.has("archetype"):
|
||
errs.append({"category": "load_bearing", "message": "%s effects 含 archetype(🔴 层级纪律——特性禁入局外树)" % nid})
|
||
# Fork 一节点双态结构:fork 行必带 fork_group(每组恰一节点);主 effects 须空,效果只放
|
||
# a/b 双键且键 ⊆ RULE_KEYS(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})
|
||
if fork_seen.has(fg):
|
||
errs.append({"category": "dup_key", "message": "%s fork 组重复(一组恰一节点): %s" % [nid, fg]})
|
||
fork_seen[fg] = true
|
||
if not fx.is_empty():
|
||
errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid})
|
||
for skey in ["effects_a", "effects_b"]:
|
||
var sd: Dictionary = row.get(skey, {}) as Dictionary
|
||
if sd.is_empty():
|
||
errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]})
|
||
for k in sd.keys():
|
||
if not RULE_KEYS.has(String(k)):
|
||
errs.append({"category": "zero_flat", "message": "%s %s 键出域(A/B 只放规则面): %s" % [nid, skey, String(k)]})
|
||
# 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(上限 3)" % 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 {}
|
||
|
||
|
||
## 行门槛(一线一节点:脊柱节点第 N 级可购需全树总等级 ≥ GATE×(N−1);原 row-gating 行号主语
|
||
## 随层行退役平移到节点等级轴)
|
||
func level_gate(level: int) -> int:
|
||
return SPINE_ROW_GATE * maxi(0, level - 1)
|