# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-16 # 数据配置 SkillTable schema(定义层"技能行",敌人系统 回填值;行 = Dictionary,键 skill_id;Rule 4 豁免)。 # 设计权威:敌人系统.md 技能池节 §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 {}