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

214 lines
10 KiB
GDScript
Raw Permalink 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"]
## 规则键参数域表(键集与取值域逐字=天赋树.md v0.12 规则键参数表——文档定域、schema 执行)
const RULE_KEY_PARAMS := {
"posture_first_strike": {"ap_bonus": [1, 4]},
"posture_bulwark": {"shield": [1, 9]},
"sense_sharp": {"halve_pct": [1, 99], "cap_per_level": [1, 9]},
"sense_guard": {"ap_refund": [1, 4], "cap_per_level": [1, 9]},
"economy_hoard": {"mult_pct": [101, 300]},
"economy_spend": {"discount_pct": [1, 99]},
}
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
errs.append_array(_validate_row_head(row, nid))
errs.append_array(_validate_row_numbers(row, nid))
errs.append_array(_validate_row_effects(row, nid))
errs.append_array(_validate_rule_key_params(row, nid))
errs.append_array(_validate_row_fork(row, nid, fork_seen))
errs.append_array(_validate_requires_dangling(rows, seen))
errs.append_array(_validate_requires_level(rows, seen))
# 彩蛋 ≤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
## 行头域:命名与枚举判据(display_name/vein/kind/currency/行键白名单)
func _validate_row_head(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
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)]})
return errs
## 数值域:cap 越域/kind×cap 交叉/price 越域
func _validate_row_numbers(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
var kind := String(row.get("kind", ""))
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]})
return errs
## 效果域:effects 键词表封闭与局外树层级纪律(archetype 禁入)
func _validate_row_effects(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
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})
return errs
## 规则键参数域校验:effects/effects_a/effects_b 中凡 RULE_KEYS 成员的值必须是 Dictionary、
## 键集恰等 RULE_KEY_PARAMS[键] 的键集(缺参数/多参数即拒)、每值 int 且在闭域内(零缺省——缺字段=加载期报错)
func _validate_rule_key_params(row: Dictionary, nid: String) -> Array[Variant]:
var errs: Array[Variant] = []
for fx_key in ["effects", "effects_a", "effects_b"]:
var fx: Dictionary[Variant, Variant] = row.get(fx_key, {}) as Dictionary
for rk in fx.keys():
if not RULE_KEYS.has(String(rk)):
continue
errs.append_array(_validate_one_rule_key_params(nid, fx_key, String(rk), fx[rk]))
return errs
## 单规则键参数字典校验(值须为 Dictionary、键集恰等参数域表、每值 int 且在闭域内)
func _validate_one_rule_key_params(nid: String, fx_key: String, rk: String, params_v: Variant) -> Array[Variant]:
var errs: Array[Variant] = []
if not (params_v is Dictionary):
errs.append({"category": "domain", "message": "%s %s.%s 缺参数(规则键值须为参数字典——零缺省)" % [nid, fx_key, rk]})
return errs
var params: Dictionary[Variant, Variant] = params_v
var expect: Dictionary[Variant, Variant] = RULE_KEY_PARAMS[rk]
for pk in params.keys():
if not expect.has(String(pk)):
errs.append({"category": "domain", "message": "%s %s.%s 多参数: %s(参数域表封闭)" % [nid, fx_key, rk, String(pk)]})
elif not (params[pk] is int):
errs.append({"category": "domain", "message": "%s %s.%s.%s 越域(须 int)" % [nid, fx_key, rk, String(pk)]})
else:
var dom: Array[Variant] = expect[String(pk)]
var v := int(params[pk])
if v < int(dom[0]) or v > int(dom[1]):
errs.append({"category": "domain", "message": "%s %s.%s.%s 越域 [%d,%d]: %d" % [nid, fx_key, rk, String(pk), int(dom[0]), int(dom[1]), v]})
for pk in expect.keys():
if not params.has(String(pk)):
errs.append({"category": "domain", "message": "%s %s.%s 缺参数: %s(参数域表封闭)" % [nid, fx_key, rk, String(pk)]})
return errs
## Fork 结构域:一节点双态结构判据(fork_group 词表/组唯一/主 effects 须空/a/b 双键规则面)
func _validate_row_fork(row: Dictionary, nid: String, fork_seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
var kind := String(row.get("kind", ""))
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
var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary
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)]})
return errs
## requires 悬空引用域
func _validate_requires_dangling(rows: Array, seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
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)]})
return errs
## requires_level 等级阈值前置域(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
func _validate_requires_level(rows: Array, seen: Dictionary) -> Array[Variant]:
var errs: Array[Variant] = []
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})
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)