卡①T2(核心数据面结构收敛批): 校验族小表组+跨表校验器+解析器八文件拆分——board_gen 行/约束/收窄三域/level_gen_params 行头(skip)/形态/计数三域+层深尾段/meta_table 三行族域/skill_table 行头(skip)/公共/四类型域+表不变量/variation 行域+效果互锁+全局互锁/status_effect 行头+量纲域/cross_validator 七编号块域函数化(_cv_reward_dangling/_cv_reward_treasure/_cv_poolmap/_cv_source_labels/_cv_skill_pool/_cv_shelf_pool/_cv_counter_closure+decls/reads分层)+validate_skill_pool_links计数束化/resolver resolve_enemy_rowset三纯行扫描+_resolve_progress三段(坐标对束化);错误清单与告警序逐字,23键全清,data面13套件全绿
This commit is contained in:
1 parent
c56b816fee
commit
9646ce545d
8 files changed
+545
-321
No files matched your search
@@ -11,21 +11,43 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var st_tab: SettlementTable = loaded.get("settlement")
|
||||
if st == null or rt == null or st_tab == null:
|
||||
return # 单表校验已报错,不连锁
|
||||
# ①c F2 投放维度覆盖校验不启用:挖格不掉道具,秘宝
|
||||
# 唯一来路=三选一共享池〔节点/关卡完成〕;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
|
||||
# 静态校验。
|
||||
var effect_ids := _effect_ids_of(st)
|
||||
_cv_reward_dangling(rt, effect_ids, push_diag) # ① 增益词表悬空
|
||||
_cv_reward_treasure(loaded, rt, push_diag) # ①b aux 投放行双表
|
||||
_cv_poolmap(st_tab, effect_ids, push_diag) # ② 池映射悬空
|
||||
_cv_source_labels(st_tab, push_diag) # ③ 来源标签键集
|
||||
_cv_skill_pool(loaded, push_diag) # ⑤ 技能池引用完整性
|
||||
_cv_shelf_pool(loaded, push_diag) # ⑥ 商店货架 × 冒险共享池
|
||||
_cv_counter_closure(loaded, push_diag) # ⑦ 计数器引用闭合
|
||||
|
||||
|
||||
## 增益状态词表全集(effect_id → true)
|
||||
func _effect_ids_of(st: StatusEffectTable) -> Dictionary:
|
||||
var effect_ids := {}
|
||||
for row in st.rows:
|
||||
effect_ids[row.get("effect_id", "")] = true
|
||||
# ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
|
||||
# 例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
|
||||
# (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
|
||||
return effect_ids
|
||||
|
||||
|
||||
## ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。
|
||||
## 例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
|
||||
## (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
|
||||
func _cv_reward_dangling(rt: RewardTable, effect_ids: Dictionary, push_diag: Callable) -> void:
|
||||
for row in rt.rows:
|
||||
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
|
||||
continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_treasure),不入增益词表面
|
||||
var eff: String = row.get("effect_id", "")
|
||||
if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff):
|
||||
push_diag.call("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
|
||||
# ①b RewardTable aux 投放行双表校验(reward_dangling_treasure):aux 行 effect_id = item_id,
|
||||
# 须命中 TreasureItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
|
||||
# 后仍投放(连带错误),同拦。
|
||||
|
||||
|
||||
## ①b RewardTable aux 投放行双表校验(reward_dangling_treasure):aux 行 effect_id = item_id,
|
||||
## 须命中 TreasureItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
|
||||
## 后仍投放(连带错误),同拦。
|
||||
func _cv_reward_treasure(loaded: Dictionary, rt: RewardTable, push_diag: Callable) -> void:
|
||||
var treasure_live := {}
|
||||
var treasure_tab = loaded.get("treasure_items")
|
||||
if treasure_tab != null:
|
||||
@@ -39,27 +61,36 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
continue
|
||||
if not treasure_live.has(treasure_eff):
|
||||
push_diag.call("error", "reward_dangling_treasure", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), treasure_eff, "双表校验"])
|
||||
# ①c F2 投放维度覆盖校验不启用:挖格不掉道具,秘宝
|
||||
# 唯一来路=三选一共享池〔节点/关卡完成〕;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
|
||||
# 静态校验。
|
||||
# ② SettlementTable.effect_pool_map 悬空
|
||||
|
||||
|
||||
## ② SettlementTable.effect_pool_map 悬空
|
||||
func _cv_poolmap(st_tab: SettlementTable, effect_ids: Dictionary, push_diag: Callable) -> void:
|
||||
for eff in st_tab.core.effect_pool_map.keys():
|
||||
if not effect_ids.has(eff):
|
||||
push_diag.call("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
|
||||
# ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, seal_miss})
|
||||
|
||||
|
||||
## ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, seal_miss})
|
||||
func _cv_source_labels(st_tab: SettlementTable, push_diag: Callable) -> void:
|
||||
var required_sources := ["safe_cell", "seal_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "seal_miss"]
|
||||
for s in required_sources:
|
||||
if not st_tab.core.source_label_map.has(s):
|
||||
push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
|
||||
|
||||
## ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
func _cv_skill_pool(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var en: EnemyTable = loaded.get("enemy")
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")):
|
||||
push_diag.call("error", e.category, e.message)
|
||||
# ⑥ 商店货架 × 冒险共享池跨表校验(M-4——次数上限域):
|
||||
# pool_shelf kind 须命中 adventure.CURIO_KINDS 且为有限库存种(cap ∈ [1,99]——gp 无上限种/牌库外种
|
||||
# 入货架 = 库存账本无锚,加载致命;gp 袋负交易切边/规则改写型特性三轴切边由 ShopTable 词表封闭另拦)
|
||||
|
||||
|
||||
## ⑥ 商店货架 × 冒险共享池跨表校验(M-4——次数上限域):
|
||||
## pool_shelf kind 须命中 adventure.CURIO_KINDS 且为有限库存种(cap ∈ [1,99]——gp 无上限种/牌库外种
|
||||
## 入货架 = 库存账本无锚,加载致命;gp 袋负交易切边/规则改写型特性三轴切边由 ShopTable 词表封闭另拦)
|
||||
func _cv_shelf_pool(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var adv = loaded.get("adventure")
|
||||
var shp = loaded.get("shop")
|
||||
if adv != null and shp != null:
|
||||
@@ -78,34 +109,42 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
if quota < 1:
|
||||
push_diag.call("error", "shelf_zero_quota", "ShopTable.pool_shelf kind %s 令牌配额为 0(牌库空种入货架 = 库存无锚)" % kind)
|
||||
|
||||
# ⑦ 计数器引用闭合(卡② V9——读点 id 必命中同名写入行,否则加载致命):
|
||||
# 声明集 = feats + treasure 各行 triggers 内 kind=counter_add 的 do.counter_id;
|
||||
# 读点 = when fact=counter 的 id、do.value_counter、kind=counter_spend 的 counter_id。
|
||||
var feats_tab = loaded.get("feats")
|
||||
|
||||
## ⑦ 计数器引用闭合(卡② V9——读点 id 必命中同名写入行,否则加载致命):
|
||||
## 声明集 = feats + treasure 各行 triggers 内 kind=counter_add 的 do.counter_id;
|
||||
## 读点 = when fact=counter 的 id、do.value_counter、kind=counter_spend 的 counter_id。
|
||||
func _cv_counter_closure(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
var trig_rows: Array[Variant] = []
|
||||
var feats_tab = loaded.get("feats")
|
||||
if feats_tab != null:
|
||||
for r in feats_tab.rows:
|
||||
trig_rows.append(r)
|
||||
var treasure_tab = loaded.get("treasure_items")
|
||||
if treasure_tab != null:
|
||||
for r in treasure_tab.rows:
|
||||
trig_rows.append(r)
|
||||
var counter_decls := _counter_decls_of(trig_rows)
|
||||
_cv_counter_reads(trig_rows, counter_decls, push_diag)
|
||||
|
||||
|
||||
## 计数器声明集收集:任一 do.counter_id 行(counter_add 直写、forgive_seal_miss 经 _apply_counter_hit 写——皆写入行)
|
||||
func _counter_decls_of(trig_rows: Array) -> Dictionary:
|
||||
var counter_decls := {}
|
||||
for row in trig_rows:
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
var dd: Dictionary[Variant, Variant] = (tr_v as Dictionary).get("do", {}) as Dictionary[Variant, Variant]
|
||||
# 声明集=任一 do.counter_id 行(counter_add 直写、forgive_seal_miss 经 _apply_counter_hit 写——皆写入行)
|
||||
if dd.has("counter_id"):
|
||||
counter_decls[String(dd.counter_id)] = true
|
||||
return counter_decls
|
||||
|
||||
|
||||
## 计数器读点校验:when counter id/do.value_counter/counter_spend 的 counter_id 必命中声明集
|
||||
func _cv_counter_reads(trig_rows: Array, counter_decls: Dictionary, push_diag: Callable) -> void:
|
||||
for row in trig_rows:
|
||||
var rid := String(row.get("feat_id", row.get("item_id", "")))
|
||||
for tr_v in (row.get("triggers", []) as Array):
|
||||
var tr: Dictionary[Variant, Variant] = tr_v as Dictionary[Variant, Variant]
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary[Variant, Variant]
|
||||
if String(c.get("fact", "")) == "counter" and c.has("id"):
|
||||
var rd := String(c.id)
|
||||
if not counter_decls.has(rd):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s when counter 读点无同名写入行: %s(引用闭合)" % [rid, rd])
|
||||
_check_counter_when(rid, tr, counter_decls, push_diag)
|
||||
var do2: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary[Variant, Variant]
|
||||
if do2.has("value_counter") and not counter_decls.has(String(do2.value_counter)):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s value_counter 读点无同名写入行: %s" % [rid, String(do2.value_counter)])
|
||||
@@ -113,6 +152,16 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
|
||||
push_diag.call("error", "counter_dangling_ref", "%s counter_spend 无同名写入行: %s" % [rid, String(do2.counter_id)])
|
||||
|
||||
|
||||
## when counter 读点校验(fact=counter 条件的 id 必命中声明集)
|
||||
func _check_counter_when(rid: String, tr: Dictionary, counter_decls: Dictionary, push_diag: Callable) -> void:
|
||||
for c_v in (tr.get("when", []) as Array):
|
||||
var c: Dictionary[Variant, Variant] = c_v as Dictionary[Variant, Variant]
|
||||
if String(c.get("fact", "")) == "counter" and c.has("id"):
|
||||
var rd := String(c.id)
|
||||
if not counter_decls.has(rd):
|
||||
push_diag.call("error", "counter_dangling_ref", "%s when counter 读点无同名写入行: %s(引用闭合)" % [rid, rd])
|
||||
|
||||
|
||||
# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用):
|
||||
# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 +
|
||||
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
|
||||
@@ -141,33 +190,25 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
|
||||
errs.append_array(_pool_cast_side_errs(skill_table, status_table)) # 施放类 side 守卫(buff→敌方侧/debuff→主角侧,防反向挂接——全表恰一次)
|
||||
for row in enemy_table.rows:
|
||||
var eid: String = String(row.get("enemy_id", ""))
|
||||
var basic_n := 0
|
||||
var buff_n := 0
|
||||
var debuff_n := 0
|
||||
var uncond_special_n := 0
|
||||
var pool_n := {"basic": 0, "buff": 0, "debuff": 0, "uncond_special": 0}
|
||||
for entry in row.get("pool", []):
|
||||
var sid: String = String(entry.get("skill_id", ""))
|
||||
if not skill_types.has(sid):
|
||||
errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]})
|
||||
continue
|
||||
match skill_types[sid]:
|
||||
"basic":
|
||||
basic_n += 1
|
||||
"buff":
|
||||
buff_n += 1
|
||||
"debuff":
|
||||
debuff_n += 1
|
||||
if pool_n.has(skill_types[sid]):
|
||||
pool_n[skill_types[sid]] += 1
|
||||
# 无条件特殊技计数(普攻不计;trigger 缺省行计数——触发条件批 2026-10-26 池构成纪律)
|
||||
if skill_types[sid] != "basic" and not (skill_table.row_by_id(sid) if skill_table.has_method("row_by_id") else {}).is_empty():
|
||||
if skill_types[sid] != "basic":
|
||||
var srow_c: Dictionary[Variant, Variant] = skill_table.row_by_id(sid) if skill_table.has_method("row_by_id") else {}
|
||||
if not srow_c.has("trigger"):
|
||||
uncond_special_n += 1
|
||||
if basic_n != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]})
|
||||
if buff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]})
|
||||
if debuff_n > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]})
|
||||
if uncond_special_n < 1:
|
||||
if not srow_c.is_empty() and not srow_c.has("trigger"):
|
||||
pool_n["uncond_special"] += 1
|
||||
if pool_n["basic"] != 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, pool_n["basic"]]})
|
||||
if pool_n["buff"] > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, pool_n["buff"]]})
|
||||
if pool_n["debuff"] > 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, pool_n["debuff"]]})
|
||||
if pool_n["uncond_special"] < 1:
|
||||
errs.append({"category": "pool_composition", "message": "EnemyTable %s 池无条件特殊技行数 = 0——每池至少一行无条件特殊技纪律(触发条件批 2026-10-26)" % [eid]})
|
||||
return errs
|
||||
@@ -43,11 +43,7 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
return []
|
||||
var rows: Array[Dictionary] = []
|
||||
rows.assign(_enemy_rows_get.call())
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
var level_keys: Array[int] = _level_keys_of(rows)
|
||||
if level_keys.is_empty():
|
||||
_diag.call("fatal", "structural", "enemy 行集为空——锚行校验应已拦截")
|
||||
return []
|
||||
@@ -58,12 +54,7 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
_warn_resolve("enemy", "resolve_level_clamp", "enemy 请求 (%d,%d) clamp 解析(超出最大关卡取最大值)→ 关卡 %d" % [L, D, l_star], l_star, D)
|
||||
elif l_star != L:
|
||||
_warn_resolve("enemy", "resolve_level_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ 关卡 %d" % [L, D, l_star], l_star, D)
|
||||
var band_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind:
|
||||
var bv := int(row.get("depth_band", -1))
|
||||
if not band_keys.has(bv):
|
||||
band_keys.append(bv)
|
||||
var band_keys: Array[int] = _band_keys_of(rows, l_star, row_kind)
|
||||
if band_keys.is_empty():
|
||||
return []
|
||||
band_keys.sort()
|
||||
@@ -73,6 +64,32 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]
|
||||
_warn_resolve("enemy", "resolve_depth_clamp", "enemy 请求 (%d,%d) clamp 解析(超出顶带取顶带)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star)
|
||||
elif d_star != D:
|
||||
_warn_resolve("enemy", "resolve_depth_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star)
|
||||
return _band_rows(rows, l_star, row_kind, d_star)
|
||||
|
||||
|
||||
## 行集关卡键收集(纯行扫描——告警发射序不变)
|
||||
func _level_keys_of(rows: Array) -> Array[int]:
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
return level_keys
|
||||
|
||||
|
||||
## 带键收集(纯行扫描——l_star × row_kind 的 depth_band 去重集)
|
||||
func _band_keys_of(rows: Array, l_star: int, row_kind: String) -> Array[int]:
|
||||
var band_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind:
|
||||
var bv := int(row.get("depth_band", -1))
|
||||
if not band_keys.has(bv):
|
||||
band_keys.append(bv)
|
||||
return band_keys
|
||||
|
||||
|
||||
## 带行收集(纯行扫描——命中带全部行、按表文件行序)
|
||||
func _band_rows(rows: Array, l_star: int, row_kind: String, d_star: int) -> Array[Dictionary]:
|
||||
var all_out: Array[Dictionary] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind \
|
||||
@@ -108,11 +125,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant
|
||||
if L < 1 or D < 0:
|
||||
_diag.call("fatal", "request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D])
|
||||
return {}
|
||||
var level_keys: Array[int] = []
|
||||
for row in rows:
|
||||
var lv := int(row.get("level_index", 0))
|
||||
if not level_keys.has(lv):
|
||||
level_keys.append(lv)
|
||||
var level_keys: Array[int] = _level_keys_of(rows)
|
||||
if level_keys.is_empty():
|
||||
_diag.call("fatal", "structural", "%s 行集为空——锚行校验应已拦截" % table_id)
|
||||
return {}
|
||||
@@ -121,40 +134,59 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant
|
||||
var l_clamped := l_star < 0
|
||||
if l_clamped:
|
||||
l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(可取范围内向上取整,超出用最大值)
|
||||
var depth_keys: Array[int] = _depth_keys_of(rows, l_star)
|
||||
depth_keys.sort()
|
||||
var d_star := _ceil_key(depth_keys, D)
|
||||
var d_clamped := d_star < 0
|
||||
if d_clamped:
|
||||
d_star = int(depth_keys[-1])
|
||||
var missing := _missing_label(l_clamped, d_clamped, l_star != L, d_star != D)
|
||||
if missing != "":
|
||||
_emit_missing_warning(table_id, missing, [l_star, d_star], [L, D])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
## 层键收集(纯行扫描——l_star 行的 node_depth 去重集)
|
||||
func _depth_keys_of(rows: Array, l_star: int) -> Array[int]:
|
||||
var depth_keys: Array[int] = []
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star:
|
||||
var dv := int(row.get("node_depth", 0))
|
||||
if not depth_keys.has(dv):
|
||||
depth_keys.append(dv)
|
||||
depth_keys.sort()
|
||||
var d_star := _ceil_key(depth_keys, D)
|
||||
var d_clamped := d_star < 0
|
||||
if d_clamped:
|
||||
d_star = int(depth_keys[-1])
|
||||
var missing := ""
|
||||
return depth_keys
|
||||
|
||||
|
||||
## 解析偏移形态标签(clamp/ceil × level/depth/both 六形态——空串 = 精确命中;
|
||||
## l_ceil/d_ceil = 解析值 ≠ 请求值)
|
||||
func _missing_label(l_clamped: bool, d_clamped: bool, l_ceil: bool, d_ceil: bool) -> String:
|
||||
var label := ""
|
||||
if l_clamped and d_clamped:
|
||||
missing = "both_clamp"
|
||||
label = "both_clamp"
|
||||
elif l_clamped:
|
||||
missing = "level_clamp"
|
||||
label = "level_clamp"
|
||||
elif d_clamped:
|
||||
missing = "depth_clamp"
|
||||
elif l_star != L and d_star != D:
|
||||
missing = "both_ceil"
|
||||
elif l_star != L:
|
||||
missing = "level_ceil"
|
||||
elif d_star != D:
|
||||
missing = "depth_ceil"
|
||||
if missing != "":
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
_diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing])
|
||||
for row in rows:
|
||||
if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star:
|
||||
return row
|
||||
return {}
|
||||
label = "depth_clamp"
|
||||
elif l_ceil and d_ceil:
|
||||
label = "both_ceil"
|
||||
elif l_ceil:
|
||||
label = "level_ceil"
|
||||
elif d_ceil:
|
||||
label = "depth_ceil"
|
||||
return label
|
||||
|
||||
|
||||
## 偏移告警发射(四元组去重注册表语义逐字——R1 既有去重;
|
||||
## req/resolved = [L, D]/[l_star, d_star] 进度坐标对)
|
||||
func _emit_missing_warning(table_id: String, missing: String, resolved: Array, req: Array) -> void:
|
||||
var dedup_key := "%s|%s|%d|%d" % [table_id, missing, resolved[0], resolved[1]]
|
||||
if not _warn_dedup.has(dedup_key):
|
||||
_warn_dedup[dedup_key] = true
|
||||
var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)"
|
||||
_diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, req[0], req[1], verb, resolved[0], resolved[1], missing])
|
||||
|
||||
|
||||
# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal
|
||||
|
||||
@@ -19,63 +19,92 @@ func validate() -> Array:
|
||||
var levels := {}
|
||||
var anchor_ok := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var key := "%d_%d" % [L, D]
|
||||
if levels.has(key):
|
||||
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
|
||||
levels[key] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
|
||||
if D < 0:
|
||||
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
if w < 8 or w > 64 or h < 8 or h > 64:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
if d <= 0.0 or d > 0.5:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
|
||||
var t: float = float(row.get("inferability_target", 0.0))
|
||||
if t < TARGET_ABS_FLOOR or t > 1.0:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
|
||||
if not SOLVER_RULES.has(row.get("solver_rules", "")):
|
||||
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
|
||||
if row.get("solver_rules", "") != "single_subset":
|
||||
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
|
||||
var ms: int = int(row.get("min_safe_count", 0))
|
||||
if ms < 1 or ms > 999:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
# 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
|
||||
var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
if w * h - n_mines < ms:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
|
||||
# F2c 成本可行性
|
||||
if att * w * h > COST_BUDGET:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
|
||||
# 图类型词表 v1.0(dungeon=地牢基线 / underdark=幽暗地域;扩词走本表修订)
|
||||
var bt: String = str(row.get("board_type", ""))
|
||||
if not BOARD_TYPE_VOCABULARY.has(bt):
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
|
||||
# 初始展开收窄两参数(加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
|
||||
var sr: int = int(row.get("shave_ring_radius", 1))
|
||||
if sr < 0 or sr > 2:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
|
||||
var r0_cap: int = int(row.get("r0_max_cap", 0))
|
||||
if r0_cap > 0:
|
||||
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
|
||||
if r0_cap < BoardData.SPAWN_AREA_CELLS:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
|
||||
if r0_cap > (w * h) / 2:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
errs.append_array(_validate_row(row, levels, anchor_ok))
|
||||
errs.append_array(_validate_row_constraints(row))
|
||||
errs.append_array(_validate_shave_params(row))
|
||||
for L in anchor_ok.keys():
|
||||
if not anchor_ok[L]:
|
||||
errs.append({"category": "anchor", "message": "level %d 缺 depth-0 锚行(EC15)" % L})
|
||||
return errs
|
||||
|
||||
|
||||
## 行域:键唯一与进度/尺寸/密度/推理目标/solver 规则冻结(levels/anchor_ok 引用累计)
|
||||
func _validate_row(row: Dictionary, levels: Dictionary, anchor_ok: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var key := "%d_%d" % [L, D]
|
||||
if levels.has(key):
|
||||
errs.append({"category": "dup_key", "message": "BoardGen 键重复: %s" % key})
|
||||
levels[key] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
|
||||
if D < 0:
|
||||
errs.append({"category": "domain", "message": "node_depth 须 ≥0"})
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
if w < 8 or w > 64 or h < 8 or h > 64:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) 尺寸越域 [8,64]" % [L, D]})
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
if d <= 0.0 or d > 0.5:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) mine_density 越域 (0,0.5]" % [L, D]})
|
||||
var t: float = float(row.get("inferability_target", 0.0))
|
||||
if t < TARGET_ABS_FLOOR or t > 1.0:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) inferability_target 越域 [0.85,1](P1 绝对锚)" % [L, D]})
|
||||
if not SOLVER_RULES.has(row.get("solver_rules", "")):
|
||||
errs.append({"category": "enum", "message": "(%d,%d) solver_rules 出域" % [L, D]})
|
||||
if row.get("solver_rules", "") != "single_subset":
|
||||
errs.append({"category": "frozen", "message": "(%d,%d) solver_rules 冻结 single_subset" % [L, D]})
|
||||
anchor_ok[L] = anchor_ok.get(L, false) or D == 0
|
||||
return errs
|
||||
|
||||
|
||||
## 约束域:min_safe/生成尝试预算/布雷数下限与可行性/成本护栏/图类型词表
|
||||
func _validate_row_constraints(row: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
var d: float = float(row.get("mine_density", 0.0))
|
||||
var ms: int = int(row.get("min_safe_count", 0))
|
||||
if ms < 1 or ms > 999:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) min_safe_count 越域 [1,999]" % [L, D]})
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
# 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
|
||||
var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
if w * h - n_mines < ms:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) min_safe_feasible 违例" % [L, D]})
|
||||
# F2c 成本可行性
|
||||
if att * w * h > COST_BUDGET:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) cost_feasible 违例 attempts×W×H ≤ %d" % [L, D, COST_BUDGET]})
|
||||
# 图类型词表 v1.0(dungeon=地牢基线 / underdark=幽暗地域;扩词走本表修订)
|
||||
var bt: String = str(row.get("board_type", ""))
|
||||
if not BOARD_TYPE_VOCABULARY.has(bt):
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) board_type 越词表: %s" % [L, D, bt]})
|
||||
return errs
|
||||
|
||||
|
||||
## 初始展开收窄域(加载期校验违例致命;缺省 shave=1 现行投值 / cap=0 闸关防御口径)
|
||||
func _validate_shave_params(row: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var D: int = int(row.get("node_depth", -1))
|
||||
var w: int = int(row.get("board_width", 0))
|
||||
var h: int = int(row.get("board_height", 0))
|
||||
var sr: int = int(row.get("shave_ring_radius", 1))
|
||||
if sr < 0 or sr > 2:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
|
||||
var r0_cap: int = int(row.get("r0_max_cap", 0))
|
||||
if r0_cap > 0:
|
||||
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
|
||||
if r0_cap < BoardData.SPAWN_AREA_CELLS:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
|
||||
if r0_cap > (w * h) / 2:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
|
||||
return errs
|
||||
@@ -17,50 +17,13 @@ func validate() -> Array:
|
||||
var seen := {}
|
||||
var layers_by_level := {}
|
||||
for row in rows:
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var key := "level %d" % L
|
||||
if seen.has(L):
|
||||
errs.append({"category": "dup_key", "message": "LevelGenParams 键重复: %d" % L})
|
||||
seen[L] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "%s 关卡序号越域(≥1)" % key})
|
||||
var ks := (row as Dictionary).keys()
|
||||
ks.sort()
|
||||
var expect := ROW_KEYS.duplicate()
|
||||
expect.sort()
|
||||
if ks != expect:
|
||||
errs.append({"category": "structural", "message": "%s 字段键集非 ROW_KEYS 闭集" % key})
|
||||
var head := _validate_row_head(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
var layers: int = int(row.get("layers", 0))
|
||||
if layers < 6:
|
||||
errs.append({"category": "domain", "message": "%s 层深 %d 低于下限 6(层深 L1≥6 已锁)" % [key, layers]})
|
||||
layers_by_level[L] = layers
|
||||
var bd: float = float(row.get("branch_density", -1.0))
|
||||
if bd < 0.0 or bd > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
|
||||
var ratio_sum := 0.0
|
||||
for rk in RATIO_KEYS:
|
||||
var v: float = float(row.get(rk, -1.0))
|
||||
if v < 0.0 or v > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s %s=%f 越域 [0,1]" % [key, rk, v]})
|
||||
ratio_sum += v
|
||||
if not is_equal_approx(ratio_sum, 1.0):
|
||||
errs.append({"category": "domain", "message": "%s 配比四类合计 %f ≠ 1(已锁)" % [key, ratio_sum]})
|
||||
var entry_count: int = int(row.get("entry_count", 0))
|
||||
if entry_count < 1:
|
||||
errs.append({"category": "domain", "message": "%s 入口个数 %d 越域(≥1)" % [key, entry_count]})
|
||||
var exit_count: int = int(row.get("exit_count", 0))
|
||||
if exit_count < 1 or exit_count > 3:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d 越域 [1,3](已锁范围)" % [key, exit_count]})
|
||||
if L == 1 and exit_count != 1:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d ≠ 1(L1 出口恒 1 已锁)" % [key, exit_count]})
|
||||
var bf: int = int(row.get("battle_floor", -1))
|
||||
if bf < 0:
|
||||
errs.append({"category": "domain", "message": "%s 战斗保底 %d 越域(≥0)" % [key, bf]})
|
||||
var fmin: int = int(row.get("fork_ways_min", 0))
|
||||
var fmax: int = int(row.get("fork_ways_max", 0))
|
||||
if fmin != 2 or fmax != 3:
|
||||
errs.append({"category": "domain", "message": "%s 岔口去路数 (%d,%d) ≠ (2,3)(已锁)" % [key, fmin, fmax]})
|
||||
errs.append_array(_validate_row_shape(row, head["L"], head["key"], layers_by_level))
|
||||
errs.append_array(_validate_row_counts(row, head["L"], head["key"]))
|
||||
# 层深递进尾段(L1<L2<L3 且增幅变大——层深 L1≥6 递增增幅变大已锁)
|
||||
if layers_by_level.has(1) and layers_by_level.has(2) and layers_by_level.has(3):
|
||||
var d12: int = int(layers_by_level[2]) - int(layers_by_level[1])
|
||||
var d23: int = int(layers_by_level[3]) - int(layers_by_level[2])
|
||||
@@ -72,6 +35,68 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:level_index 查重/关卡序号越域/字段键集闭集(ok=跳过行剩余判据)
|
||||
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L: int = int(row.get("level_index", 0))
|
||||
var key := "level %d" % L
|
||||
if seen.has(L):
|
||||
errs.append({"category": "dup_key", "message": "LevelGenParams 键重复: %d" % L})
|
||||
seen[L] = true
|
||||
if L < 1:
|
||||
errs.append({"category": "domain", "message": "%s 关卡序号越域(≥1)" % key})
|
||||
var ks := (row as Dictionary).keys()
|
||||
ks.sort()
|
||||
var expect := ROW_KEYS.duplicate()
|
||||
expect.sort()
|
||||
if ks != expect:
|
||||
errs.append({"category": "structural", "message": "%s 字段键集非 ROW_KEYS 闭集" % key})
|
||||
return {"errs": errs, "L": L, "key": key, "ok": false}
|
||||
return {"errs": errs, "L": L, "key": key, "ok": true}
|
||||
|
||||
|
||||
## 行形态域:层深下限(layers_by_level 引用登记——层深递进尾段取数)/分支密度/配比四类合计 1
|
||||
func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var layers: int = int(row.get("layers", 0))
|
||||
if layers < 6:
|
||||
errs.append({"category": "domain", "message": "%s 层深 %d 低于下限 6(层深 L1≥6 已锁)" % [key, layers]})
|
||||
layers_by_level[L] = layers
|
||||
var bd: float = float(row.get("branch_density", -1.0))
|
||||
if bd < 0.0 or bd > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
|
||||
var ratio_sum := 0.0
|
||||
for rk in RATIO_KEYS:
|
||||
var v: float = float(row.get(rk, -1.0))
|
||||
if v < 0.0 or v > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s %s=%f 越域 [0,1]" % [key, rk, v]})
|
||||
ratio_sum += v
|
||||
if not is_equal_approx(ratio_sum, 1.0):
|
||||
errs.append({"category": "domain", "message": "%s 配比四类合计 %f ≠ 1(已锁)" % [key, ratio_sum]})
|
||||
return errs
|
||||
|
||||
|
||||
## 行计数域:入口/出口(L1 恒 1)/战斗保底/岔口去路数锁值
|
||||
func _validate_row_counts(row: Dictionary, L: int, key: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var entry_count: int = int(row.get("entry_count", 0))
|
||||
if entry_count < 1:
|
||||
errs.append({"category": "domain", "message": "%s 入口个数 %d 越域(≥1)" % [key, entry_count]})
|
||||
var exit_count: int = int(row.get("exit_count", 0))
|
||||
if exit_count < 1 or exit_count > 3:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d 越域 [1,3](已锁范围)" % [key, exit_count]})
|
||||
if L == 1 and exit_count != 1:
|
||||
errs.append({"category": "domain", "message": "%s 出口个数 %d ≠ 1(L1 出口恒 1 已锁)" % [key, exit_count]})
|
||||
var bf: int = int(row.get("battle_floor", -1))
|
||||
if bf < 0:
|
||||
errs.append({"category": "domain", "message": "%s 战斗保底 %d 越域(≥0)" % [key, bf]})
|
||||
var fmin: int = int(row.get("fork_ways_min", 0))
|
||||
var fmax: int = int(row.get("fork_ways_max", 0))
|
||||
if fmin != 2 or fmax != 3:
|
||||
errs.append({"category": "domain", "message": "%s 岔口去路数 (%d,%d) ≠ (2,3)(已锁)" % [key, fmin, fmax]})
|
||||
return errs
|
||||
|
||||
|
||||
# 参数档单点读取(键 = level_index,缺行返回空字典——沿 level_row() 体例)
|
||||
func row_by_level_index(level_index: int) -> Dictionary:
|
||||
for row in rows:
|
||||
|
||||
@@ -18,7 +18,15 @@ func validate() -> Array:
|
||||
for family in ["boss_keystone", "conversion", "attune_refresh_upgrade"]:
|
||||
if get(family).is_empty():
|
||||
errs.append({"category": "empty_table", "message": "MetaTable 行族为空: %s" % family})
|
||||
# boss_keystone:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
|
||||
errs.append_array(_validate_boss_keystone())
|
||||
errs.append_array(_validate_conversion())
|
||||
errs.append_array(_validate_attune_refresh())
|
||||
return errs
|
||||
|
||||
|
||||
## boss_keystone 行族域:level_index 唯一 + 连续从 1 + 两档域 + first ≥ repeat(首杀全量、重复小额恒定)
|
||||
func _validate_boss_keystone() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen_li := {}
|
||||
for row in boss_keystone:
|
||||
var li: int = int(row.get("level_index", 0))
|
||||
@@ -36,9 +44,13 @@ func validate() -> Array:
|
||||
if li_at != i + 1:
|
||||
errs.append({"category": "structural", "message": "boss_keystone level_index 不连续(须从 1 连续递增): 位 %d = %d" % [i + 1, li_at]})
|
||||
break
|
||||
# 无 workshop_items 行族(解锁混合制——条件谓词挂 treasure_items 行内;
|
||||
# 本表不校验秘宝解锁,单点=TreasureItemTable.validate)
|
||||
# conversion:attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
|
||||
return errs
|
||||
|
||||
|
||||
## conversion 行族域(属性换算):attr 词表与 forge 一致 + 唯一 + 覆盖恰三 + 平移量域 [0,100]
|
||||
## (无 workshop_items 行族——解锁混合制:条件谓词挂 treasure_items 行内;本表不校验秘宝解锁,单点=TreasureItemTable.validate)
|
||||
func _validate_conversion() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen_conv := {}
|
||||
for row in conversion:
|
||||
var cattr: String = row.get("attr", "")
|
||||
@@ -54,7 +66,12 @@ func validate() -> Array:
|
||||
for conv_key in FORGE_ATTRS:
|
||||
if not seen_conv.has(conv_key):
|
||||
errs.append({"category": "coverage", "message": "conversion 缺属性行: %s(三行与 forge 词表一致)" % conv_key})
|
||||
# attune_refresh_upgrade:level 唯一 + 连续从 1 + charges/cost 域
|
||||
return errs
|
||||
|
||||
|
||||
## attune_refresh_upgrade 行族域(Y 刷新升级):level 唯一 + 连续从 1 + charges/cost 域
|
||||
func _validate_attune_refresh() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen_al := {}
|
||||
for row in attune_refresh_upgrade:
|
||||
var al: int = int(row.get("level", -1))
|
||||
|
||||
@@ -31,73 +31,108 @@ func validate() -> Array:
|
||||
var seen := {}
|
||||
var has_basic := false
|
||||
for row in rows:
|
||||
var sid: String = row.get("skill_id", "")
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
|
||||
var head := _validate_row_head(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
if seen.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
|
||||
seen[sid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
|
||||
var type: String = row.get("type", "")
|
||||
if not SKILL_TYPES.has(type):
|
||||
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
|
||||
continue
|
||||
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
|
||||
if type == "basic" and row.has("trigger"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
|
||||
# 冷却域(全类型):施放后连续 N 拍不可摇中
|
||||
var cd: int = int(row.get("cooldown_beats", -1))
|
||||
if cd < 0 or cd > 99:
|
||||
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
|
||||
# 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2)
|
||||
var sid: String = head["sid"]
|
||||
var type: String = head["type"]
|
||||
_validate_common_fields(sid, row, type, errs)
|
||||
if type == "basic":
|
||||
has_basic = true
|
||||
_validate_basic_row(sid, row, cd, errs)
|
||||
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
|
||||
# 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
|
||||
_validate_basic_row(sid, row, int(row.get("cooldown_beats", -1)), errs)
|
||||
if type == "damage":
|
||||
if not row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
|
||||
else:
|
||||
var m: float = float(row.damage_mult)
|
||||
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
|
||||
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
|
||||
if row.has("rider_mult"):
|
||||
var rm: float = float(row.rider_mult)
|
||||
if rm <= 0.0 or rm > RIDER_MULT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
|
||||
# 增益技:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
|
||||
_validate_damage_row(sid, row, errs)
|
||||
if type == "buff":
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
|
||||
var bd: int = int(row.get("buff_duration", 0))
|
||||
if bd < 1 or bd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
|
||||
# 妨害技:0 伤害施放动作,对主角施加负面状态——
|
||||
# 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
|
||||
_validate_buff_row(sid, row, errs)
|
||||
if type == "debuff":
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
|
||||
var dbd: int = int(row.get("buff_duration", 0))
|
||||
if dbd < 1 or dbd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
|
||||
_validate_debuff_row(sid, row, errs)
|
||||
_validate_table_invariants(has_basic, errs)
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:skill_id 非空与查重/display_name/type 枚举(ok=跳过行剩余判据)
|
||||
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var sid: String = row.get("skill_id", "")
|
||||
if sid == "":
|
||||
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
|
||||
return {"errs": errs, "sid": sid, "type": "", "ok": false}
|
||||
if seen.has(sid):
|
||||
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
|
||||
seen[sid] = true
|
||||
return {"errs": errs, "sid": sid, "type": "", "ok": false}
|
||||
seen[sid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
|
||||
var type: String = row.get("type", "")
|
||||
if not SKILL_TYPES.has(type):
|
||||
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
|
||||
return {"errs": errs, "sid": sid, "type": type, "ok": false}
|
||||
return {"errs": errs, "sid": sid, "type": type, "ok": true}
|
||||
|
||||
|
||||
## 公共域(全类型):trigger 触发条件校验/普攻行禁配 trigger/冷却域
|
||||
func _validate_common_fields(sid: String, row: Dictionary, type: String, errs: Array[Dictionary]) -> void:
|
||||
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
|
||||
if type == "basic" and row.has("trigger"):
|
||||
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
|
||||
# 冷却域(全类型):施放后连续 N 拍不可摇中
|
||||
var cd: int = int(row.get("cooldown_beats", -1))
|
||||
if cd < 0 or cd > 99:
|
||||
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
|
||||
|
||||
|
||||
## 伤害技域:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
|
||||
## 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
|
||||
func _validate_damage_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if not row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
|
||||
else:
|
||||
var m: float = float(row.damage_mult)
|
||||
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
|
||||
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
|
||||
if row.has("buff_ref") or row.has("buff_duration"):
|
||||
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
|
||||
if row.has("rider_mult"):
|
||||
var rm: float = float(row.rider_mult)
|
||||
if rm <= 0.0 or rm > RIDER_MULT_MAX:
|
||||
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
|
||||
|
||||
|
||||
## 增益技域:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
|
||||
func _validate_buff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
|
||||
var bd: int = int(row.get("buff_duration", 0))
|
||||
if bd < 1 or bd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
|
||||
|
||||
|
||||
## 妨害技域:0 伤害施放动作,对主角施加负面状态——
|
||||
## 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
|
||||
func _validate_debuff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
|
||||
if row.has("damage_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
|
||||
if row.has("rider_mult"):
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||||
if String(row.get("buff_ref", "")) == "":
|
||||
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
|
||||
var dbd: int = int(row.get("buff_duration", 0))
|
||||
if dbd < 1 or dbd > 99:
|
||||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
|
||||
|
||||
|
||||
## 表不变量域:普攻行存在性(池构成纪律的结构前提)与保底旋钮值域
|
||||
func _validate_table_invariants(has_basic: bool, errs: Array[Dictionary]) -> void:
|
||||
if not has_basic:
|
||||
errs.append({"category": "structural", "message": "SkillTable 无普攻行(basic)——池构成纪律的结构前提"})
|
||||
if pity_threshold < 1 or pity_threshold > 99:
|
||||
errs.append({"category": "domain", "message": "pity_threshold 越域 [1,99]: %d" % pity_threshold})
|
||||
return errs
|
||||
|
||||
|
||||
# trigger 域校验(敌人系统 v1.2 触发条件:四键白名单/词表出域/类型分派——枚举事实算子限域 eq/neq
|
||||
|
||||
@@ -18,30 +18,12 @@ func validate() -> Array:
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var eid: String = row.get("effect_id", "")
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
|
||||
var head := _validate_row_head(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
if seen.has(eid):
|
||||
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
|
||||
seen[eid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
if not TARGET_SIDES.has(row.get("target_side", "")):
|
||||
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
|
||||
var unit: String = row.get("unit", "")
|
||||
if not UNITS.has(unit):
|
||||
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
|
||||
if not STACK_POLICIES.has(row.get("stack_policy", "")):
|
||||
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
|
||||
var m: int = int(row.get("magnitude", 0))
|
||||
# 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1 / ac ∈ [-20,20] 且 ≠0(AC 阈值轴——幅度域独立,防呆上限 20)
|
||||
if unit == "percent" and (m == 0 or m < -100 or m > 100):
|
||||
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
|
||||
if unit == "flat" and m < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
|
||||
if unit == "ac" and (m == 0 or m < -20 or m > 20):
|
||||
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
|
||||
var eid: String = head["eid"]
|
||||
errs.append_array(_validate_magnitude_domain(row, eid, head["unit"]))
|
||||
var dur: int = int(row.get("duration_settlements", 0))
|
||||
if dur < 1 or dur > 99:
|
||||
errs.append({"category": "domain", "message": "%s duration_settlements 越域 [1,99]: %d" % [eid, dur]})
|
||||
@@ -51,6 +33,44 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
## 行头域:effect_id 非空与查重/display_name/target_side/unit/stack_policy 词表
|
||||
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var eid: String = row.get("effect_id", "")
|
||||
if eid == "":
|
||||
errs.append({"category": "domain", "message": "effect_id 缺失/空"})
|
||||
return {"errs": errs, "eid": eid, "unit": "", "ok": false}
|
||||
if seen.has(eid):
|
||||
errs.append({"category": "dup_key", "message": "effect_id 重复: %s" % eid})
|
||||
seen[eid] = true
|
||||
return {"errs": errs, "eid": eid, "unit": "", "ok": false}
|
||||
seen[eid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % eid})
|
||||
if not TARGET_SIDES.has(row.get("target_side", "")):
|
||||
errs.append({"category": "enum", "message": "%s target_side 出域" % eid})
|
||||
var unit: String = row.get("unit", "")
|
||||
if not UNITS.has(unit):
|
||||
errs.append({"category": "enum", "message": "%s unit 出域" % eid})
|
||||
if not STACK_POLICIES.has(row.get("stack_policy", "")):
|
||||
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
|
||||
return {"errs": errs, "eid": eid, "unit": unit, "ok": true}
|
||||
|
||||
|
||||
## 幅度量纲域(三段量纲判据——percent/flat/ac 各自值域;
|
||||
## ac = AC 阈值轴效果,非池百分比非定点值——幅度域独立,防呆上限 20)
|
||||
func _validate_magnitude_domain(row: Dictionary, eid: String, unit: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var m: int = int(row.get("magnitude", 0))
|
||||
if unit == "percent" and (m == 0 or m < -100 or m > 100):
|
||||
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
|
||||
if unit == "flat" and m < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
|
||||
if unit == "ac" and (m == 0 or m < -20 or m > 20):
|
||||
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
|
||||
return errs
|
||||
|
||||
|
||||
func row_by_id(effect_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if row.get("effect_id", "") == effect_id:
|
||||
|
||||
@@ -26,57 +26,82 @@ func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var vid: String = row.get("variant_id", "")
|
||||
if vid == "":
|
||||
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
|
||||
var head := _validate_row(row, seen)
|
||||
errs.append_array(head["errs"])
|
||||
if not head["ok"]:
|
||||
continue
|
||||
if seen.has(vid):
|
||||
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
|
||||
var vid: String = head["vid"]
|
||||
errs.append_array(_validate_effect_interlocks(row, vid))
|
||||
errs.append_array(_validate_global_interlocks(row, vid))
|
||||
return errs
|
||||
|
||||
|
||||
## 行域:variant_id 非空与查重/display_name/unlock_level/四词表(scope/effect/trigger/placement)
|
||||
func _validate_row(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
var vid: String = row.get("variant_id", "")
|
||||
if vid == "":
|
||||
errs.append({"category": "domain", "message": "variant_id 缺失/空"})
|
||||
return {"errs": errs, "vid": vid, "ok": false}
|
||||
if seen.has(vid):
|
||||
errs.append({"category": "dup_key", "message": "variant_id 重复: %s" % vid})
|
||||
seen[vid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
|
||||
var ul: int = int(row.get("unlock_level", 0))
|
||||
if ul < 1 or ul > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
|
||||
if not SCOPES.has(row.get("scope", "")):
|
||||
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
|
||||
if not EFFECTS.has(row.get("effect", "")):
|
||||
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
|
||||
if not TRIGGERS.has(row.get("trigger", "")):
|
||||
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
|
||||
if not PLACEMENTS.has(row.get("placement", "")):
|
||||
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
|
||||
# 跨字段互锁(加载期致命——枚举语义一致性立闸):
|
||||
# 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数
|
||||
if row.get("effect", "") == "flat_damage":
|
||||
if row.get("trigger", "") != "on_open":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
|
||||
if row.get("placement", "") != "closed_only":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
|
||||
var ev: int = int(row.get("effect_value", 0))
|
||||
if ev < 1 or ev > 9:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
|
||||
# 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
|
||||
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
|
||||
# 信息遮蔽型只允许落安全格(遮蔽对象是数字;危险格无数字可遮)——placement=any 时由读取方
|
||||
# 呈现面按 kind=safe 自然短路,此处不设布置约束
|
||||
# 全局作用域型互锁(resource_face ⟺ scope=global 三条 + 新值域;
|
||||
# 判定键=effect/scope 词值,既有六 region 行(flat_damage/hide_number)不入本组——零误伤)
|
||||
if row.get("effect", "") == "resource_face":
|
||||
if row.get("scope", "") != "global":
|
||||
errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid})
|
||||
# resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用)
|
||||
var gev: int = int(row.get("effect_value", 0))
|
||||
if gev < 0 or gev > 50:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]})
|
||||
if row.get("scope", "") == "global":
|
||||
if row.get("effect", "") != "resource_face":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid})
|
||||
if row.get("placement", "") != "any":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid})
|
||||
if row.get("trigger", "") != "always":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(整图恒在,无触发时机)" % vid})
|
||||
return {"errs": errs, "vid": vid, "ok": false}
|
||||
seen[vid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % vid})
|
||||
var ul: int = int(row.get("unlock_level", 0))
|
||||
if ul < 1 or ul > 999:
|
||||
errs.append({"category": "domain", "message": "%s unlock_level 越域 [1,999]: %d" % [vid, ul]})
|
||||
if not SCOPES.has(row.get("scope", "")):
|
||||
errs.append({"category": "enum", "message": "%s scope 出域: %s" % [vid, row.get("scope")]})
|
||||
if not EFFECTS.has(row.get("effect", "")):
|
||||
errs.append({"category": "enum", "message": "%s effect 出域: %s" % [vid, row.get("effect")]})
|
||||
if not TRIGGERS.has(row.get("trigger", "")):
|
||||
errs.append({"category": "enum", "message": "%s trigger 出域: %s" % [vid, row.get("trigger")]})
|
||||
if not PLACEMENTS.has(row.get("placement", "")):
|
||||
errs.append({"category": "enum", "message": "%s placement 出域: %s" % [vid, row.get("placement")]})
|
||||
return {"errs": errs, "vid": vid, "ok": true}
|
||||
|
||||
|
||||
## 效果互锁域(加载期致命——枚举语义一致性立闸):
|
||||
## 伤害型 = 挖开即伤 ⇒ 触发 on_open + 只落未开格(closed_only)+ 伤害量为加法核小整数;
|
||||
## 信息型 = 被打开且显示数字时起作用 ⇒ 触发 on_number_shown
|
||||
## (信息遮蔽型只允许落安全格——遮蔽对象是数字,危险格无数字可遮;placement=any 时由读取方
|
||||
## 呈现面按 kind=safe 自然短路,此处不设布置约束)
|
||||
func _validate_effect_interlocks(row: Dictionary, vid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if row.get("effect", "") == "flat_damage":
|
||||
if row.get("trigger", "") != "on_open":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ trigger 必为 on_open" % vid})
|
||||
if row.get("placement", "") != "closed_only":
|
||||
errs.append({"category": "cross_field", "message": "%s flat_damage ⇒ placement 必为 closed_only(只出现在未打开的格子)" % vid})
|
||||
var ev: int = int(row.get("effect_value", 0))
|
||||
if ev < 1 or ev > 9:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [1,9](加法核小整数): %d" % [vid, ev]})
|
||||
if row.get("effect", "") == "hide_number" and row.get("trigger", "") != "on_number_shown":
|
||||
errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid})
|
||||
return errs
|
||||
|
||||
|
||||
## 全局作用域互锁域(resource_face ⟺ scope=global;判定键=effect/scope 词值,
|
||||
## 既有六 region 行(flat_damage/hide_number)不入本组——零误伤);
|
||||
## resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用)
|
||||
func _validate_global_interlocks(row: Dictionary, vid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if row.get("effect", "") == "resource_face":
|
||||
if row.get("scope", "") != "global":
|
||||
errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid})
|
||||
var gev: int = int(row.get("effect_value", 0))
|
||||
if gev < 0 or gev > 50:
|
||||
errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]})
|
||||
if row.get("scope", "") == "global":
|
||||
if row.get("effect", "") != "resource_face":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid})
|
||||
if row.get("placement", "") != "any":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid})
|
||||
if row.get("trigger", "") != "always":
|
||||
errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(整图恒在,无触发时机)" % vid})
|
||||
return errs
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user