# 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", "treasure_item"] # 十型(迭代 7 T1 秘宝内容批扩值——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_ 命名空间预留拒绝 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 跨表层 # 对秘宝词表校验——reward_dangling_treasure)/无 power 无区间(数值语义不复用,道具效果读 TreasureItemTable) if ctype == TREASURE_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/区间数值(道具数值读 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_ 命名空间预留拒绝: %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 row_by_id(rid: String) -> Dictionary: for row in rows: if row.get("reward_id", "") == rid: return row return {}