# 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: # 判据出处: 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: # 判据出处: 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: # 判据出处: 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: # 判据出处: 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: # 判据出处: 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: # 判据出处: 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)