166 lines
9.1 KiB
GDScript
166 lines
9.1 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 格面资源表 schema:逐 reward_id 行(类型×极性字典 + 摇号池 + aux 投放行)的类型化读取(垂直切片验证:完整战斗逐行归因)。
|
||
## (类型, 极性) 字典齐备与 cell 池非空是加载前提——缺行/池空即致命(fail-fast)。
|
||
class_name RewardTable
|
||
extends Resource
|
||
|
||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||
|
||
const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp", "treasure_item"] # 十型(registry 秘宝词表;aux 行 effect_id = item_id 双表校验键)
|
||
const TREASURE_ITEM_TYPE := "treasure_item" # aux 行标识(掉落投放行:挖开→战斗结算 grant 路由关卡域库存,不入增益容器)
|
||
const POLARITIES := ["positive", "negative"]
|
||
const POOL_TAGS := ["cell", "clear_default"] # cell = 格面资源单一池;clear_<biome> 命名空间预留拒绝
|
||
const CELL_TYPE_KEYS := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp"] # 字典完整性校验轴(类型 × 极性 = 十八格)
|
||
|
||
|
||
func validate() -> Array:
|
||
var errs: Array[Dictionary] = []
|
||
if rows.is_empty():
|
||
errs.append({"category": "empty_table", "message": "RewardTable 为空"})
|
||
return errs
|
||
var seen := {}
|
||
var cell_dict := {} # "type|polarity" → row(字典完整性)
|
||
var cell_positive_live := 0 # cell 池 weight>0 正向行计数(池空护栏)
|
||
var clear_value_ok := false # clear_default 恒正当量(行动经济 条款④)
|
||
for row in rows:
|
||
var rid: String = row.get("reward_id", "")
|
||
if rid == "":
|
||
errs.append({"category": "domain", "message": "reward_id 空"})
|
||
continue
|
||
if seen.has(rid):
|
||
errs.append({"category": "dup_key", "message": "reward_id 重复: %s" % rid})
|
||
seen[rid] = true
|
||
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 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 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]})
|
||
if cell_positive_live < 1:
|
||
errs.append({"category": "cross_field", "message": "cell 池零活正向行(weight>0)——摇号池空"})
|
||
if not clear_value_ok:
|
||
errs.append({"category": "cross_field", "message": "clear_default 池无正当量活行(清图奖励 > 0 违例)"})
|
||
return errs
|
||
|
||
|
||
func row_by_id(rid: String) -> Dictionary:
|
||
for row in rows:
|
||
if row.get("reward_id", "") == rid:
|
||
return row
|
||
return {}
|