Files
gplan-2/game/data/schemas/meta_tree_table.gd
T

125 lines
6.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 天赋树表 schema:逐 node_id 行(脊柱/锻造/钥石/同调四脉)的类型化读取(空表合法态;数值 provisional)。
## 🔴 层级纪律:局外树只放全局规则件——effects 禁流派标签(archetype 键出域=加载致命);
## 规则改写型特性只走局内随机渠道,永不入树。
class_name MetaTreeTable
extends Resource
@export var rows: Array[Variant] = [] # Array[Dictionary] 键 node_id
const VEINS := ["spine", "forge", "keystone", "attune"]
const KINDS := ["stat", "rule", "unlock", "socket", "fork", "easter"]
const CURRENCIES := ["keystone", "free"] # 树全钥石单币(无 gp 枚举值,字段保留为呈现元数据)
const STAT_KEYS := ["atk", "max_hp", "hit", "armor_class"]
## 规则键封闭词表(零裸 flat 同族护栏——effects.rule 键出域=加载致命;六键=三组 Fork 双态真实接线,
## 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", "requires_level", "effects", "effects_a", "effects_b", "name_a", "name_b", "fork_group", "note"]
## 脊柱行门槛(一线一节点:节点第 N 级可购需全树总等级 ≥ GATE×(N−1)——「投入点数累计解锁深度」)
const SPINE_ROW_GATE := 4
## Fork 双态组词表(每组恰一节点——互斥由单态承载;营地零币切换承载面;
## 无 fg_mastery 克制专精组——类型表达归锻造工坊)
const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy"]
func validate() -> Array:
var errs: Array[Variant] = []
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[Variant, Variant] = 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[Variant, Variant] = 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)]})
# requires_level 等级阈值前置(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
for row in rows:
var nid2: String = String(row.get("node_id", ""))
var rl_v: Variant = row.get("requires_level", {})
if rl_v is Dictionary:
for req_id in (rl_v as Dictionary).keys():
if not seen.has(String(req_id)):
errs.append({"category": "dangling", "message": "%s requires_level 悬空: %s" % [nid2, String(req_id)]})
if int(rl_v[req_id]) < 1:
errs.append({"category": "domain", "message": "%s requires_level 阈值须 ≥1: %s=%d" % [nid2, String(req_id), int(rl_v[req_id])]})
elif row.has("requires_level"):
errs.append({"category": "domain", "message": "%s requires_level 须为 Dictionary {node_id: n}" % nid2})
# 彩蛋 ≤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);
## 门槛主语=节点等级轴)
func level_gate(level: int) -> int:
return SPINE_ROW_GATE * maxi(0, level - 1)