卡①T1(核心数据面结构收敛批): 数据校验族大表组五文件validate拆私有域函数族——meta_tree 4域+两尾段(requires/requires_level)/treasure_item 行头+订阅+活行+N值+充能档位+积点闸+解锁互斥+效果数值+谓词九域(enemy skip语义经aux同款信号承载)/enemy 行头基面+数值+atk区间+池模板+攻防类型+行序违例+锚行尾段/reward 轴+aux投放(skip信号)+池标签+数值+区间+表闭合/forge 行头(skip)+数值+解锁通道+初始覆盖尾段;错误清单类别/文案/追加序逐字保持(契约1①),17键全清(gdstyle直扫5文件零warning),data_config 12套件+下游5套件全绿
This commit is contained in:
1 parent
7f49a8c27b
commit
c56b816fee
5 files changed
+621
-393
No files matched your search
@@ -44,98 +44,147 @@ func validate() -> Array:
|
||||
var anchor_ok := {}
|
||||
var boss_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var rk: String = String(row.get("row_kind", ""))
|
||||
var band: int = int(row.get("depth_band", -1))
|
||||
var eid: String = row.get("enemy_id", "")
|
||||
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or band < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
|
||||
if not ROW_KINDS.has(rk):
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]})
|
||||
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]})
|
||||
var hp: int = int(row.get("hp", 0))
|
||||
if hp < 1 or hp > 999:
|
||||
errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid})
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
if atk < 0 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
|
||||
# atk_min/atk_max(可省略配对):遭遇骰同流 spawn 期整战持久 roll;
|
||||
# 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
var has_amin: bool = row.has("atk_min")
|
||||
var has_amax: bool = row.has("atk_max")
|
||||
if has_amin != has_amax:
|
||||
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
|
||||
elif has_amin:
|
||||
var amin: int = int(row.get("atk_min", 0))
|
||||
var amax: int = int(row.get("atk_max", 0))
|
||||
if amin < 0 or amax > 999 or amin > amax:
|
||||
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
|
||||
if atk != int((amin + amax) / 2.0):
|
||||
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
|
||||
# 池模板(单表结构校验;跨表构成纪律在 ConfigDB):
|
||||
# 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
|
||||
# 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
|
||||
var pool: Array[Variant] = row.get("pool", [])
|
||||
if pool.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
|
||||
var pool_ids := {}
|
||||
for entry_v in pool:
|
||||
if not (entry_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
|
||||
continue
|
||||
var entry: Dictionary[Variant, Variant] = entry_v
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
|
||||
elif pool_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
|
||||
pool_ids[sid] = true
|
||||
var w: int = int(entry.get("weight", 0))
|
||||
if w <= 0 or w > 9999:
|
||||
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
|
||||
# armor_type(可省略——缺省 light 向后兼容)
|
||||
var at: String = String(row.get("armor_type", "light"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
|
||||
# attack_type(可省略,缺省 normal 向后兼容;写法沿用 armor_type 同款)
|
||||
var att: String = String(row.get("attack_type", "normal"))
|
||||
if not ATTACK_TYPES.has(att):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]})
|
||||
# armor_class(可省略——缺省 0 向后兼容;防守方掷点判定阈值基数)
|
||||
var ac: int = int(row.get("armor_class", 0))
|
||||
if ac < 0 or ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
|
||||
errs.append_array(_validate_row_base(row, seen))
|
||||
var eid2 := String(row.get("enemy_id", ""))
|
||||
errs.append_array(_validate_row_numbers(row, eid2))
|
||||
errs.append_array(_validate_row_atk_range(row, eid2))
|
||||
errs.append_array(_validate_row_pool(row, eid2))
|
||||
errs.append_array(_validate_row_types(row, eid2))
|
||||
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须
|
||||
# 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。
|
||||
# 同带键多敌种混编合法;同带键同敌种第二行仍由
|
||||
# dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。
|
||||
var rowset_key := "%d_%s_%d" % [L, rk, band]
|
||||
var rowset_key := "%d_%s_%d" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]
|
||||
if rowset_key != last_rowset_key:
|
||||
if closed_rowset.has(rowset_key):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, rk, band]})
|
||||
errs.append_array(_validate_rowset_closed(rowset_key, closed_rowset, row))
|
||||
closed_rowset[rowset_key] = true
|
||||
last_rowset_key = rowset_key
|
||||
if rk == "battle" and band == 0:
|
||||
anchor_ok[L] = true
|
||||
elif rk == "boss":
|
||||
boss_ok[L] = true
|
||||
if not anchor_ok.has(L):
|
||||
anchor_ok[L] = false
|
||||
var L2: int = int(row.get("level_index", 0))
|
||||
var rk2: String = String(row.get("row_kind", ""))
|
||||
if rk2 == "battle" and int(row.get("depth_band", -1)) == 0:
|
||||
anchor_ok[L2] = true
|
||||
elif rk2 == "boss":
|
||||
boss_ok[L2] = true
|
||||
if not anchor_ok.has(L2):
|
||||
anchor_ok[L2] = false
|
||||
errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok))
|
||||
return errs
|
||||
|
||||
|
||||
## 行头基面域:键提取/查重与登记/进度键越域/row_kind 枚举/enemy_id 非空/display_name
|
||||
func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var rk: String = String(row.get("row_kind", ""))
|
||||
var band: int = int(row.get("depth_band", -1))
|
||||
var eid: String = row.get("enemy_id", "")
|
||||
var key := "%d_%s_%d_%s" % [L, rk, band, eid]
|
||||
if seen.has(key):
|
||||
errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key})
|
||||
seen[key] = true
|
||||
if L < 1 or band < 0:
|
||||
errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]})
|
||||
if not ROW_KINDS.has(rk):
|
||||
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]})
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "enemy_id 空"})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
return errs
|
||||
|
||||
|
||||
## 数值域:unit_count(含可省略上限)/hp/atk 基值
|
||||
func _validate_row_numbers(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var uc: int = int(row.get("unit_count", 0))
|
||||
if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]})
|
||||
# unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值
|
||||
if row.has("unit_count_max"):
|
||||
var ucm: int = int(row.get("unit_count_max", uc))
|
||||
if ucm < uc or ucm > UNIT_COUNT_MAX:
|
||||
errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]})
|
||||
var hp: int = int(row.get("hp", 0))
|
||||
if hp < 1 or hp > 999:
|
||||
errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid})
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
if atk < 0 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid})
|
||||
return errs
|
||||
|
||||
|
||||
## atk 区间域(可省略配对):遭遇骰同流 spawn 期整战持久 roll;
|
||||
## 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk)
|
||||
func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var atk: int = int(row.get("atk", -1))
|
||||
var has_amin: bool = row.has("atk_min")
|
||||
var has_amax: bool = row.has("atk_max")
|
||||
if has_amin != has_amax:
|
||||
errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid})
|
||||
elif has_amin:
|
||||
var amin: int = int(row.get("atk_min", 0))
|
||||
var amax: int = int(row.get("atk_max", 0))
|
||||
if amin < 0 or amax > 999 or amin > amax:
|
||||
errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]})
|
||||
if atk != int((amin + amax) / 2.0):
|
||||
errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]})
|
||||
return errs
|
||||
|
||||
|
||||
## 池模板域(单表结构校验;跨表构成纪律在 ConfigDB):
|
||||
## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
|
||||
## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
|
||||
func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var pool: Array[Variant] = row.get("pool", [])
|
||||
if pool.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
|
||||
var pool_ids := {}
|
||||
for entry_v in pool:
|
||||
if not (entry_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
|
||||
continue
|
||||
var entry: Dictionary[Variant, Variant] = entry_v
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
|
||||
elif pool_ids.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
|
||||
pool_ids[sid] = true
|
||||
var w: int = int(entry.get("weight", 0))
|
||||
if w <= 0 or w > 9999:
|
||||
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
|
||||
return errs
|
||||
|
||||
|
||||
## 攻防类型域(均可省略——armor_type 缺省 light/attack_type 缺省 normal/armor_class 缺省 0 向后兼容)
|
||||
func _validate_row_types(row: Dictionary, eid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var at: String = String(row.get("armor_type", "light"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
|
||||
var att: String = String(row.get("attack_type", "normal"))
|
||||
if not ATTACK_TYPES.has(att):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]})
|
||||
# armor_class(防守方掷点判定阈值基数)
|
||||
var ac: int = int(row.get("armor_class", 0))
|
||||
if ac < 0 or ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
|
||||
return errs
|
||||
|
||||
|
||||
## 行序违例域:当前行集键曾出现且 ≠ 上一行行集键 = 同带键行被隔断(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)
|
||||
func _validate_rowset_closed(rowset_key: String, closed_rowset: Dictionary, row: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if closed_rowset.has(rowset_key):
|
||||
errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]})
|
||||
return errs
|
||||
|
||||
|
||||
## 锚行尾段:每 level 必有 (battle, band 0) 锚行与 Boss 行集
|
||||
func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 (battle, band 0) 锚行" % L})
|
||||
|
||||
@@ -45,66 +45,101 @@ func validate() -> Array:
|
||||
for row in routes: # 预扫:route_id→kind 全集(前置反查需前向引用——轻甲行在长枪行之后)
|
||||
kind_by_id[String(row.get("route_id", ""))] = String(row.get("kind", ""))
|
||||
for row in routes:
|
||||
var rid := String(row.get("route_id", ""))
|
||||
if not ROUTE_IDS.has(rid):
|
||||
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var kind := String(row.get("kind", ""))
|
||||
if not ["weapon", "armor"].has(kind):
|
||||
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
|
||||
var head_out := _validate_route_head(row, seen, type_seen)
|
||||
errs.append_array(head_out["errs"])
|
||||
if head_out["skip"]:
|
||||
continue
|
||||
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
|
||||
var axis := String(row.get("type_axis", ""))
|
||||
var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
|
||||
if not domain.has(axis):
|
||||
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
|
||||
var type_key := "%s|%s" % [kind, axis]
|
||||
if type_seen.has(type_key):
|
||||
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
|
||||
type_seen[type_key] = true
|
||||
# 等级/增量/价格/基础 AC 值域
|
||||
var cap_v: int = int(row.get("cap", -1))
|
||||
if cap_v < 1 or cap_v > 9:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
|
||||
var gain: int = int(row.get("per_level_gain", -1))
|
||||
if gain < 0 or gain > 9:
|
||||
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
|
||||
var base_ac: int = int(row.get("base_ac", -1))
|
||||
if kind == "armor":
|
||||
if base_ac < 0 or base_ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
|
||||
elif base_ac != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
|
||||
var base_price: int = int(row.get("base_price", -1))
|
||||
if base_price < 1 or base_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
|
||||
var ratio: int = int(row.get("price_ratio_pct", -1))
|
||||
if ratio < 100 or ratio > 500:
|
||||
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
|
||||
var unlock_price: int = int(row.get("unlock_price", -1))
|
||||
if unlock_price < 0 or unlock_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
|
||||
# 解锁通道(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置
|
||||
var initial: bool = bool(row.get("initial_owned", false))
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
if initial:
|
||||
if prereq != "":
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
|
||||
if unlock_price != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
|
||||
initial_ids[rid] = kind
|
||||
else:
|
||||
if unlock_price < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
|
||||
if not ROUTE_IDS.has(prereq):
|
||||
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
|
||||
elif prereq == rid:
|
||||
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
|
||||
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
|
||||
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
|
||||
# 初始持有恰两行且一武器一甲(长剑/轻甲)
|
||||
var rid := String(row.get("route_id", ""))
|
||||
errs.append_array(_validate_route_values(row, rid))
|
||||
errs.append_array(_validate_route_unlock(row, rid, kind_by_id, initial_ids))
|
||||
errs.append_array(_validate_initial_coverage(initial_ids))
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:route_id 词表与查重/kind 出域(skip=跳过行剩余判据)/类型轴域与每型恰一路线
|
||||
func _validate_route_head(row: Dictionary, seen: Dictionary, type_seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skip := false
|
||||
var rid := String(row.get("route_id", ""))
|
||||
if not ROUTE_IDS.has(rid):
|
||||
errs.append({"category": "enum", "message": "锻造路线 route_id 出域 %s: %s" % [ROUTE_IDS, rid]})
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "锻造路线 route_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var kind := String(row.get("kind", ""))
|
||||
if not ["weapon", "armor"].has(kind):
|
||||
errs.append({"category": "enum", "message": "%s kind 出域 {weapon, armor}: %s" % [rid, kind]})
|
||||
skip = true
|
||||
return {"errs": errs, "skip": skip}
|
||||
# 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一)
|
||||
var axis := String(row.get("type_axis", ""))
|
||||
var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES
|
||||
if not domain.has(axis):
|
||||
errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]})
|
||||
var type_key := "%s|%s" % [kind, axis]
|
||||
if type_seen.has(type_key):
|
||||
errs.append({"category": "dup_key", "message": "%s 类型轴重复(每型恰一路线): %s %s" % [kind, axis, rid]})
|
||||
type_seen[type_key] = true
|
||||
return {"errs": errs, "skip": skip}
|
||||
|
||||
|
||||
## 数值域:等级/增量/价格/基础 AC 值域(甲型基础 AC 只属护甲路线)
|
||||
func _validate_route_values(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var kind := String(row.get("kind", ""))
|
||||
var cap_v: int = int(row.get("cap", -1))
|
||||
if cap_v < 1 or cap_v > 9:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,9]: %d" % [rid, cap_v]})
|
||||
var gain: int = int(row.get("per_level_gain", -1))
|
||||
if gain < 0 or gain > 9:
|
||||
errs.append({"category": "domain", "message": "%s per_level_gain 越域 [0,9]: %d" % [rid, gain]})
|
||||
var base_ac: int = int(row.get("base_ac", -1))
|
||||
if kind == "armor":
|
||||
if base_ac < 0 or base_ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_ac 越域 [0,999]: %d" % [rid, base_ac]})
|
||||
elif base_ac != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 武器行 base_ac 须为 0(甲型基础 AC 只属护甲路线): %d" % [rid, base_ac]})
|
||||
var base_price: int = int(row.get("base_price", -1))
|
||||
if base_price < 1 or base_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s base_price 越域 [1,999]: %d" % [rid, base_price]})
|
||||
var ratio: int = int(row.get("price_ratio_pct", -1))
|
||||
if ratio < 100 or ratio > 500:
|
||||
errs.append({"category": "domain", "message": "%s price_ratio_pct 越域 [100,500]: %d" % [rid, ratio]})
|
||||
var unlock_price: int = int(row.get("unlock_price", -1))
|
||||
if unlock_price < 0 or unlock_price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_price 越域 [0,999]: %d" % [rid, unlock_price]})
|
||||
return errs
|
||||
|
||||
|
||||
## 解锁通道域(并联双通道):初始持有行无前置无解锁价;待解锁行须有一次性解锁价+合法前置(武器←武器/甲←甲)
|
||||
func _validate_route_unlock(row: Dictionary, rid: String, kind_by_id: Dictionary, initial_ids: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var kind := String(row.get("kind", ""))
|
||||
var unlock_price: int = int(row.get("unlock_price", -1))
|
||||
var initial: bool = bool(row.get("initial_owned", false))
|
||||
var prereq := String(row.get("prereq_route", ""))
|
||||
if initial:
|
||||
if prereq != "":
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行不得带前置: %s" % [rid, prereq]})
|
||||
if unlock_price != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 初始持有行解锁价须为 0: %d" % [rid, unlock_price]})
|
||||
initial_ids[rid] = kind
|
||||
else:
|
||||
if unlock_price < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s 待解锁行须有一次性解锁价(并联双通道②): %d" % [rid, unlock_price]})
|
||||
if not ROUTE_IDS.has(prereq):
|
||||
errs.append({"category": "enum", "message": "%s prereq_route 出域 %s: %s" % [rid, ROUTE_IDS, prereq]})
|
||||
elif prereq == rid:
|
||||
errs.append({"category": "cross_field", "message": "%s prereq_route 不得自指" % rid})
|
||||
elif kind_by_id.has(prereq) and String(kind_by_id[prereq]) != kind:
|
||||
errs.append({"category": "cross_field", "message": "%s 前置 %s 类型不符(武器←武器/甲←甲): %s vs %s" % [rid, prereq, kind, String(kind_by_id[prereq])]})
|
||||
return errs
|
||||
|
||||
|
||||
## 初始持有覆盖域(两尾段):初始持有恰两行且一武器一甲(长剑/轻甲)+
|
||||
## 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
|
||||
func _validate_initial_coverage(initial_ids: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var init_weapons := 0
|
||||
var init_armors := 0
|
||||
for k in initial_ids.keys():
|
||||
@@ -114,7 +149,6 @@ func validate() -> Array:
|
||||
init_armors += 1
|
||||
if init_weapons != 1 or init_armors != 1:
|
||||
errs.append({"category": "coverage", "message": "初始持有须恰一武器一甲(现 sword/light): 武器 %d / 甲 %d" % [init_weapons, init_armors]})
|
||||
# 默认轴=初始持有路线类型轴(Q1 裸体态语义:未锻造=0 加值且类型=普通/轻甲)
|
||||
if init_weapons == 1 and init_armors == 1:
|
||||
for row in routes:
|
||||
if not bool(row.get("initial_owned", false)):
|
||||
|
||||
@@ -39,57 +39,105 @@ func validate() -> Array:
|
||||
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]})
|
||||
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_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
|
||||
|
||||
|
||||
## 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
|
||||
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 悬空引用
|
||||
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)]})
|
||||
# requires_level 等级阈值前置(R1 机制位——键须为在册节点、阈值整数 ≥1;类型错即拒)
|
||||
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", {})
|
||||
@@ -101,13 +149,6 @@ func validate() -> Array:
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -30,84 +30,129 @@ func validate() -> Array:
|
||||
if seen.has(rid):
|
||||
errs.append({"category": "dup_key", "message": "reward_id 重复: %s" % rid})
|
||||
seen[rid] = true
|
||||
var ctype: String = row.get("cell_type", "")
|
||||
if not CELL_TYPES.has(ctype):
|
||||
errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]})
|
||||
var pol: String = row.get("polarity", "")
|
||||
if not POLARITIES.has(pol):
|
||||
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
|
||||
var atk_type: String = String(row.get("attack_type", ""))
|
||||
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
|
||||
# aux 投放行专属校验:极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
|
||||
# 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable)
|
||||
if ctype == TREASURE_ITEM_TYPE:
|
||||
# 负向行 = 危险格极性字典占位(雷安同池摇号下危险格可摇中 aux 身份——负路由零兑付兜底,
|
||||
# effect_id 空/power 0/weight 0 不入池)
|
||||
if pol == "negative":
|
||||
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
|
||||
continue
|
||||
if String(row.get("effect_id", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
|
||||
if int(row.get("power", 0)) != 0 or row.has("power_min"):
|
||||
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 TreasureItemTable)" % rid})
|
||||
var pool: String = row.get("pool_tag", "")
|
||||
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
|
||||
if not POOL_TAGS.has(pool) and not pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
|
||||
elif pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
var power: int = int(row.get("power", 0))
|
||||
var weight: int = int(row.get("weight", -1))
|
||||
var tier: int = int(row.get("tier", 0))
|
||||
if weight < 0 or weight > 999:
|
||||
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
|
||||
if tier < 1 or tier > 8:
|
||||
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
|
||||
var stacks: int = int(row.get("stacks", 1))
|
||||
if stacks < 1 or stacks > 16:
|
||||
errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]})
|
||||
# 跨字段:effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。
|
||||
# flat 行 power ≥ 1(直付/直兑行:治疗/群伤/直伤/行动点)——action 型 = 排队点数行(power = ±点数量级)。
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if power < 0 or power > 999:
|
||||
errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]})
|
||||
if eff == "" and power < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid})
|
||||
# 区间配对(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚
|
||||
var has_min: bool = row.has("power_min")
|
||||
var has_max: bool = row.has("power_max")
|
||||
if has_min != has_max:
|
||||
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
|
||||
elif has_min:
|
||||
var pmin: int = int(row.get("power_min", 0))
|
||||
var pmax: int = int(row.get("power_max", 0))
|
||||
if pmin < 1 or pmax > 999 or pmin > pmax:
|
||||
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
|
||||
if eff != "":
|
||||
errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid})
|
||||
if power != int((pmin + pmax) / 2.0):
|
||||
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
|
||||
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
|
||||
errs.append_array(_validate_row_axes(row, rid))
|
||||
var aux_out := _validate_row_aux(row, rid)
|
||||
errs.append_array(aux_out["errs"])
|
||||
if aux_out["skip"]:
|
||||
continue
|
||||
errs.append_array(_validate_row_pool_tag(row, rid))
|
||||
errs.append_array(_validate_row_values(row, rid))
|
||||
errs.append_array(_validate_row_power_range(row, rid))
|
||||
# 字典完整性 / 池护栏累计(cell 池行)
|
||||
if pool == "cell" and CELL_TYPES.has(ctype) and POLARITIES.has(pol):
|
||||
cell_dict["%s|%s" % [ctype, pol]] = true
|
||||
if pol == "positive" and weight > 0:
|
||||
if String(row.get("pool_tag", "")) == "cell" and CELL_TYPES.has(String(row.get("cell_type", ""))) and POLARITIES.has(String(row.get("polarity", ""))):
|
||||
cell_dict["%s|%s" % [String(row.get("cell_type", "")), String(row.get("polarity", ""))]] = true
|
||||
if String(row.get("polarity", "")) == "positive" and int(row.get("weight", -1)) > 0:
|
||||
cell_positive_live += 1
|
||||
if pool == "clear_default" and eff == "" and power >= 1:
|
||||
if String(row.get("pool_tag", "")) == "clear_default" and String(row.get("effect_id", "")) == "" and int(row.get("power", 0)) >= 1:
|
||||
clear_value_ok = true
|
||||
# ① 字典完整性:五型 × 正负两行齐备(缺任一即致命)
|
||||
errs.append_array(_validate_table_closure(cell_dict, cell_positive_live, clear_value_ok))
|
||||
return errs
|
||||
|
||||
|
||||
## 轴域:cell_type 五型封闭/polarity 两极/attack_type 可省略枚举
|
||||
func _validate_row_axes(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var ctype: String = row.get("cell_type", "")
|
||||
if not CELL_TYPES.has(ctype):
|
||||
errs.append({"category": "enum", "message": "%s cell_type 出域(五型封闭): %s" % [rid, ctype]})
|
||||
var pol: String = row.get("polarity", "")
|
||||
if not POLARITIES.has(pol):
|
||||
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
|
||||
var atk_type: String = String(row.get("attack_type", ""))
|
||||
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
|
||||
return errs
|
||||
|
||||
|
||||
## aux 投放行域(掉落投放行):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
|
||||
## 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable);
|
||||
## 负占位行返回 skip=true(跳过行剩余判据——原 continue 语义)
|
||||
func _validate_row_aux(row: Dictionary, rid: String) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var skip := false
|
||||
if String(row.get("cell_type", "")) == TREASURE_ITEM_TYPE:
|
||||
# 负向行 = 危险格极性字典占位(雷安同池摇号下危险格可摇中 aux 身份——负路由零兑付兜底,
|
||||
# effect_id 空/power 0/weight 0 不入池)
|
||||
if String(row.get("polarity", "")) == "negative":
|
||||
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
|
||||
skip = true
|
||||
return {"errs": errs, "skip": skip}
|
||||
if String(row.get("effect_id", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
|
||||
if int(row.get("power", 0)) != 0 or row.has("power_min"):
|
||||
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 TreasureItemTable)" % rid})
|
||||
return {"errs": errs, "skip": skip}
|
||||
|
||||
|
||||
## 池标签域:pool_tag 词表与 clear_<biome> 命名空间预留拒绝/display_name 非空
|
||||
func _validate_row_pool_tag(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var pool: String = row.get("pool_tag", "")
|
||||
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
|
||||
if not POOL_TAGS.has(pool) and not pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag 未知: %s" % [rid, pool]})
|
||||
elif pool_reserved:
|
||||
errs.append({"category": "enum", "message": "%s pool_tag clear_<biome> 命名空间预留拒绝: %s" % [rid, pool]})
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
return errs
|
||||
|
||||
|
||||
## 数值域:weight/tier/stacks/power 越域与 flat 行 power ≥ 1
|
||||
## (effect_id 非空 = percent 行(引用悬空校验归 ConfigDB 跨表通道);空 = flat 行。
|
||||
## flat 行 power ≥ 1(直付/直兑行:治疗/群伤/直伤/行动点)——action 型 = 排队点数行(power = ±点数量级))
|
||||
func _validate_row_values(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var power: int = int(row.get("power", 0))
|
||||
var weight: int = int(row.get("weight", -1))
|
||||
var tier: int = int(row.get("tier", 0))
|
||||
if weight < 0 or weight > 999:
|
||||
errs.append({"category": "domain", "message": "%s weight 越域 [0,999]" % rid})
|
||||
if tier < 1 or tier > 8:
|
||||
errs.append({"category": "domain", "message": "%s tier 越域 [1,8]" % rid})
|
||||
var stacks: int = int(row.get("stacks", 1))
|
||||
if stacks < 1 or stacks > 16:
|
||||
errs.append({"category": "domain", "message": "%s stacks 越域 [1,16]: %d" % [rid, stacks]})
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if power < 0 or power > 999:
|
||||
errs.append({"category": "domain", "message": "%s power 越域 [0,999]: %d" % [rid, power]})
|
||||
if eff == "" and power < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat 行 power ≥ 1" % rid})
|
||||
return errs
|
||||
|
||||
|
||||
## 区间配对域(缺省 = 定值行向后兼容):必成对 · percent 行禁止 · power = 中点锚
|
||||
func _validate_row_power_range(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var power: int = int(row.get("power", 0))
|
||||
var eff: String = row.get("effect_id", "")
|
||||
var has_min: bool = row.has("power_min")
|
||||
var has_max: bool = row.has("power_max")
|
||||
if has_min != has_max:
|
||||
errs.append({"category": "cross_field", "message": "%s power_min/power_max 必须成对出现" % rid})
|
||||
elif has_min:
|
||||
var pmin: int = int(row.get("power_min", 0))
|
||||
var pmax: int = int(row.get("power_max", 0))
|
||||
if pmin < 1 or pmax > 999 or pmin > pmax:
|
||||
errs.append({"category": "domain", "message": "%s power 域违例 [1,999] 且 min≤max: %d..%d" % [rid, pmin, pmax]})
|
||||
if eff != "":
|
||||
errs.append({"category": "cross_field", "message": "%s percent 行禁区间字段" % rid})
|
||||
if power != int((pmin + pmax) / 2.0):
|
||||
# 中点锚:power = 区间中点(行动经济 条款①期望当量锚——调区间必同步调 power)
|
||||
errs.append({"category": "cross_field", "message": "%s power(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [rid, power, (pmin + pmax) / 2]})
|
||||
return errs
|
||||
|
||||
|
||||
## 表闭合域(三尾段):①九型字典完整性(类型×极性齐备)②池空护栏 ③clear_default 恒正当量
|
||||
func _validate_table_closure(cell_dict: Dictionary, cell_positive_live: int, clear_value_ok: bool) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
for ctype in CELL_TYPE_KEYS:
|
||||
for pol in POLARITIES:
|
||||
if not cell_dict.has("%s|%s" % [ctype, pol]):
|
||||
errs.append({"category": "cross_field", "message": "九型字典缺行: %s %s——(类型, 极性) 齐备为加载前提" % [ctype, pol]})
|
||||
# ② 池空护栏:cell 池 ≥1 正向行 weight>0(摇号池非空)
|
||||
if cell_positive_live < 1:
|
||||
errs.append({"category": "cross_field", "message": "cell 池零活正向行(weight>0)——摇号池空"})
|
||||
# ③ clear_default 恒正当量(行动经济 条款④——清图奖励 > 0 的行级兑现口径)
|
||||
if not clear_value_ok:
|
||||
errs.append({"category": "cross_field", "message": "clear_default 池无正当量活行(清图奖励 > 0 违例)"})
|
||||
return errs
|
||||
|
||||
@@ -73,136 +73,195 @@ func validate() -> Array:
|
||||
errs.append({"category": "dup_key", "message": "item_id 重复: %s" % iid})
|
||||
continue
|
||||
seen[iid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
|
||||
var retired: bool = bool(row.get("retired", false))
|
||||
var deferred: bool = bool(row.get("deferred", false))
|
||||
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
|
||||
var tags: Array[Variant] = row.get("dimension_tags", [])
|
||||
if tags.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
|
||||
for tag in tags:
|
||||
if not DIMENSIONS.has(String(tag)):
|
||||
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
|
||||
var kinds: Array[Variant] = row.get("effect_kinds", [])
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if not TARGETS.has(target):
|
||||
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
|
||||
var phase: String = String(row.get("use_phase", "active_dig"))
|
||||
if not USE_PHASES.has(phase):
|
||||
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
|
||||
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
|
||||
for k in row.keys():
|
||||
if not KEY_WHITELIST.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
|
||||
# triggers 事件订阅行(被动件形态;与 effect_kinds 互斥校验
|
||||
# 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
|
||||
# (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
|
||||
if row.has("triggers"):
|
||||
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
|
||||
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
|
||||
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
|
||||
var form: String = String(row.get("form", ""))
|
||||
if not FORMS.has(form):
|
||||
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
|
||||
if retired or deferred:
|
||||
errs.append_array(_validate_row_head(row, iid))
|
||||
if bool(row.get("retired", false)) or bool(row.get("deferred", false)):
|
||||
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
|
||||
continue
|
||||
# ---- 活行校验 ----
|
||||
if kinds.is_empty() and not row.has("triggers"):
|
||||
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
|
||||
var kind_seen := {}
|
||||
for k in kinds:
|
||||
var kind: String = String(k)
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
|
||||
if kind_seen.has(kind):
|
||||
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
|
||||
kind_seen[kind] = true
|
||||
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
|
||||
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
|
||||
for rv in REVEAL_KINDS:
|
||||
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
|
||||
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
|
||||
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
|
||||
if target == "board_wide" and kind_seen.has("mine_reveal"):
|
||||
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
|
||||
# magnitude:MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
|
||||
var mag: int = int(row.get("magnitude", 0))
|
||||
if mag < 0 or mag > 999:
|
||||
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
|
||||
var mag_consuming := false
|
||||
for k in kind_seen.keys():
|
||||
if MAGNITUDE_KINDS.has(String(k)):
|
||||
mag_consuming = true
|
||||
if mag_consuming and mag < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
|
||||
if not mag_consuming and mag != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
|
||||
# ---- 同调行约束(form 非空——装配面;过渡行 form="" 不校验)----
|
||||
errs.append_array(_validate_row_active(row, iid, kind_seen))
|
||||
var form: String = String(row.get("form", ""))
|
||||
if form != "":
|
||||
if int(row.get("slot_cost", 1)) != 1:
|
||||
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
|
||||
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
|
||||
var tier: String = String(row.get("tier", ""))
|
||||
if not TIERS.has(tier):
|
||||
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
|
||||
var charges: int = int(row.get("charges_per_adventure", 0))
|
||||
if form == "passive" and charges != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges < 0:
|
||||
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges == 0:
|
||||
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
|
||||
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
|
||||
var has_gate := kind_seen.has("ap_charge_safe")
|
||||
if not has_gate and row.has("triggers"):
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
|
||||
has_gate = true
|
||||
break
|
||||
if not has_gate:
|
||||
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
|
||||
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
|
||||
var creset: String = String(row.get("charge_reset", "adventure"))
|
||||
if not CHARGE_RESETS.has(creset):
|
||||
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
|
||||
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
|
||||
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
|
||||
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
|
||||
var price: int = int(row.get("unlock_keystone_cost", 0))
|
||||
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
|
||||
if price != 0 and ureq_count > 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
|
||||
if price < 0 or price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
|
||||
var eff_rounds: int = int(row.get("effective_rounds", 0))
|
||||
if eff_rounds < 0 or eff_rounds > 999:
|
||||
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
|
||||
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
|
||||
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
|
||||
var charge_n: int = int(row.get("charge_n", 0))
|
||||
if charge_n < 0 or charge_n > 999:
|
||||
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
|
||||
if kind_seen.has("ap_charge_safe") and charge_n < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
|
||||
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
|
||||
# unlock_requires 谓词行:空 = 已解锁(首批全解锁合法态,底稿 §2.2);非空键 ∈ 计数键封闭枚举
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", [])
|
||||
var ukeys := {}
|
||||
for req in ureqs:
|
||||
var ukey: String = String(req.get("key", ""))
|
||||
if not UNLOCK_COUNTER_KEYS.has(ukey):
|
||||
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
|
||||
if ukeys.has(ukey):
|
||||
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
|
||||
ukeys[ukey] = true
|
||||
var uth: int = int(req.get("threshold", 0))
|
||||
if uth < 1 or uth > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
|
||||
errs.append_array(_validate_row_attunement(row, iid, kind_seen))
|
||||
errs.append_array(_validate_row_unlock_costs(row, iid))
|
||||
errs.append_array(_validate_row_effect_numbers(row, iid, kind_seen))
|
||||
errs.append_array(_validate_row_unlock_requires(row, iid))
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:display_name/维度标签/target/use_phase/行键白名单/triggers 订阅形态/form 词表
|
||||
func _validate_row_head(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
|
||||
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
|
||||
var tags: Array[Variant] = row.get("dimension_tags", [])
|
||||
if tags.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
|
||||
for tag in tags:
|
||||
if not DIMENSIONS.has(String(tag)):
|
||||
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if not TARGETS.has(target):
|
||||
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
|
||||
var phase: String = String(row.get("use_phase", "active_dig"))
|
||||
if not USE_PHASES.has(phase):
|
||||
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
|
||||
# 🔴 零裸 flat 结构护栏:键白名单封闭(同调批 T1——机制轴禁无条件属性加项)
|
||||
for k in row.keys():
|
||||
if not KEY_WHITELIST.has(String(k)):
|
||||
errs.append({"category": "zero_flat", "message": "%s 行键出白名单: %s(KEY_WHITELIST 外键禁入)" % [iid, String(k)]})
|
||||
errs.append_array(_validate_row_triggers(row, iid))
|
||||
var form: String = String(row.get("form", ""))
|
||||
if not FORMS.has(form):
|
||||
errs.append({"category": "enum", "message": "%s form 出域 %s: %s" % [iid, str(FORMS), form]})
|
||||
return errs
|
||||
|
||||
|
||||
## 订阅形态域:triggers 事件订阅行判据与行级触发词表校验(被动件形态;与 effect_kinds 互斥校验
|
||||
## 面=passive 行(单形态——效果行为只走订阅);active_charged 行两形合法并存
|
||||
## (effect_kinds=主动分发通道 + triggers=受理闸订阅,掘进蓄能先例))
|
||||
func _validate_row_triggers(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if row.has("triggers"):
|
||||
if (row.get("effect_kinds", []) as Array).size() > 0 and String(row.get("form", "")) != "active_charged" and not bool(row.get("retired", false)):
|
||||
errs.append({"category": "domain", "message": "%s triggers 与 effect_kinds 并存(换代单形态——被动件迁移期禁双挂)" % iid})
|
||||
EffectEngine.validate_trigger_row(iid, row, row.get("triggers", []) as Array, errs)
|
||||
return errs
|
||||
|
||||
|
||||
## 活行域:effect_kinds 非空与词表/真视窄域与 board_wide 承重护栏/magnitude 语义(kind_seen 引用填充——同调域消费)
|
||||
func _validate_row_active(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var kinds: Array[Variant] = row.get("effect_kinds", [])
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if kinds.is_empty() and not row.has("triggers"):
|
||||
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred,被动件应挂 triggers 订阅)" % iid})
|
||||
for k in kinds:
|
||||
var kind: String = String(k)
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
|
||||
if kind_seen.has(kind):
|
||||
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
|
||||
kind_seen[kind] = true
|
||||
# 承重护栏①真视窄域:真视类活行 target 必须 area_3x3 或 cell(一次最多 3×3,禁全图看雷;
|
||||
# reveal_tiles 与 cell 档纳入同护栏,线香单格=cell ⊂ 3×3 契约内)
|
||||
for rv in REVEAL_KINDS:
|
||||
if kind_seen.has(String(rv)) and not (target == "area_3x3" or target == "cell"):
|
||||
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:%s 行 target 必须 area_3x3 或 cell,实得 %s(秘宝 GDD 结构冻结)" % [iid, String(rv), target]})
|
||||
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
|
||||
if target == "board_wide" and kind_seen.has("mine_reveal"):
|
||||
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
|
||||
errs.append_array(_validate_row_magnitude(row, iid, kind_seen))
|
||||
return errs
|
||||
|
||||
|
||||
## N 值语义域:magnitude 越域与 MAGNITUDE_KINDS 行消费(≥1);其余活行恒 0(同调批 T1——消费面自 ap_grant 扩至 N 值 kind)
|
||||
func _validate_row_magnitude(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var mag: int = int(row.get("magnitude", 0))
|
||||
if mag < 0 or mag > 999:
|
||||
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
|
||||
var mag_consuming := false
|
||||
for k in kind_seen.keys():
|
||||
if MAGNITUDE_KINDS.has(String(k)):
|
||||
mag_consuming = true
|
||||
if mag_consuming and mag < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s N 值 kind 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
|
||||
if not mag_consuming and mag != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 N 值 kind 活行 magnitude 应 0(trigger 行 magnitude=value_from 取数源豁免),实得 %d" % [iid, mag]})
|
||||
return errs
|
||||
|
||||
|
||||
## 同调充能与档位域(form 非空——装配面;过渡行 form="" 不校验):槽位统一/档位必填/充能值域与积点制受理闸/充能重置周期
|
||||
func _validate_row_attunement(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if int(row.get("slot_cost", 1)) != 1:
|
||||
errs.append({"category": "load_bearing", "message": "%s 同调占位统一 1 槽(slot_cost 恒 1——底稿裁决 5,schema 强制)" % iid})
|
||||
# 档位必填(槽位档位约束数据面:件档≤格档判定的取数源)
|
||||
var tier: String = String(row.get("tier", ""))
|
||||
if not TIERS.has(tier):
|
||||
errs.append({"category": "domain", "message": "%s 同调行 tier 必填且 ∈ %s,实得 %s" % [iid, str(TIERS), tier]})
|
||||
var charges: int = int(row.get("charges_per_adventure", 0))
|
||||
var form: String = String(row.get("form", ""))
|
||||
if form == "passive" and charges != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s passive 行 charges_per_adventure 应 0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges < 0:
|
||||
errs.append({"category": "domain", "message": "%s active_charged 行 charges_per_adventure 越域 ≥0,实得 %d" % [iid, charges]})
|
||||
if form == "active_charged" and charges == 0:
|
||||
# 充能 0=积点制/滑耗制合法态(试炼镐转化获取,无固定充能上限):
|
||||
# 必须带使用闸——ap_charge_safe 滑耗(安全格积点)或 treasure_used+counter_spend 受理闸,二者缺一即孤儿行
|
||||
errs.append_array(_validate_row_charge_gate(row, iid, kind_seen))
|
||||
# 充能重置周期(adventure 缺省/battle 每战/level 每关;仅 active_charged 行可设)
|
||||
var creset: String = String(row.get("charge_reset", "adventure"))
|
||||
if not CHARGE_RESETS.has(creset):
|
||||
errs.append({"category": "enum", "message": "%s charge_reset 出域 %s: %s" % [iid, str(CHARGE_RESETS), creset]})
|
||||
if form != "active_charged" and row.has("charge_reset") and creset != "adventure":
|
||||
errs.append({"category": "cross_field", "message": "%s 非 active_charged 行 charge_reset 仅可缺省(adventure),实得 %s" % [iid, creset]})
|
||||
return errs
|
||||
|
||||
|
||||
## 积点制受理闸域:charges=0 的 active_charged 行必须带使用闸(ap_charge_safe 滑耗或 counter_spend 受理闸)
|
||||
func _validate_row_charge_gate(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var has_gate := kind_seen.has("ap_charge_safe")
|
||||
if not has_gate and row.has("triggers"):
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
if String((tr_v as Dictionary).get("do", {}).get("kind", "")) == "counter_spend":
|
||||
has_gate = true
|
||||
break
|
||||
if not has_gate:
|
||||
errs.append({"category": "cross_field", "message": "%s active_charged 行 charges=0(积点制)须带 ap_charge_safe 或 counter_spend 受理闸,实得二者皆无" % iid})
|
||||
return errs
|
||||
|
||||
|
||||
## 解锁互斥域(form 非空行,契约 6):钥石购买价与条件谓词互斥——每件单解锁方式,混合不做
|
||||
func _validate_row_unlock_costs(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
# 解锁混合制字段校验(契约 6:钥石购买价与条件谓词互斥——每件单解锁方式,混合不做)
|
||||
var price: int = int(row.get("unlock_keystone_cost", 0))
|
||||
var ureq_count: int = (row.get("unlock_requires", []) as Array).size()
|
||||
if price != 0 and ureq_count > 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 解锁方式互斥:unlock_keystone_cost 与 unlock_requires 不得并存(每件单解锁方式,契约 6)" % iid})
|
||||
if price < 0 or price > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_keystone_cost 越域 [0,999](0=非购买件缺省),实得 %d" % [iid, price]})
|
||||
return errs
|
||||
|
||||
|
||||
## 效果数值域(form 非空行):effective_rounds 与 charge_n 的值域及 kind 交叉
|
||||
func _validate_row_effect_numbers(row: Dictionary, iid: String, kind_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var eff_rounds: int = int(row.get("effective_rounds", 0))
|
||||
if eff_rounds < 0 or eff_rounds > 999:
|
||||
errs.append({"category": "domain", "message": "%s effective_rounds 越域 [0,999]: %d" % [iid, eff_rounds]})
|
||||
if kind_seen.has("ap_rounds_front") and eff_rounds < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_rounds_front 行 effective_rounds 应 ≥1(P4 甲表值化),实得 %d" % [iid, eff_rounds]})
|
||||
if not kind_seen.has("ap_rounds_front") and eff_rounds != 0 and not row.has("triggers"):
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_rounds_front 行 effective_rounds 应 0(trigger 行=when value_from 取数源豁免),实得 %d" % [iid, eff_rounds]})
|
||||
var charge_n: int = int(row.get("charge_n", 0))
|
||||
if charge_n < 0 or charge_n > 999:
|
||||
errs.append({"category": "domain", "message": "%s charge_n 越域 [0,999]: %d" % [iid, charge_n]})
|
||||
if kind_seen.has("ap_charge_safe") and charge_n < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_charge_safe 行 charge_n 应 ≥1(每次充能需 N 安全格),实得 %d" % [iid, charge_n]})
|
||||
if not kind_seen.has("ap_charge_safe") and charge_n != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]})
|
||||
return errs
|
||||
|
||||
|
||||
## 谓词行域:unlock_requires 计数键封闭枚举与阈值(空 = 已解锁——首批全解锁合法态,底稿 §2.2)
|
||||
func _validate_row_unlock_requires(row: Dictionary, iid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
var ureqs: Array[Variant] = row.get("unlock_requires", [])
|
||||
var ukeys := {}
|
||||
for req in ureqs:
|
||||
var ukey: String = String(req.get("key", ""))
|
||||
if not UNLOCK_COUNTER_KEYS.has(ukey):
|
||||
errs.append({"category": "enum", "message": "%s unlock_requires 计数键出域 %s: %s" % [iid, str(UNLOCK_COUNTER_KEYS), ukey]})
|
||||
if ukeys.has(ukey):
|
||||
errs.append({"category": "dup_key", "message": "%s unlock_requires 计数键重复: %s" % [iid, ukey]})
|
||||
ukeys[ukey] = true
|
||||
var uth: int = int(req.get("threshold", 0))
|
||||
if uth < 1 or uth > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_requires 阈值越域 [1,999]: %d" % [iid, uth]})
|
||||
return errs
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user