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

89 lines
7.4 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
# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径 + 克制/充能批 + 护甲等级掷点批。
# 三行致命 manifest:segorder_violation / poolmap_dangling / pperfect_oob。
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 segment_order: PackedStringArray = [] # 结构冻结——恒等拍板段序校验违例致命
@export var source_label_map: Dictionary = {} # 结构冻结——source_class→文案键
@export var effect_pool_map: Dictionary = {} # 结构冻结——effect_id→pool(悬空加载致命)
@export var pool_clamp_lo: int = -100 # 结构冻结
@export var pool_clamp_hi: int = 100 # 结构冻结
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
# ---- 攻击类型 × 护甲克制批 + 充能奖励定义(2026-09-12 用户拍板结构;2026-09-15 命名统一批 / 抽象分离批;数值 provisional)----
# 主角攻击 = 普通(atk×(1+attack_up),不吃克制)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加。
# 【两条抽象分离,勿焊死】攻击类型 = 伤害的属性轴,只决定查 armor_effectiveness 哪一行,定义里不含任何棋盘来源;
# 充能 = 按攻击类型分桶的附加伤害储备,"谁往桶里放东西"属**奖励的定义**(开格/标雷只是奖励触发源,奖励内容可改)。
# 原 damage_a_per_cell / damage_a_cap / damage_b_per_flag 与 charge_grants 充能奖励定义均退役(2026-09-19 充能退役批——攻击模型回归单分量)。
@export var armor_effectiveness: Dictionary = {} # 克制修正矩阵(加法点数 delta):{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: 点数 [-3,+3]}}
# ---- 护甲等级掷点批(2026-09-16 用户拍板,数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 同批定稿)----
# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档;
# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何伤害落地全局下限 damage_floor(≥1)——
# 2026-09-20 用户拍板扩全来源(攻击双方/棋盘直伤/地形变异/反噬,取值域 [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 = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
@export var roll_tier_discounts: Array = [] # 档位值 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 成员——并流后不参与派生,仅作事件日志用途标记;ADR-0007 裁丙 2026-09-27)
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 = []
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})
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 = 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