- meta_tree.tres: 脊柱 16 行 currency gp→keystone(价格 10/20/30/40 不动,provisional 归 M4);
表尾 T8 注释改钥石口径+裁甲销账行注
- meta_progression.gd tree_buy: 删 currency 分叉直走 spend_keystone(前后同消费口=零行为变更;
free 行 price=0 零额消费无副作用),T8 双币骨架披露注释销账
- meta_tree_table.gd: CURRENCIES 枚举 {gp,keystone,free}→{keystone,free}(currency 字段保留为呈现
元数据),头注补单币口径
- camp_overlay_view.gd: 树购买钮文案「购买 N」→「购买 N 钥石」(行板紧凑钮豁免)
- smoke_all.gd: 4670/4677 断言消息文字随批换代(逻辑零改)
- meta.tres: 表尾陈旧注释订正(「GP 商店同族…base 10」→同族指数值档位非货币+base_price 现值 20
系 2026-09-22 对齿批投值)
闸: 基线 834/0;改后 smoke 834/0+probe_gate --click 全绿+ui_layout/ui_tune 过;
run_all 17 套件全绿;MC 零跑(静态论证:改动前后同走 spend_keystone,currency 无残余消费方)
120 lines
6.6 KiB
GDScript
120 lines
6.6 KiB
GDScript
# 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 豁免
|
||
# 定义表——FeatTable 先例)。
|
||
# 行字段:node_id / display_name / vein ∈ VEINS(spine=主干脊柱 row-gating,钥石买深度〔2026-10-07
|
||
# 用户裁甲——树全钥石单币,原双币铁律骨架披露销账〕;forge=锻造脉
|
||
# 数值专精;keystone=钥石脉内容解锁;attune=同调脉槽位与规则)/ kind ∈ KINDS(stat=四线数值/
|
||
# rule=全局规则/unlock=内容解锁/socket=同调位/fork=A/B 互斥对/easter=彩蛋觉醒)/
|
||
# currency ∈ {keystone, free}(树全钥石单币——2026-10-07 用户裁甲;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 := ["keystone", "free"] # 树全钥石单币(2026-10-07 用户裁甲——原 gp 枚举值退役,字段保留为呈现元数据)
|
||
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)
|