Files
gplan-2/game/tests/unit/data_config/elite_row_test.gd
T

306 lines
17 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.
# 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)