## VERTICAL SLICE - NOT FOR PRODUCTION ## 敌人表 schema:逐 (level_index, row_kind, depth_band, enemy_id) 行集的类型化读取(行集可多行混编;垂直切片验证:完整战斗逐行归因)。 ## 行序即契约:同带键多行必须文件内相邻——resolver 返回序/生成序/叙述取首行全依赖文件行序。 ## 深度带 = 节点深度所在的区间:配池按带不按点,难度坡随带递增;row_kind 承载「Boss 行按终点配置」。 ## 行集实例总数 ≤4 是开战骰后守卫(加载期不可判,闸在单位容器 spawn);技能池构成纪律需跨表视野,校验在 ConfigDB 跨表通道。 class_name EnemyTable extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] # 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集 const ROW_KINDS := ["battle", "boss"] # 护甲类型封闭枚举(克制矩阵本体在 SettlementTable; # 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲) const ARMOR_TYPES := ["light", "ward", "heavy"] # 攻击类型封闭枚举(敌方攻击类型轴开列,承接锻造工坊迁移面⑥; # 克制矩阵点数制键前缀 = 本枚举:normal → light 0 / ward +1 / heavy −1;pierce/magic 为差异化 # 预留档,差异化填值属新设计另立项。可省略 + 缺省 normal 向后兼容) const ATTACK_TYPES := ["normal", "pierce", "magic"] # 实例化数量域(域 [1,3] 双落点收口单点: # 本表 validate 与 单位容器 spawn 守卫共用;遭遇骰结果与行值 unit_count/unit_count_max 同域。 # MAX 语义 = **单种数**(单行实例数上限),行集实例总数 ≤4 另由 # spawn 期守卫承担——骰后值加载期不可判,双闸分工见 敌人系统 v0.15 行集构成纪律③) const UNIT_COUNT_MIN := 1 const UNIT_COUNT_MAX := 3 # 行集实例总数上限(行集构成纪律③:各行开战骰后数量求和 ≤4; # 「总数」= 实例数非行数〔Q9 二裁〕,行数不设上限。骰后值加载期不可判——守卫在 单位容器 spawn, # 常量单点在本表与数量域同位收口) const ROWSET_INSTANCE_MAX := 4 func validate() -> Array: var errs: Array[Dictionary] = [] if rows.is_empty(): errs.append({"category": "empty_table", "message": "EnemyTable 为空"}) return errs var seen := {} var last_rowset_key := "" var closed_rowset := {} var anchor_ok := {} var boss_ok := {} for row in rows: errs.append_array(_validate_row_base(row, seen)) var eid2 := String(row.get("enemy_id", "")) errs.append_array(_validate_row_numbers(row, eid2)) errs.append_array(_validate_row_atk_range(row, eid2)) errs.append_array(_validate_row_pool(row, eid2)) errs.append_array(_validate_row_types(row, eid2)) # 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须 # 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。 # 同带键多敌种混编合法;同带键同敌种第二行仍由 # dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。 var rowset_key := "%d_%s_%d" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))] if rowset_key != last_rowset_key: errs.append_array(_validate_rowset_closed(rowset_key, closed_rowset, row)) closed_rowset[rowset_key] = true last_rowset_key = rowset_key var L2: int = int(row.get("level_index", 0)) var rk2: String = String(row.get("row_kind", "")) if rk2 == "battle" and int(row.get("depth_band", -1)) == 0: anchor_ok[L2] = true elif rk2 == "boss": boss_ok[L2] = true if not anchor_ok.has(L2): anchor_ok[L2] = false errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok)) return errs ## 行头基面域:键提取/查重与登记/进度键越域/row_kind 枚举/enemy_id 非空/display_name func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]: var errs: Array[Dictionary] = [] var L: int = int(row.get("level_index", 0)) var rk: String = String(row.get("row_kind", "")) var band: int = int(row.get("depth_band", -1)) var eid: String = row.get("enemy_id", "") var key := "%d_%s_%d_%s" % [L, rk, band, eid] if seen.has(key): errs.append({"category": "dup_key", "message": "EnemyTable 键重复: %s" % key}) seen[key] = true if L < 1 or band < 0: errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]}) if not ROW_KINDS.has(rk): errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]}) if eid == "": errs.append({"category": "domain", "message": "enemy_id 空"}) if String(row.get("display_name", "")) == "": errs.append({"category": "domain", "message": "%s display_name 空" % eid}) return errs ## 数值域:unit_count(含可省略上限)/hp/atk 基值 func _validate_row_numbers(row: Dictionary, eid: String) -> Array[Dictionary]: var errs: Array[Dictionary] = [] var uc: int = int(row.get("unit_count", 0)) if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX: errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]}) # unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值 if row.has("unit_count_max"): var ucm: int = int(row.get("unit_count_max", uc)) if ucm < uc or ucm > UNIT_COUNT_MAX: errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]}) var hp: int = int(row.get("hp", 0)) if hp < 1 or hp > 999: errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid}) var atk: int = int(row.get("atk", -1)) if atk < 0 or atk > 999: errs.append({"category": "domain", "message": "%s atk 越域 [0,999]" % eid}) return errs ## atk 区间域(可省略配对):遭遇骰同流 spawn 期整战持久 roll; ## 中点锚 = 行动经济 条款①同款期望当量纪律(调区间必同步调 atk) func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]: var errs: Array[Dictionary] = [] var atk: int = int(row.get("atk", -1)) var has_amin: bool = row.has("atk_min") var has_amax: bool = row.has("atk_max") if has_amin != has_amax: errs.append({"category": "cross_field", "message": "%s atk_min/atk_max 必须成对出现" % eid}) elif has_amin: var amin: int = int(row.get("atk_min", 0)) var amax: int = int(row.get("atk_max", 0)) if amin < 0 or amax > 999 or amin > amax: errs.append({"category": "domain", "message": "%s atk 区间越域 [0,999] 且 min≤max: %d..%d" % [eid, amin, amax]}) if atk != int((amin + amax) / 2.0): errs.append({"category": "cross_field", "message": "%s atk(%d) ≠ 区间中点(%d)——中点恒等期望当量锚" % [eid, atk, (amin + amax) / 2]}) return errs ## 池模板域(单表结构校验;跨表构成纪律在 ConfigDB): ## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。 ## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。 func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]: var errs: Array[Dictionary] = [] var pool: Array[Variant] = row.get("pool", []) if pool.is_empty(): errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid}) var pool_ids := {} for entry_v in pool: if not (entry_v is Dictionary): errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]}) continue var entry: Dictionary[Variant, Variant] = entry_v var sid: String = String(entry.get("skill_id", "")) if sid == "": errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid}) elif pool_ids.has(sid): errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]}) pool_ids[sid] = true var w: int = int(entry.get("weight", 0)) if w <= 0 or w > 9999: errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]}) return errs ## 攻防类型域(均可省略——armor_type 缺省 light/attack_type 缺省 normal/armor_class 缺省 0 向后兼容) func _validate_row_types(row: Dictionary, eid: String) -> Array[Dictionary]: var errs: Array[Dictionary] = [] var at: String = String(row.get("armor_type", "light")) if not ARMOR_TYPES.has(at): errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]}) var att: String = String(row.get("attack_type", "normal")) if not ATTACK_TYPES.has(att): errs.append({"category": "enum", "message": "%s attack_type 未知枚举: %s(域 {normal, pierce, magic})" % [eid, att]}) # armor_class(防守方掷点判定阈值基数) var ac: int = int(row.get("armor_class", 0)) if ac < 0 or ac > 999: errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]}) return errs ## 行序违例域:当前行集键曾出现且 ≠ 上一行行集键 = 同带键行被隔断(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序) func _validate_rowset_closed(rowset_key: String, closed_rowset: Dictionary, row: Dictionary) -> Array[Dictionary]: var errs: Array[Dictionary] = [] if closed_rowset.has(rowset_key): errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [int(row.get("level_index", 0)), String(row.get("row_kind", "")), int(row.get("depth_band", -1))]}) return errs ## 锚行尾段:每 level 必有 (battle, band 0) 锚行与 Boss 行集 func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[Dictionary]: var errs: Array[Dictionary] = [] for L in anchor_ok.keys(): if not anchor_ok[L]: errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 (battle, band 0) 锚行" % L}) if not boss_ok.has(L): errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 Boss 行集(row_kind=boss 行 ≥1)" % L}) return errs # 深度带切法单点(唯一实现,禁第二份算法):band_keys = 该 (level_index, row_kind) 的 # depth_band 键升序数组;返回首个 ≥ D 的键(请求落带取偏难一侧)。 # D 高于全部键 → 返回 -1——resolver 侧 clamp 顶带+warning,跨表校验侧判「顶 clamp 消解」违例; # band_keys 空 → 同样返回 -1(resolver 随之返回空行集,交消费侧既有空行集 fatal 路径)。 static func band_of(band_keys: Array, D: int) -> int: for k in band_keys: if int(k) >= D: return int(k) return -1 # 进度键解析单点收口(全仓统走 # ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧), # 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。 var _resolver # R1 解析协作件(ConfigDB load_tables 装配段注入——resolver 文件与键零搬移) ## 带寻址行集解析委托(消费面 db.combat.enemy.resolve_rowset——D 维 band_of 落带语义归 ConfigDbResolver) func resolve_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]: return _resolver.resolve_enemy_rowset(L, D, row_kind)