## 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 {}