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

201 lines
11 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:逐 skill_id 行的类型化读取(多敌种共享的定义层;垂直切片验证:完整战斗逐行归因)。
## 本表无池视野:池构成纪律(每池恰一行普攻、增益 ≤1、skill_id 悬空致命)校验在 ConfigDB 跨表通道。
## 普攻行结构锁死(倍率恒 1、冷却恒 0)——「池恒非空」的结构前提,禁配伤害/冷却参数。
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"] # 普通攻击 / 伤害 / 增益 / 妨害(封闭枚举四型)
# trigger(触发条件)封闭词表(敌人系统 v1.2「触发条件」节——出域加载期拒绝止动):
const TRIGGER_FACTS := ["self_hp_pct", "hero_hp_pct", "round_index", "last_round_end", "mines_hit_last", "mines_hit_total"]
const TRIGGER_OPS := ["eq", "neq", "gt", "gte", "lt", "lte"]
const TRIGGER_KEYS := ["fact", "op", "value", "latch"]
const TRIGGER_ENUM_FACTS := ["last_round_end"] # 枚举事实(op 限域 eq/neq;value 限枚举串)
const TRIGGER_ENUM_VALUES := ["none", "cleared", "stopped", "ap_exhausted"]
const TRIGGER_PCT_FACTS := ["self_hp_pct", "hero_hp_pct"] # 百分比事实(value 域 [1,99]——阈值式求值)
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 head := _validate_row_head(row, seen)
errs.append_array(head["errs"])
if not head["ok"]:
continue
var sid: String = head["sid"]
var type: String = head["type"]
_validate_common_fields(sid, row, type, errs)
if type == "basic":
has_basic = true
_validate_basic_row(sid, row, int(row.get("cooldown_beats", -1)), errs)
if type == "damage":
_validate_damage_row(sid, row, errs)
if type == "buff":
_validate_buff_row(sid, row, errs)
if type == "debuff":
_validate_debuff_row(sid, row, errs)
_validate_table_invariants(has_basic, errs)
return errs
## 行头域:skill_id 非空与查重/display_name/type 枚举(ok=跳过行剩余判据)
func _validate_row_head(row: Dictionary, seen: Dictionary) -> Dictionary:
var errs: Array[Dictionary] = []
var sid: String = row.get("skill_id", "")
if sid == "":
errs.append({"category": "domain", "message": "skill_id 缺失/空"})
return {"errs": errs, "sid": sid, "type": "", "ok": false}
if seen.has(sid):
errs.append({"category": "dup_key", "message": "skill_id 重复: %s" % sid})
seen[sid] = true
return {"errs": errs, "sid": sid, "type": "", "ok": false}
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)]})
return {"errs": errs, "sid": sid, "type": type, "ok": false}
return {"errs": errs, "sid": sid, "type": type, "ok": true}
## 公共域(全类型):trigger 触发条件校验/普攻行禁配 trigger/冷却域
func _validate_common_fields(sid: String, row: Dictionary, type: String, errs: Array[Dictionary]) -> void:
_validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动)
if type == "basic" and row.has("trigger"):
errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid})
# 冷却域(全类型):施放后连续 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]})
## 伤害技域:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘);
## 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2)
func _validate_damage_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
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 跨表校验兜底
func _validate_buff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
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]})
## 妨害技域:0 伤害施放动作,对主角施加负面状态——
## 字段复用 buff_ref/buff_duration(落点由状态行 target_side 路由,ConfigDB side 守卫兜底);池纪律妨害 ≤1 在跨表通道
func _validate_debuff_row(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
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]})
## 表不变量域:普攻行存在性(池构成纪律的结构前提)与保底旋钮值域
func _validate_table_invariants(has_basic: bool, errs: Array[Dictionary]) -> void:
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})
# trigger 域校验(敌人系统 v1.2 触发条件:四键白名单/词表出域/类型分派——枚举事实算子限域 eq/neq
# 且 value 限枚举串、百分比事实 value 域 [1,99]、数值事实 value 须整数、latch 须布尔。行缺 trigger
# 直返——可省略字段)
func _validate_trigger(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void:
if not row.has("trigger"):
return
var trig: Variant = row.get("trigger", null)
if not (trig is Dictionary):
errs.append({"category": "domain", "message": "%s trigger 须为 {fact,op,value,latch} 字典" % sid})
return
var t: Dictionary[Variant, Variant] = trig
for tk in t.keys():
if not TRIGGER_KEYS.has(String(tk)):
errs.append({"category": "domain", "message": "%s trigger 键白名单外: %s" % [sid, str(tk)]})
_validate_trigger_domain(t, sid, errs)
# trigger fact/op/value/latch 域校验(词表出域与类型分派——报错顺序=报告顺序,分派段单列)
func _validate_trigger_domain(t: Dictionary[Variant, Variant], sid: String, errs: Array[Dictionary]) -> void:
var tf := String(t.get("fact", ""))
var top := String(t.get("op", ""))
if not TRIGGER_FACTS.has(tf):
errs.append({"category": "enum", "message": "%s trigger fact 出域: %s" % [sid, tf]})
if not TRIGGER_OPS.has(top):
errs.append({"category": "enum", "message": "%s trigger op 出域: %s" % [sid, top]})
if TRIGGER_ENUM_FACTS.has(tf) and not (top == "eq" or top == "neq"):
errs.append({"category": "domain", "message": "%s trigger 枚举事实 %s 算子限域 eq/neq: %s" % [sid, tf, top]})
_validate_trigger_value(sid, t, tf, errs)
if t.has("latch") and not (t.get("latch", false) is bool):
errs.append({"category": "domain", "message": "%s trigger latch 须布尔: %s" % [sid, str(t.get("latch", false))]})
# trigger value 类型分派校验(枚举事实 value 限枚举串/百分比事实 value 域 [1,99]/数值事实 value 须整数)
func _validate_trigger_value(sid: String, t: Dictionary[Variant, Variant], tf: String, errs: Array[Dictionary]) -> void:
if tf != "" and TRIGGER_ENUM_FACTS.has(tf):
var ev = t.get("value", "")
if not (ev is String) or not TRIGGER_ENUM_VALUES.has(String(ev)):
errs.append({"category": "domain", "message": "%s trigger 枚举事实 value 越域: %s(域 %s)" % [sid, str(ev), str(TRIGGER_ENUM_VALUES)]})
elif TRIGGER_PCT_FACTS.has(tf):
var pv := int(t.get("value", 0))
if pv < 1 or pv > 99:
errs.append({"category": "domain", "message": "%s trigger 百分比事实 value 越域 [1,99]: %d" % [sid, pv]})
elif tf != "" and not (t.get("value", 0) is int):
errs.append({"category": "domain", "message": "%s trigger 数值事实 value 须整数: %s" % [sid, str(t.get("value", 0))]})
# 普攻行禁配校验(倍率恒 1、冷却恒 0、不带增益/rider 字段——「池恒非空」的结构前提)
func _validate_basic_row(sid: String, row: Dictionary, cd: int, errs: Array[Dictionary]) -> void:
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})
func row_by_id(skill_id: String) -> Dictionary:
for row in rows:
if row.get("skill_id", "") == skill_id:
return row
return {}