306 lines
17 KiB
GDScript
306 lines
17 KiB
GDScript
# tests/unit/data_config/elite_row_test.gd — 精英行单元套(精英专属池批)
|
||
# 断言面 = 精英行加载期校验(字段互锁/引用校验/错误类别)·有效池合并(基础池取深度带最小行
|
||
# +附加技)·继承不复制(改基础池传到精英、改写返回不回灌)·合并池过池构成纪律(跨表件)·
|
||
# 暂不接线(寻址行类门只放 battle/boss,elite 行静置合法)·生产表 goblin_elite 行(随 T3 落表追加)。
|
||
# 断言从公开入口进出:EnemyTable.validate()/effective_pool()/validate_skill_pool_links()/
|
||
# resolve_enemy_rowset()/ConfigDB.load_tables()。夹具一律 rows.assign([...])(定型数组直赋会触发
|
||
# 脚本错误静默中断——config_db_test 同款坑注)。
|
||
# 运行:godot --headless --path game --script res://tests/unit/data_config/elite_row_test.gd(expect 0)
|
||
extends SceneTree
|
||
|
||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||
const EnemyTableScript := preload("res://data/schemas/enemy_table.gd")
|
||
const ValidatorScript := preload("res://core/config_db_cross_validator.gd")
|
||
const SkillTableScript := preload("res://data/schemas/skill_table.gd")
|
||
const StatusTableScript := preload("res://data/schemas/status_effect_table.gd")
|
||
const ResolverScript := preload("res://core/config_db_resolver.gd")
|
||
|
||
var _diags: Array[String] = [] # T2.4 夹具 resolver 诊断捕获
|
||
|
||
|
||
func _init() -> void:
|
||
_test_elite_validation_rejects()
|
||
_test_effective_pool_merge()
|
||
_test_base_pool_change_propagates()
|
||
_test_merged_pool_composition()
|
||
_test_elite_unreachable_by_encounter()
|
||
_test_production_goblin_elite()
|
||
quit(Kit.finish("elite_row_test", "PASS — 精英行校验/有效池/继承不复制/合并池纪律/暂不接线/生产表落行 全过"))
|
||
|
||
|
||
# 行构造助手(照 config_db_test mk_row 写全字段;elite 行形态=去掉 pool、加 base_ref/pool_extra)
|
||
func _mk_row(l: int, kind: String, band: int, eid: String) -> Dictionary:
|
||
return {"level_index": l, "row_kind": kind, "depth_band": band, "enemy_id": eid, "display_name": eid + "名",
|
||
"unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]}
|
||
|
||
|
||
func _mk_elite_row(l: int, band: int, eid: String, base_ref: String, extra: Array) -> Dictionary:
|
||
return {"level_index": l, "row_kind": "elite", "depth_band": band, "enemy_id": eid, "display_name": eid + "名",
|
||
"unit_count": 1, "hp": 20, "atk": 2, "base_ref": base_ref, "pool_extra": extra}
|
||
|
||
|
||
# 合法夹具(三行——battle 锚/boss/elite 引用同关 battle 敌种)
|
||
func _legal_rows() -> Array:
|
||
var goblin := _mk_row(1, "battle", 0, "goblin")
|
||
goblin["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||
return [goblin, _mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}])]
|
||
|
||
|
||
# category 计数(错误列表按类别筛)
|
||
func _count_cat(errs: Array, cat: String) -> int:
|
||
var n := 0
|
||
for e in errs:
|
||
if String((e as Dictionary).get("category", "")) == cat:
|
||
n += 1
|
||
return n
|
||
|
||
|
||
# ---- R1:elite 行类合法与字段互锁、引用校验(合法零错+十一违例各自「含」预期类别)----
|
||
func _test_elite_validation_rejects() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:325 elite 行字段互锁与基础引用违例加载期返回错误
|
||
var t_ok = EnemyTableScript.new()
|
||
t_ok.rows.assign(_legal_rows())
|
||
var errs: Array = t_ok.validate()
|
||
Kit.check("合法夹具 validate 零错(battle 锚+boss+elite 引用在位——实得 %s)" % str(errs), errs.is_empty())
|
||
# ① elite 删 base_ref → cross_field
|
||
var rows1 := _legal_rows()
|
||
rows1[2].erase("base_ref")
|
||
Kit.check("① elite 删 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows1), "cross_field") >= 1)
|
||
# ② base_ref 与自身同名 → cross_field
|
||
var rows2 := _legal_rows()
|
||
rows2[2]["base_ref"] = "goblin_elite"
|
||
Kit.check("② base_ref 与 enemy_id 同名报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows2), "cross_field") >= 1)
|
||
# ③ pool_extra 为空数组 → cross_field
|
||
var rows3 := _legal_rows()
|
||
rows3[2]["pool_extra"] = []
|
||
Kit.check("③ pool_extra 空数组报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows3), "cross_field") >= 1)
|
||
# ④ elite 删 pool_extra 键 → cross_field
|
||
var rows4 := _legal_rows()
|
||
rows4[2].erase("pool_extra")
|
||
Kit.check("④ elite 删 pool_extra 键报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows4), "cross_field") >= 1)
|
||
# ⑤ elite 加 pool → cross_field
|
||
var rows5 := _legal_rows()
|
||
rows5[2]["pool"] = [{"skill_id": "attack", "weight": 9}]
|
||
Kit.check("⑤ elite 带 pool 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows5), "cross_field") >= 1)
|
||
# ⑥ battle 行加 base_ref → cross_field
|
||
var rows6 := _legal_rows()
|
||
rows6[0]["base_ref"] = "ogre_chief"
|
||
Kit.check("⑥ battle 行带 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows6), "cross_field") >= 1)
|
||
# ⑦ battle 行加 pool_extra → cross_field
|
||
var rows7 := _legal_rows()
|
||
rows7[0]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}]
|
||
Kit.check("⑦ battle 行带 pool_extra 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows7), "cross_field") >= 1)
|
||
# ⑧ base_ref="hobgoblin"(同关无此 battle 敌种)→ dangling_base
|
||
var rows8 := _legal_rows()
|
||
rows8[2]["base_ref"] = "hobgoblin"
|
||
Kit.check("⑧ base_ref 悬空报 dangling_base〔精英批 V1〕", _count_cat(_validate_rows(rows8), "dangling_base") >= 1)
|
||
# ⑨ 再加 elite2 行、base_ref 改指它 → dangling_base 且 message 含「elite」
|
||
var rows9 := _legal_rows()
|
||
rows9.append(_mk_elite_row(1, 0, "goblin_elite2", "goblin", [{"skill_id": "heavy", "weight": 2}]))
|
||
rows9[2]["base_ref"] = "goblin_elite2"
|
||
var errs9: Array = _validate_rows(rows9)
|
||
var has_elite_base := false
|
||
for e in errs9:
|
||
if String((e as Dictionary).get("category", "")) == "dangling_base" \
|
||
and String((e as Dictionary).get("message", "")).contains("elite"):
|
||
has_elite_base = true
|
||
Kit.check("⑨ base_ref 指向 elite 行报 dangling_base 且 message 含 elite〔精英批 V1〕", has_elite_base)
|
||
# ⑩ pool_extra 含基础池已有 lunge → dup_key
|
||
var rows10 := _legal_rows()
|
||
rows10[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}, {"skill_id": "lunge", "weight": 1}]
|
||
Kit.check("⑩ 附加技与基础池重复报 dup_key〔精英批 V1〕", _count_cat(_validate_rows(rows10), "dup_key") >= 1)
|
||
# ⑪ pool_extra 条目权重 0 → domain 且 message 含 pool_extra
|
||
var rows11 := _legal_rows()
|
||
rows11[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 0}]
|
||
var errs11: Array = _validate_rows(rows11)
|
||
var has_extra_domain := false
|
||
for e in errs11:
|
||
if String((e as Dictionary).get("category", "")) == "domain" \
|
||
and String((e as Dictionary).get("message", "")).contains("pool_extra"):
|
||
has_extra_domain = true
|
||
Kit.check("⑪ pool_extra 权重越域报 domain 且定位含 pool_extra〔精英批 V1〕", has_extra_domain)
|
||
|
||
|
||
func _validate_rows(rows: Array) -> Array:
|
||
var t = EnemyTableScript.new()
|
||
t.rows.assign(rows)
|
||
return t.validate()
|
||
|
||
|
||
# ---- R2:有效池=基础池+附加技,基础池取深度带最小行(深带行在前防取首行假绿)----
|
||
func _test_effective_pool_merge() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 精英有效池=同关战斗行类基础敌种深度带最小行的池+附加技
|
||
var rows := [
|
||
_mk_row(1, "battle", 3, "goblin"), # 深带行在前(取首行的错误实现必红)
|
||
_mk_row(1, "battle", 0, "goblin"),
|
||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||
]
|
||
rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}]
|
||
rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||
var t = EnemyTableScript.new()
|
||
t.rows.assign(rows)
|
||
var got := {}
|
||
for e in t.effective_pool(rows[3]):
|
||
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||
Kit.check("精英有效池=浅带基础池+附加技({attack9,lunge1,heavy2}——非深带行池)〔精英批 V2〕",
|
||
got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 3)
|
||
var got_b0 := {}
|
||
for e in t.effective_pool(rows[1]):
|
||
got_b0[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||
Kit.check("battle 行有效池=自身 pool(读法等价面——{attack9,lunge1})〔精英批 V2〕",
|
||
got_b0.get("attack", 0) == 9 and got_b0.get("lunge", 0) == 1 and got_b0.size() == 2)
|
||
|
||
|
||
# ---- R3:继承不复制——基础池改表传到精英、改写返回数组不回灌 ----
|
||
func _test_base_pool_change_propagates() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 继承不复制——基础池调参自动传到精英
|
||
var rows := [
|
||
_mk_row(1, "battle", 3, "goblin"),
|
||
_mk_row(1, "battle", 0, "goblin"),
|
||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||
]
|
||
rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}]
|
||
rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||
var t = EnemyTableScript.new()
|
||
t.rows.assign(rows)
|
||
var first: Array = t.effective_pool(rows[3])
|
||
for e in first:
|
||
if String((e as Dictionary).get("skill_id", "")) == "attack":
|
||
e["weight"] = 99 # 改写返回数组条目——表内行不得被回灌
|
||
rows[1]["pool"][0]["weight"] = 7 # 基础行(depth_band==0)attack 权重改 7
|
||
var got := {}
|
||
for e in t.effective_pool(rows[3]):
|
||
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||
Kit.check("基础池改权重 7 传到精英有效池(按需现算零缓存)〔精英批 V3〕", got.get("attack", 0) == 7)
|
||
Kit.check("改写返回数组不回灌表内行(基础行 attack 仍=7)〔精英批 V3〕",
|
||
int(rows[1]["pool"][0]["weight"]) == 7)
|
||
|
||
|
||
# ---- R4:合并池过池构成纪律(跨表件校验对象=合并后的池)----
|
||
func _test_merged_pool_composition() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:150 池构成纪律校验对象是合并后的池
|
||
var skills := SkillTableScript.new()
|
||
skills.rows.assign([
|
||
{"skill_id": "attack", "type": "basic"},
|
||
{"skill_id": "lunge", "type": "damage"},
|
||
{"skill_id": "heavy", "type": "damage"},
|
||
{"skill_id": "harden", "type": "buff"},
|
||
{"skill_id": "charge", "type": "buff"},
|
||
])
|
||
var status := StatusTableScript.new()
|
||
status.rows.assign([]) # 空状态表——buff 无 buff_ref 可解析时 cast_side 不报(cross_validator :179-180)
|
||
# 合规精英:基础 attack/lunge+附加 heavy → 恰一普攻/增益≤1/无条件特殊≥1 全合规
|
||
var enemy_ok = EnemyTableScript.new()
|
||
enemy_ok.rows.assign([
|
||
_mk_row(1, "battle", 0, "goblin"),
|
||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||
])
|
||
enemy_ok.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||
enemy_ok.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池(纪律对全行生效——本组只数精英违规)
|
||
var errs_ok: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_ok, status)
|
||
Kit.check("合规精英合并池 pool_composition=0(实得 %s)" % str(errs_ok), _count_cat(errs_ok, "pool_composition") == 0)
|
||
# 附加第二普攻:基础 attack+附加 attack → basic=2 → pool_composition=1 且 message 含 goblin_elite
|
||
var enemy_dup_basic = EnemyTableScript.new()
|
||
enemy_dup_basic.rows.assign([
|
||
_mk_row(1, "battle", 0, "goblin"),
|
||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "attack", "weight": 2}]),
|
||
])
|
||
enemy_dup_basic.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}]
|
||
enemy_dup_basic.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池
|
||
var errs_dup: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_basic, status)
|
||
Kit.check("附加第二普攻报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕",
|
||
_count_cat(errs_dup, "pool_composition") == 1 and _msgs(errs_dup).any(func(m: String) -> bool: return m.contains("goblin_elite")))
|
||
# 附加第二增益:基础 attack/lunge/harden+附加 charge → buff=2 → pool_composition=1
|
||
var enemy_dup_buff = EnemyTableScript.new()
|
||
enemy_dup_buff.rows.assign([
|
||
_mk_row(1, "battle", 0, "goblin"),
|
||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "charge", "weight": 2}]),
|
||
])
|
||
enemy_dup_buff.rows[0]["pool"] = [
|
||
{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}, {"skill_id": "harden", "weight": 2}]
|
||
enemy_dup_buff.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池
|
||
var errs_buff: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_buff, status)
|
||
Kit.check("合并后增益超 1 报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕",
|
||
_count_cat(errs_buff, "pool_composition") == 1 and _msgs(errs_buff).any(func(m: String) -> bool: return m.contains("goblin_elite")))
|
||
|
||
|
||
func _msgs(errs: Array) -> Array[String]:
|
||
var out: Array[String] = []
|
||
for e in errs:
|
||
out.append(String((e as Dictionary).get("message", "")))
|
||
return out
|
||
|
||
|
||
# ---- R5:暂不接线——表里有 elite 行时寻址拿不到它(行类门改动特有判据)----
|
||
func _test_elite_unreachable_by_encounter() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:151 精英行接线前不会被任何节点读到
|
||
_diags.clear()
|
||
var resolver = ResolverScript.new(
|
||
func(level, category, message) -> void:
|
||
_diags.append("%s/%s/%s" % [str(level), str(category), str(message)]),
|
||
func() -> Array: return [],
|
||
func() -> Array: return [
|
||
_mk_row(1, "battle", 0, "goblin"),
|
||
_mk_row(1, "boss", 9, "ogre_chief"),
|
||
_mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]),
|
||
],
|
||
func(): return null,
|
||
)
|
||
# ① 请求 elite → 空数组且诊断含 fatal 与 request_oob(只改枚举不改行类门时此请求会返回 goblin_elite——判据绑行类门改动)
|
||
var hit: Array = resolver.resolve_enemy_rowset(1, 0, "elite")
|
||
var has_fatal_oob := false
|
||
for d in _diags:
|
||
if d.contains("fatal") and d.contains("request_oob"):
|
||
has_fatal_oob = true
|
||
Kit.check("请求 elite 返回空且诊断含 fatal/request_oob〔精英批 V5〕", hit.is_empty() and has_fatal_oob)
|
||
# ② 同夹具 battle/boss 查询返回行中 elite 计数=0(非空前置:合计行数>0)
|
||
var battle_rows: Array = resolver.resolve_enemy_rowset(1, 0, "battle")
|
||
var boss_rows: Array = resolver.resolve_enemy_rowset(1, 9, "boss")
|
||
var elite_n := 0
|
||
for r in battle_rows + boss_rows:
|
||
if String((r as Dictionary).get("row_kind", "")) == "elite":
|
||
elite_n += 1
|
||
Kit.check("battle/boss 查询合计>0 且零 elite 行(非空前置在判据内)〔精英批 V5〕",
|
||
battle_rows.size() + boss_rows.size() > 0 and elite_n == 0)
|
||
|
||
|
||
# ---- R6:生产表地精精英落表(真实加载零错+行值逐位+有效池字典)----
|
||
func _test_production_goblin_elite() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:147 精英=敌人表行类为 elite 的独立一行
|
||
var db = load("res://core/config_db.gd").new()
|
||
var ok: bool = db.load_tables("res://data/tables", true)
|
||
Kit.check("生产表 ConfigDB.load_tables 装载(is_ready=%s)〔精英批 V6〕" % str(db.is_ready), ok and db.is_ready)
|
||
Kit.check("生产表加载 error 级诊断计数=0(实得 %d)〔精英批 V6〕" % db.diagnostics_count("error"),
|
||
db.diagnostics_count("error") == 0)
|
||
var elite_rows: Array = []
|
||
for row in db.combat.enemy.rows:
|
||
if String(row.get("enemy_id", "")) == "goblin_elite":
|
||
elite_rows.append(row)
|
||
Kit.check("goblin_elite 行恰 1 条(实得 %d)〔精英批 V6〕" % elite_rows.size(), elite_rows.size() == 1)
|
||
if elite_rows.is_empty():
|
||
return
|
||
var r: Dictionary = elite_rows[0]
|
||
var checks := {
|
||
"level_index": 1, "row_kind": "elite", "depth_band": 0,
|
||
"display_name": "精英·地精", "hp": 20, "atk": 2, "atk_min": 1, "atk_max": 3,
|
||
"armor_class": 2, "armor_type": "light", "unit_count": 1, "base_ref": "goblin",
|
||
}
|
||
var bad: Array[String] = []
|
||
for k in checks:
|
||
if r.get(k) != checks[k]:
|
||
bad.append("%s=%s≠%s" % [k, str(r.get(k)), str(checks[k])])
|
||
Kit.check("行值 12 字段逐位相等(失配项 %s)〔精英批 V6〕" % [bad if not bad.is_empty() else "无"], bad.is_empty())
|
||
Kit.check("无 unit_count_max 键、无 pool 键(精英行形态面)〔精英批 V6〕",
|
||
not r.has("unit_count_max") and not r.has("pool"))
|
||
var got := {}
|
||
for e in db.combat.enemy.effective_pool(r):
|
||
got[String(e.get("skill_id", ""))] = int(e.get("weight", 0))
|
||
Kit.check("有效池字典={attack9,lunge1,desperate_guard1,heavy2}(实得 %s)〔精英批 V6〕" % str(got),
|
||
got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("desperate_guard", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 4)
|