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

338 lines
18 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:逐 (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]
# 行类封闭枚举:表内允许出现的行类(battle/boss 对齐关卡节点 type 词表,elite=精英行)
const ROW_KINDS := ["battle", "boss", "elite"]
# 开战寻址可读的行类——elite 接线前不在此列(敌人系统 v1.9 暂不接线;resolver 行类门单点消费)
const ENCOUNTER_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_elite_fields(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_elite_refs())
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, elite})" % [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 no_errs: Array[Dictionary] = []
if String(row.get("row_kind", "")) == "elite":
return no_errs # elite 行不带 pool 键(pool_extra 承载附加技——条目域校验随 _validate_elite_fields)
var errs: Array[Dictionary] = []
var pool: Array[Variant] = row.get("pool", [])
if pool.is_empty():
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
errs.append_array(_validate_pool_entries(pool, eid, "pool"))
return errs
## 池条目域共用校验(pool 与 pool_extra 共用——权重上限 9999 不出现第二份,契约 3 单点);
## field 承载报错定位段("pool"=既有文案逐字不变,"pool_extra"=附加技清单定位)。
func _validate_pool_entries(entries: Array, eid: String, field: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var pool_ids := {}
for entry_v in entries:
if not (entry_v is Dictionary):
errs.append({"category": "domain", "message": "%s %s 条目非字典: %s" % [eid, field, 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 %s 条目 skill_id 空" % [eid, field]})
elif pool_ids.has(sid):
errs.append({"category": "dup_key", "message": "%s %s 内 skill_id 重复: %s" % [eid, field, 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 %s 权重越域 (0,9999]: %d" % [eid, field, w]})
return errs
## 精英行字段互锁(敌人系统 v1.9 :281/:282/:283):elite 行必备 base_ref/pool_extra、禁 pool、
## enemy_id 不与 base_ref 同名;非 elite 行禁带 base_ref/pool_extra。base_ref 引用存在性归 _validate_elite_refs。
func _validate_elite_fields(row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
if String(row.get("row_kind", "")) == "elite":
var base_ref := String(row.get("base_ref", ""))
if base_ref == "":
errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 缺或空(基础敌种引用必带)" % eid})
if base_ref == eid:
errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 与自身 enemy_id 同名(禁)" % eid})
if not row.has("pool_extra") or (row.get("pool_extra") as Array).is_empty():
errs.append({"category": "cross_field", "message": "%s elite 行 pool_extra 缺键或空(附加技清单非空必带)" % eid})
if row.has("pool"):
errs.append({"category": "cross_field", "message": "%s elite 行带 pool 键(禁——池走继承+附加)" % eid})
if row.has("pool_extra") and (row.get("pool_extra") as Array).size() > 0:
errs.append_array(_validate_pool_entries(row.get("pool_extra"), eid, "pool_extra"))
else:
if row.has("base_ref"):
errs.append({"category": "cross_field", "message": "%s 非 elite 行带 base_ref 键(禁——基础敌种引用为 elite 行专属)" % eid})
if row.has("pool_extra"):
errs.append({"category": "cross_field", "message": "%s 非 elite 行带 pool_extra 键(禁——附加技清单为 elite 行专属)" % eid})
return errs
## 精英行跨行引用校验(敌人系统 v1.9 :148/:282):base_ref 指向同关 battle 行类敌种(缺/空已由
## _validate_elite_fields 报——此处跳过);指向 elite 行判别同门 _min_band_row;附加技与基础池
## skill_id 交集非空报 dup_key(敌人系统.md:149 附加技与基础池重复拒绝)。
func _validate_elite_refs() -> Array[Dictionary]:
var errs: Array[Dictionary] = []
for row in rows:
if String(row.get("row_kind", "")) != "elite":
continue
var base_ref := String(row.get("base_ref", ""))
if base_ref == "":
continue
var level := int(row.get("level_index", 0))
var eid := String(row.get("enemy_id", ""))
var base := _min_band_row(level, base_ref, "battle")
if base.is_empty():
if not _min_band_row(level, base_ref, "elite").is_empty():
errs.append({"category": "dangling_base", "message": "%s base_ref 指向 elite 行: %s(基础敌种须为 battle 行类)" % [eid, base_ref]})
else:
errs.append({"category": "dangling_base", "message": "%s base_ref 同关 battle 行类无此敌种: %s" % [eid, base_ref]})
continue
errs.append_array(_extra_overlap_errs(row, base, eid))
return errs
## 附加技与基础池重叠判(_validate_elite_refs 交集段提出——分支预算内聚)
func _extra_overlap_errs(row: Dictionary, base_row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var base_ids := {}
for entry_v in base_row.get("pool", []):
if entry_v is Dictionary:
base_ids[String(entry_v.get("skill_id", ""))] = true
for entry_v in row.get("pool_extra", []):
if entry_v is Dictionary:
var sid := String(entry_v.get("skill_id", ""))
if sid != "" and base_ids.has(sid):
errs.append({"category": "dup_key", "message": "%s pool_extra 与基础池 skill_id 重复: %s" % [eid, sid]})
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
# 基础行查找单点(按 (关, 敌种, 行类) 取深度带最小行——精英有效池与「指向 elite 判别」共用,
# 禁第二份查找算法):无匹配返回空字典。行字段一律 row.get 读取(夹具行可缺键)。
func _min_band_row(level: int, eid: String, kind: String) -> Dictionary:
var best := {}
var best_band := -1
for row in rows:
if int(row.get("level_index", -1)) != level or String(row.get("row_kind", "")) != kind \
or String(row.get("enemy_id", "")) != eid:
continue
var band := int(row.get("depth_band", -1))
if best.is_empty() or band < best_band:
best = row
best_band = band
return best
## 精英有效池(按需现算、零缓存零写回——敌人系统.md:149 继承不复制):非 elite 行 = 自身 pool
## 字典条目副本;elite 行 = 基础行(同关 battle 行类基础敌种深度带最小行)pool 条目 + 本行
## pool_extra 条目(均只取 is Dictionary 条目并 duplicate——返回值改写不回灌表内行)。
func effective_pool(row: Dictionary) -> Array[Dictionary]:
var out: Array[Dictionary] = []
if String(row.get("row_kind", "")) != "elite":
for entry_v in row.get("pool", []):
if entry_v is Dictionary:
out.append((entry_v as Dictionary).duplicate())
return out
var base := _min_band_row(int(row.get("level_index", 0)), String(row.get("base_ref", "")), "battle")
if not base.is_empty():
for entry_v in base.get("pool", []):
if entry_v is Dictionary:
out.append((entry_v as Dictionary).duplicate())
for entry_v in row.get("pool_extra", []):
if entry_v is Dictionary:
out.append((entry_v as Dictionary).duplicate())
return out
# 深度带切法单点(唯一实现,禁第二份算法):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 装配段经 bind_resolver 注入——resolver 文件与键零搬移)
# 解析协作件挂载口(ConfigDB 装配段公开面〔5.1 封装修复——直写 _resolver 改经此口〕)
func bind_resolver(resolver: ConfigDbResolver) -> void:
_resolver = 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)