108 lines
6.7 KiB
GDScript
108 lines
6.7 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||
# Date: 2026-09-16
|
||
# 数据配置 SkillTable schema(敌人技能池批——定义层"技能行",敌人系统 回填值;行 = Dictionary,键 skill_id;Rule 4 豁免)。
|
||
# 设计权威:proposal-skill-pool.md〔提案已吸收退役 git a6833b6a——现行事实源=敌人系统.md 技能池节〕 v1.0 §2.2 字段词汇表。
|
||
# 三层对象:技能行(本表,多敌种共享)· 池模板(EnemyTable.pool:{skill_id, weight})· 实例池(单位容器 spawn 快照)。
|
||
# 池构成纪律的加载校验在 ConfigDB 跨表通道(每池恰一行普攻、增益 ≤1、skill_id 悬空致命——本表无池视野)。
|
||
class_name SkillTable
|
||
extends Resource
|
||
|
||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||
# 保底 M(provisional 归 难度曲线与Boss):连续 M 拍执行的都是普通攻击 → 下拍必出特殊技
|
||
# (特殊技内按权重、不受冷却限制——proposal §2.1 两道闸之二)。全局单旋钮(不分敌种)。
|
||
@export var pity_threshold: int = 4
|
||
|
||
const SKILL_TYPES := ["basic", "damage", "buff", "debuff"] # 普通攻击 / 伤害 / 增益 / 妨害(封闭枚举——新效应词汇批 2026-09-19 由三扩四)
|
||
const MULT_DOMAIN_MIN := 1.1 # damage_mult 域 [1.1, 16] provisional(proposal §2.2)
|
||
const MULT_DOMAIN_MAX := 16.0
|
||
const RIDER_MULT_MAX := 1.0 # rider_mult 域 (0, 1](必中附加分量——proposal-status-vocabulary §2;首发 0.75 锚 AC 折扣档最轻档)
|
||
|
||
|
||
func validate() -> Array:
|
||
var errs: Array[Dictionary] = []
|
||
if rows.is_empty():
|
||
errs.append({"category": "empty_table", "message": "SkillTable 为空"})
|
||
return errs
|
||
var seen := {}
|
||
var has_basic := false
|
||
for row in rows:
|
||
var sid: String = row.get("skill_id", "")
|
||
if sid == "":
|
||
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
|
||
continue
|
||
if seen.has(sid):
|
||
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
|
||
seen[sid] = true
|
||
if String(row.get("display_name", "")) == "":
|
||
errs.append({"category": "domain", "message": "%s display_name 空" % sid})
|
||
var type: String = row.get("type", "")
|
||
if not SKILL_TYPES.has(type):
|
||
errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]})
|
||
continue
|
||
# 冷却域(全类型):施放后连续 N 拍不可摇中
|
||
var cd: int = int(row.get("cooldown_beats", -1))
|
||
if cd < 0 or cd > 99:
|
||
errs.append({"category": "domain", "message": "%s cooldown_beats 越域 [0,99]: %d" % [sid, cd]})
|
||
# 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2)
|
||
if type == "basic":
|
||
has_basic = true
|
||
if row.has("damage_mult") and not is_equal_approx(float(row.damage_mult), 1.0):
|
||
errs.append({"category": "cross_field", "message": "%s 普攻 damage_mult ≠ 1(禁配——要配就是伤害技): %s" % [sid, str(row.damage_mult)]})
|
||
if row.has("cooldown_beats") and cd != 0:
|
||
errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]})
|
||
if row.has("buff_ref") or row.has("buff_duration"):
|
||
errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid})
|
||
if row.has("rider_mult"):
|
||
errs.append({"category": "cross_field", "message": "%s 普攻不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||
# 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
|
||
# 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
|
||
if type == "damage":
|
||
if not row.has("damage_mult"):
|
||
errs.append({"category": "cross_field", "message": "%s 伤害技缺 damage_mult(存倍率不存固定值)" % sid})
|
||
else:
|
||
var m: float = float(row.damage_mult)
|
||
if m < MULT_DOMAIN_MIN or m > MULT_DOMAIN_MAX:
|
||
errs.append({"category": "domain", "message": "%s damage_mult 越域 [%.1f, %.1f]: %s" % [sid, MULT_DOMAIN_MIN, MULT_DOMAIN_MAX, str(m)]})
|
||
if row.has("buff_ref") or row.has("buff_duration"):
|
||
errs.append({"category": "cross_field", "message": "%s 伤害技不带增益字段" % sid})
|
||
if row.has("rider_mult"):
|
||
var rm: float = float(row.rider_mult)
|
||
if rm <= 0.0 or rm > RIDER_MULT_MAX:
|
||
errs.append({"category": "domain", "message": "%s rider_mult 越域 (0, %.1f]: %s" % [sid, RIDER_MULT_MAX, str(rm)]})
|
||
# 增益技:0 伤害动作(无倍率),效果引用 + 持续回合;悬空引用由 ConfigDB 跨表校验兜底
|
||
if type == "buff":
|
||
if row.has("damage_mult"):
|
||
errs.append({"category": "cross_field", "message": "%s 增益技为 0 伤害动作,不带 damage_mult" % sid})
|
||
if row.has("rider_mult"):
|
||
errs.append({"category": "cross_field", "message": "%s 增益技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||
if String(row.get("buff_ref", "")) == "":
|
||
errs.append({"category": "cross_field", "message": "%s 增益技缺 buff_ref(效果引用)" % sid})
|
||
var bd: int = int(row.get("buff_duration", 0))
|
||
if bd < 1 or bd > 99:
|
||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, bd]})
|
||
# 妨害技(新效应词汇批 2026-09-19):0 伤害施放动作,对主角施加负面状态——
|
||
# 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
|
||
if type == "debuff":
|
||
if row.has("damage_mult"):
|
||
errs.append({"category": "cross_field", "message": "%s 妨害技为 0 伤害动作,不带 damage_mult" % sid})
|
||
if row.has("rider_mult"):
|
||
errs.append({"category": "cross_field", "message": "%s 妨害技不带必中附加分量(rider_mult 仅伤害技)" % sid})
|
||
if String(row.get("buff_ref", "")) == "":
|
||
errs.append({"category": "cross_field", "message": "%s 妨害技缺 buff_ref(效果引用)" % sid})
|
||
var dbd: int = int(row.get("buff_duration", 0))
|
||
if dbd < 1 or dbd > 99:
|
||
errs.append({"category": "domain", "message": "%s buff_duration 越域 [1,99]: %d" % [sid, dbd]})
|
||
if not has_basic:
|
||
errs.append({"category": "structural", "message": "SkillTable 无普攻行(basic)——池构成纪律的结构前提"})
|
||
if pity_threshold < 1 or pity_threshold > 99:
|
||
errs.append({"category": "domain", "message": "pity_threshold 越域 [1,99]: %d" % pity_threshold})
|
||
return errs
|
||
|
||
|
||
func row_by_id(skill_id: String) -> Dictionary:
|
||
for row in rows:
|
||
if row.get("skill_id", "") == skill_id:
|
||
return row
|
||
return {}
|