- 新增 design/gdd/systems/冒险循环/特性.md(概念唯一定义文档) 定义=一次冒险内角色自身得到的提升(不是持有物);作用范围、生命周期、 获取途径(战场奖励/商店·训练)、关联与边界五要素齐全;数值一律指路表不入正文 - 02-系统清单:冒险循环补行 + 命名对照表补 ADVENTURE_FEATURE - registry entities.yaml:新增 feature 概念条目(owner/referenced_by)+ adventure_table 字段名同步 - 数据层标识符 112 处换名(值零变更):BONUS_IDS→FEATURE_KINDS、bonus_token_count→feature_quota、 bonus_value→feature_value、token_*/bonus_*_flat→feature_quota_*/feature_value_* - 称谓统一 94 处:旧称「流派件/数值件/本关强化」→ 特性 + 形态修饰语(规则改写型/属性加成型) - 两张 schema 顶部加概念指路注释,代码可直接查到定义文档 - 06-待办清单:BD 分层条目现状改写 + 新增「命名统一后续」条目(relic 词族与类名对齐待实施) 未动:类名与文件名(Godot .uid/.tscn 引用需跑测试自证)、断点存档键(兼容)
119 lines
6.5 KiB
GDScript
119 lines
6.5 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 豁免
|
||
# 定义表——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)
|