Files
gplan-2/game/data/schemas/reward_table.gd
T

121 lines
6.8 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 = [] # Array[Dictionary]
const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp"] # 九型封闭(格面资源九型批 2026-09-20 八→九,词表 v1.1)——清单外加载拒绝
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 = []
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]})
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 = []
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 {}