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

90 lines
7.7 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
## 战斗结算表 schema:结算全局旋钮(段序/克制矩阵/掷点档位)单行全局类型化读取(垂直切片验证:完整战斗逐行归因)。
## 结构冻结位(段序/取整/pool clamp/效果池映射)违例加载期致命(fail-fast)。
class_name SettlementTable
extends Resource
@export var equivalence_ref_damage: int = 12 # [1,999] knob——当量参考敞口 D_ref
@export var p_perfect: int = 200 # {100,200} percent——完美排雷第二份兑付触发(载体域)
@export var seal_hit_premium: int = 100 # [100,999] percent knob——封印命中兑付溢价 ×(1+(n−100)/100):125=×1.25(条款3 条件二封印溢价形态 a,provisional);100=无溢价直通零扰动。只乘封印命中正向兑付(flat power/percent 幅度/gp 缓冲三形态),标空/挖格/清图不乘;雷安同池冻结不动
@export var segment_order: PackedStringArray = [] # 结构冻结——段序恒等校验违例致命
@export var source_label_map: Dictionary[String, Variant] = {} # 结构冻结——source_class→文案键
@export var effect_pool_map: Dictionary[String, Variant] = {} # 结构冻结——effect_id→pool(悬空加载致命)
@export var pool_clamp_lo: int = -100 # 结构冻结
@export var pool_clamp_hi: int = 100 # 结构冻结
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
# ---- 攻击类型 × 护甲克制 + 充能奖励定义(数值 provisional)----
# 主角攻击 = 普通(atk×(1+attack_up),不吃克制)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加。
# 【两条抽象分离,勿焊死】攻击类型 = 伤害的属性轴,只决定查 armor_effectiveness 哪一行,定义里不含任何棋盘来源;
# 充能 = 按攻击类型分桶的附加伤害储备,"谁往桶里放东西"属**奖励的定义**(开格/标雷只是奖励触发源,奖励内容可改)。
# 无 damage_a_per_cell / damage_a_cap / damage_b_per_seal 与 charge_grants 字段(攻击模型=单分量)。
@export var armor_effectiveness: Dictionary[String, Variant] = {} # 克制修正矩阵(加法点数 delta):{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: 点数 [-3,+3]}}
# ---- 护甲等级掷点(数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 定稿)----
# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档;
# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何伤害落地全局下限 damage_floor(≥1)——
# 全来源(攻击双方/棋盘直伤/地形变异/反噬,取值域 [1,+∞) 无上限)。
# 掷点只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线),穿刺/魔法充能与棋盘来源伤害不掷不折。
# 微调旋钮 = 档位边界(roll_tier_bounds)与档位值(roll_tier_discounts),不动 AC 值(armor_class 归单位表)。
@export var roll_faces: int = 20 # [2,1000] knob——掷点骰面(d20)
@export var attack_bonus: int = 0 # [0,999] 结构位——攻击者掷点加成,切片恒 0(元进度/武器附魔未来投值)
@export var roll_tier_bounds: Array[int] = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
@export var roll_tier_discounts: Array[int] = [] # 档位值 percent 降序(与 bounds 同长;[75,50,25] = ×0.75/×0.5/×0.25)
@export var crit_roll: int = 20 # [1,roll_faces] knob——nat 最大面 = 暴击触发掷点(恒全额)
@export var crit_multiplier: int = 200 # [100,999] percent knob——暴击倍率(200 = ×2,只乘该次基础攻击分量)
@export var damage_floor: int = 1 # [1,999] knob——任何伤害落地全局下限·全来源(无"被减伤归零 v=0"语义)
@export var armor_roll_stream: String = "armor_roll" # 掷点用途标签(rng_service PURPOSE_TAGS 成员——不参与派生,仅作事件日志用途标记)
const ARMOR_KEYS := ["light", "ward", "heavy"]
const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"]
const FROZEN_SEGMENT_ORDER := ["grant", "buff_debuff", "combat", "decay", "verdict"]
const P_PERFECT_DOMAIN := [100, 200]
func validate() -> Array:
var errs: Array[Dictionary] = []
if equivalence_ref_damage < 1 or equivalence_ref_damage > 999:
errs.append({"category": "domain", "message": "equivalence_ref_damage 越域 [1,999]: %d" % equivalence_ref_damage})
if not P_PERFECT_DOMAIN.has(p_perfect):
errs.append({"category": "pperfect_oob", "message": "p_perfect 载体域 {100,200}: %d" % p_perfect})
if seal_hit_premium < 100 or seal_hit_premium > 999:
errs.append({"category": "domain", "message": "seal_hit_premium 越域 [100,999](100=无溢价): %d" % seal_hit_premium})
var seg := Array(segment_order)
if seg != FROZEN_SEGMENT_ORDER:
errs.append({"category": "segorder_violation", "message": "segment_order 违例——恒等拍板段序校验失败: %s" % str(seg)})
if round_direction != "floor":
errs.append({"category": "frozen", "message": "round_direction 结构冻结 floor: %s" % round_direction})
if pool_clamp_lo != -100 or pool_clamp_hi != 100:
errs.append({"category": "frozen", "message": "pool_clamp 域结构冻结 [-100,+100]"})
if roll_faces < 2 or roll_faces > 1000:
errs.append({"category": "domain", "message": "roll_faces 越域 [2,1000]: %d" % roll_faces})
if crit_roll < 1 or crit_roll > roll_faces:
errs.append({"category": "domain", "message": "crit_roll 越域 [1,roll_faces(%d)]: %d" % [roll_faces, crit_roll]})
if crit_multiplier < 100 or crit_multiplier > 999:
errs.append({"category": "domain", "message": "crit_multiplier 越域 [100,999]: %d" % crit_multiplier})
if damage_floor < 1 or damage_floor > 999:
errs.append({"category": "domain", "message": "damage_floor 越域 [1,999]: %d" % damage_floor})
if attack_bonus < 0 or attack_bonus > 999:
errs.append({"category": "domain", "message": "attack_bonus 越域 [0,999]: %d" % attack_bonus})
if String(armor_roll_stream) == "":
errs.append({"category": "domain", "message": "armor_roll_stream 空"})
# 档位表形状:bounds 升序且 ∈ [1, faces-1];discounts 降序且 ∈ [1,99](无零档——非归零);
# 档数 = 边界数 + 1(缺口 [1,b0] → discounts[0],(b_i,b_{i+1}] → discounts[i+1],> b 末值 → 末位档)
if roll_tier_bounds.is_empty() or roll_tier_discounts.size() != roll_tier_bounds.size() + 1:
errs.append({"category": "cross_field", "message": "roll_tier_bounds/discounts 形状违例(档数须 = 边界数 + 1): %d 边界 vs %d 档" % [roll_tier_bounds.size(), roll_tier_discounts.size()]})
else:
for i in roll_tier_bounds.size():
var b: int = int(roll_tier_bounds[i])
if b < 1 or b >= roll_faces or (i > 0 and b <= int(roll_tier_bounds[i - 1])):
errs.append({"category": "cross_field", "message": "roll_tier_bounds[%d] 越域/非升序: %d" % [i, b]})
var d: int = int(roll_tier_discounts[i])
if d < 1 or d > 99 or (i > 0 and d >= int(roll_tier_discounts[i - 1])):
errs.append({"category": "cross_field", "message": "roll_tier_discounts[%d] 越域/非降序(档位不设零档): %d" % [i, d]})
for atk_type in ATTACK_TYPE_KEYS:
var row: Dictionary[Variant, Variant] = armor_effectiveness.get(atk_type, {})
for armor in ARMOR_KEYS:
var pct: int = int(row.get(armor, -99))
if pct < -3 or pct > 3:
errs.append({"category": "domain", "message": "armor_effectiveness.%s.%s 越域 [-3,+3]: %d" % [atk_type, armor, pct]})
return errs