134 lines
8.1 KiB
GDScript
134 lines
8.1 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||
# Date: 2026-09-10(2026-09-15 格面资源批结构性重写:category/use_timing/库存/供给轴下线,五型 × 极性字典取代)
|
||
# 数据配置 RewardTable schema(地图战斗资源 回填值)。
|
||
# 行 = Dictionary,键 reward_id(Rule 4 豁免:定义表);跨字段校验族 + 池护栏。
|
||
# 九型封闭枚举(attack/defense/heal/blast/weaken/pierce/magic/action/gp)× 极性(positive/negative):
|
||
# 正向行 = 摇号池(权重 = 类型频次);负向行 = 纯兑付字典(不消费权重)。
|
||
# 九型批(2026-09-20 词表 v1.1 注记——用户拍板 GP 进第九型):gp 型正负两行入池,
|
||
# effect_id gp_gain/gp_loss = 战斗内缓冲通道(战斗结算 escrow 记账,不入增益容器)。
|
||
class_name RewardTable
|
||
extends Resource
|
||
|
||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||
|
||
const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp", "aux_item"] # 十型(迭代 7 T1 魔法道具内容批扩值——registry aux_items 词表;aux 行 effect_id = item_id 双表校验键)
|
||
const AUX_ITEM_TYPE := "aux_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
|
||
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 投放行专属校验(迭代 7 T1):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
|
||
# 对 aux_items 词表校验——reward_dangling_aux)/无 power 无区间(数值语义不复用,道具效果读 AuxItemTable)
|
||
if ctype == AUX_ITEM_TYPE:
|
||
# 负向行 = 雷格极性字典占位(雷安同池摇号下雷格可摇中 aux 身份——负路由零兑付兜底,
|
||
# effect_id 空/power 0/weight 0 不入池;2026-09-29 T1)
|
||
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/区间数值(道具数值读 AuxItemTable)" % 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]})
|
||
# 字典完整性 / 池护栏累计(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:
|
||
cell_positive_live += 1
|
||
if pool == "clear_default" and eff == "" and power >= 1:
|
||
clear_value_ok = true
|
||
# ① 字典完整性:五型 × 正负两行齐备(缺任一即致命)
|
||
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
|
||
|
||
|
||
func rows_by_pool(pool_tag: String) -> Array:
|
||
var out: Array[Dictionary] = []
|
||
for row in rows:
|
||
if row.get("pool_tag", "") == pool_tag:
|
||
out.append(row)
|
||
return out
|
||
|
||
|
||
func row_by_id(rid: String) -> Dictionary:
|
||
for row in rows:
|
||
if row.get("reward_id", "") == rid:
|
||
return row
|
||
return {}
|