From 8690230573f939b9c04207a50d1d277444d8ebcc Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 7 Oct 2026 02:05:30 +0800 Subject: [PATCH] =?UTF-8?q?T2=E7=BB=84B=C2=B7schema=E4=B8=8E=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E6=8D=A2=E4=BB=A3(=E6=95=8C=E4=BA=BA=E8=A1=A8?= =?UTF-8?q?=E8=A1=8C=E9=9B=86=E5=A4=9A=E8=A1=8C=E6=89=B9=E2=80=94=E2=80=94?= =?UTF-8?q?P1=E7=94=B2/P2=E7=94=B2=E8=90=BD=E7=A0=81):=20enemy=5Ftable.gd?= =?UTF-8?q?=20=E6=81=B0=E4=B8=80=E6=95=8C=E7=A7=8D=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E9=80=80=E5=BD=B9=E6=94=B9=E8=A1=8C=E9=9B=86=E6=9E=84=E6=88=90?= =?UTF-8?q?=E7=BA=AA=E5=BE=8B=E2=91=A1=E8=A1=8C=E5=BA=8F=E7=9B=B8=E9=82=BB?= =?UTF-8?q?=E6=A0=A1=E9=AA=8C(=E5=90=8C=E5=9D=90=E6=A0=87=E8=A1=8C?= =?UTF-8?q?=E4=B8=8D=E7=9B=B8=E9=82=BB=E5=8D=B3rowset=5Frule=E6=8A=A5?= =?UTF-8?q?=E9=94=99,=E7=B1=BB=E5=88=AB=E6=B2=BF=E7=94=A8=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E9=9D=A2=E4=B8=8D=E5=8F=98;=E5=90=8C=E6=95=8C?= =?UTF-8?q?=E7=A7=8D=E7=AC=AC=E4=BA=8C=E8=A1=8C=E4=BB=8Ddup=5Fkey=E6=8B=A6?= =?UTF-8?q?)+UNIT=5FCOUNT=5FMAX=E8=AF=AD=E4=B9=89=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=E6=94=B9=E5=8D=95=E7=A7=8D=E6=95=B0(=E5=8D=95=E8=A1=8C?= =?UTF-8?q?=E5=9F=9F[1,3]=E4=B8=8D=E5=8F=98,=E8=A1=8C=E9=9B=86=E6=80=BB?= =?UTF-8?q?=E6=95=B0=E2=89=A44=E5=BD=92spawn=E5=AE=88=E5=8D=AB=C2=B7?= =?UTF-8?q?=E9=AA=B0=E5=90=8E=E5=80=BC=E5=8A=A0=E8=BD=BD=E6=9C=9F=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E5=88=A4)+floor=E7=89=88resolve=5Frowset=E9=80=80?= =?UTF-8?q?=E5=BD=B9(=E5=8F=8C=E7=AE=97=E6=B3=95=E9=94=80=E5=8F=B7,?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=97=A5=E5=BF=97312)=EF=BC=9Bconfig=5Fdb=5F?= =?UTF-8?q?resolver.gd=20resolve=5Fenemy=5Frowset=E6=94=B9=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E8=A1=8C=E9=9B=86=E6=95=B0=E7=BB=84Array[Dictionary](?= =?UTF-8?q?ceil=E5=91=BD=E4=B8=AD=E5=9D=90=E6=A0=87=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E8=A1=8C,=E6=96=87=E4=BB=B6=E5=BA=8F=3D=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E5=BA=8F,=E7=A9=BA=E9=9B=86=3D=E7=A9=BA=E6=95=B0=E7=BB=84;=5Fr?= =?UTF-8?q?esolve=5Fprogress=E5=8A=A0return=5Fall=E5=8F=82=E6=95=B0board?= =?UTF-8?q?=5Fgen=E5=8D=95=E8=A1=8C=E9=80=9A=E9=81=93=E4=B8=8D=E5=8A=A8)?= =?UTF-8?q?=EF=BC=9Bconfig=5Fdb.gd=E9=97=A8=E9=9D=A2=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=EF=BC=9B=E5=85=A8=E4=BB=93=E6=B6=88=E8=B4=B9?= =?UTF-8?q?=E7=82=B9=E6=9C=BA=E6=A2=B0=E9=80=82=E9=85=8D=E5=8F=96=E9=A6=96?= =?UTF-8?q?=E8=A1=8C(T2=E8=A1=8C=E4=B8=BA=E9=9B=B6=E5=8F=98=E5=8C=96:spawn?= =?UTF-8?q?/orchestrator/narrator/smoke=E4=B8=83=E5=A4=84/config=5Fdb=5Fte?= =?UTF-8?q?st/skill=5Frhythm=5Fprobe/damage=5Fprobe=E4=B8=A4=E5=A4=84;floo?= =?UTF-8?q?r=E5=9B=9B=E8=B0=83=E7=94=A8=E7=82=B9=E8=BF=81ceil=E5=9D=90?= =?UTF-8?q?=E6=A0=87=E5=85=A8=E7=B2=BE=E7=A1=AE=E5=91=BD=E4=B8=AD=E9=9B=B6?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E5=B7=AE)=EF=BC=9B=E8=BD=BB=E9=97=B8=3D?= =?UTF-8?q?=E5=86=92=E7=83=9F805/0=E4=B8=89=E9=97=B8=E5=85=A8=E8=BF=87+run?= =?UTF-8?q?=5Fall=2017=E5=A5=97=E5=85=A8=E7=BB=BF=E2=80=94=E2=80=94?= =?UTF-8?q?=E9=9B=B6=E5=86=85=E5=AE=B9=E6=9C=9F=E6=96=AD=E8=A8=80=E9=80=90?= =?UTF-8?q?=E4=BD=8D=E9=9B=B6=E6=BC=82=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_orchestrator.gd | 9 ++++- game/core/config_db.gd | 4 +- game/core/config_db_resolver.gd | 15 +++++-- game/core/unit_container.gd | 7 +++- game/data/schemas/enemy_table.gd | 40 +++++++++---------- game/presentation/battle_narrator.gd | 10 +++-- game/smoke/damage_probe.gd | 4 +- game/smoke/skill_rhythm_probe.gd | 4 +- game/smoke/smoke_all.gd | 14 +++---- game/tests/unit/data_config/config_db_test.gd | 5 ++- 10 files changed, 68 insertions(+), 44 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index e8415804..fa0678b1 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -178,7 +178,14 @@ func begin_battle(enc: Dictionary) -> bool: # 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 - var erow_pre: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) + # 行集数组口径(2026-10-07 行集多行批——本卡 T2 机械适配取首行保持行为零变化:遭遇骰读首行 + # 数量区间 = 全行集骰域〔多行后首行区间即行集骰域,知情代价登记 敌人系统 v0.15〕; + # 每行各骰换代随批 T3) + var erowset_pre: Array = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) + if erowset_pre.is_empty(): + _fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)]) + return false + var erow_pre: Dictionary = erowset_pre[0] var roll_lo: int = int(erow_pre.get("unit_count", 1)) var roll_hi: int = int(erow_pre.get("unit_count_max", roll_lo)) var needs_count: bool = roll_hi > roll_lo diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 9df68de8..b6ace23c 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -186,8 +186,8 @@ func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolver.resolve_board_gen(L, D) -func resolve_enemy_rowset(L: int, D: int) -> Dictionary: - return _resolver.resolve_enemy_rowset(L, D) +func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]: + return _resolver.resolve_enemy_rowset(L, D) # 行集数组形态(2026-10-07 行集多行批——原单行 Dictionary 退役) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index 159af6c3..3ae06969 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -30,8 +30,11 @@ func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolve_progress(_db.board_gen.rows, L, D, "board_gen") -func resolve_enemy_rowset(L: int, D: int) -> Dictionary: - return _resolve_progress(_db.enemy.rows, L, D, "enemy") +func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]: + # 行集数组形态(2026-10-07 敌人表行集多行批——原单行 Dictionary 退役):ceil 命中 (l*,d*) 后 + # 返回该坐标全部行、按表文件行序(行序即契约——同坐标多行文件内相邻由 EnemyTable.validate 兜底)。 + # 空集表达 = 空数组(生产表下 ceil 命中坐标恒有行,空集仅表空时可达——validate 已拦)。 + return _resolve_progress(_db.enemy.rows, L, D, "enemy", true) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) @@ -47,7 +50,7 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: return {} -func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary: +func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all := false) -> Variant: if L < 1 or D < 0: _db._fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) return {} @@ -94,6 +97,12 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Diction _warn_dedup[dedup_key] = true var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" _db.push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) + if return_all: + var all_out: Array[Dictionary] = [] + for row in rows: + if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: + all_out.append(row) + return all_out for row in rows: if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: return row diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 7daa9049..46005ff3 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -44,11 +44,14 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: if _spawned: push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)") return false - var row: Dictionary = db.resolve_enemy_rowset(level_index, node_depth) - if row.is_empty(): + # 行集数组口径(2026-10-07 行集多行批——resolver 返回该坐标全部行;本卡 T2 先机械适配取首行 + # 保持行为零变化,行集逐行实例化换代随批 T3) + var rowset: Array = db.resolve_enemy_rowset(level_index, node_depth) + if rowset.is_empty(): push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth]) assert(false, "遭遇行悬空(R1 前置守卫)") return false + var row: Dictionary = rowset[0] var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk) var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp) if hero_hp > eff_max_hp or hero_hp <= 0: diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index a6b3b74f..3441f9b2 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -1,7 +1,9 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2) # Date: 2026-09-10 -# 数据配置 EnemyTable / HeroTable schema(敌人系统 回填值)。EnemyTable 键 (L,D,enemy_id),每行集恰一敌种; +# 数据配置 EnemyTable / HeroTable schema(敌人系统 回填值)。EnemyTable 键 (L,D,enemy_id),行集可多行混编 +# (行集构成纪律 = 敌人系统 v0.15:行集内 enemy_id 唯一〔dup_key 拦〕/ 同坐标多行文件内相邻〔行序即契约,本表 +# validate 兜底〕/ 行集实例总数 ≤4 守卫在 单位容器 spawn〔骰后值,加载期不可判〕); # HeroTable 单行全局(Rule 4 豁免)。依据 design/gdd/systems/战斗循环/敌人系统.md Schema 节。 # 敌人技能池批(2026-09-16 用户拍板,设计权威 proposal-skill-pool.md〔提案已吸收退役 git a6833b6a——现行事实源=敌人系统.md 技能池节〕 v1.0):pattern 行为环数组与 # heavy_multiplier 退役,改池模板 pool = [{skill_id, weight}](定义层——实例池快照与冷却状态在 单位容器)。 @@ -16,7 +18,9 @@ extends Resource const ARMOR_TYPES := ["light", "ward", "heavy"] # 实例化数量域(2026-10-02 修复批 T2 魔法数结构化——数量域 [1,3] 双落点收口单点: -# 本表 validate 与 单位容器 spawn 守卫共用;遭遇骰结果与行值 unit_count/unit_count_max 同域) +# 本表 validate 与 单位容器 spawn 守卫共用;遭遇骰结果与行值 unit_count/unit_count_max 同域。 +# 2026-10-07 行集多行批:MAX 语义 = **单种数**(单行实例数上限),行集实例总数 ≤4 另由 +# spawn 期守卫承担——骰后值加载期不可判,双闸分工见 敌人系统 v0.15 行集构成纪律③) const UNIT_COUNT_MIN := 1 const UNIT_COUNT_MAX := 3 @@ -27,7 +31,8 @@ func validate() -> Array: errs.append({"category": "empty_table", "message": "EnemyTable 为空"}) return errs var seen := {} - var rowset_species := {} + var last_rk := "" + var closed_rk := {} var anchor_ok := {} for row in rows: var L: int = int(row.get("level_index", 0)) @@ -105,11 +110,16 @@ func validate() -> Array: 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]}) - # 每行集恰一敌种(行集内 enemy_id 唯一) + # 行集构成纪律②(2026-10-07 行集多行批——行序即契约,敌人系统 v0.15):同坐标多行必须 + # 文件内相邻。迭代中记录上一行坐标;当前坐标曾出现过但 ≠ 上一行坐标 = 同坐标行被隔断,报错。 + # 原纪律「每行集恰一敌种」同批退役——同坐标多敌种混编合法;同坐标同敌种第二行仍由 + # dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。 var rk := "%d_%d" % [L, D] - if rowset_species.has(rk): - errs.append({"category": "rowset_rule", "message": "(%d,%d) 行集出现第二敌种(每行集恰一敌种违例)" % [L, D]}) - rowset_species[rk] = eid + if rk != last_rk: + if closed_rk.has(rk): + errs.append({"category": "rowset_rule", "message": "(%d,%d) 行集行序违例:同坐标多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, D]}) + closed_rk[rk] = true + last_rk = rk anchor_ok[L] = anchor_ok.get(L, false) or D == 0 for L in anchor_ok.keys(): if not anchor_ok[L]: @@ -117,16 +127,6 @@ func validate() -> Array: return errs -# R1:进度键 floor + 行集恰一敌种(实体键无歧义,悬空致命由调用方处理) -func resolve_rowset(L: int, D: int) -> Dictionary: - var best: Dictionary[String, Variant] = {} - var best_l := -1 - var best_d := -1 - for row in rows: - var rl: int = int(row.get("level_index", 0)) - var rd: int = int(row.get("node_depth", 0)) - if rl <= L and rd <= D and (rl > best_l or (rl == best_l and rd > best_d)): - best.assign(row) - best_l = rl - best_d = rd - return best +# 进度键解析单点收口(2026-10-07 行集多行批 P2 甲——原 floor 语义 resolve_rowset 退役,全仓统走 +# ConfigDbResolver.resolve_enemy_rowset ceil 版行集数组:2026-09-18 用户拍板 ceil 保证偏难, +# 双算法并存至本批销号〔工作日志 312 待裁项〕;原冒烟四调用点随批迁移,坐标全精确命中零行为差)。 diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index 32e612b6..fbfd3e89 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -67,13 +67,17 @@ static func phase_text(round_index: int, battle_result: String, waiting: bool, b return "第 %d 轮 · 挖掘中%s" % [round_index, boss_tag] -# Boss 名解析(runner Boss 节点 → enemy 表行名;缺省遗迹守卫) +# Boss 名解析(runner Boss 节点 → enemy 表行名;缺省遗迹守卫)。 +# 行集多行后取**首行** display_name(2026-10-07 行集多行批——行序即契约的呈现面后果, +# Boss 行集混编护卫时 Boss 名显示可能为护卫名,知情代价登记 敌人系统 v0.15 / 出口报告; +# 零内容期每坐标恰一行,行为零变化) static func boss_name(runner, db) -> String: if runner != null: var node: Dictionary = runner.current_node() if String(node.get("type", "")) == "boss": - var row: Dictionary = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) - return String(row.get("display_name", "")) + var rowset: Array = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) + if not rowset.is_empty(): + return String(rowset[0].get("display_name", "")) return "遗迹守卫" diff --git a/game/smoke/damage_probe.gd b/game/smoke/damage_probe.gd index 8bf58a11..56b2e4df 100644 --- a/game/smoke/damage_probe.gd +++ b/game/smoke/damage_probe.gd @@ -267,7 +267,7 @@ func _encounter_prediction(db, seed_val: int) -> Dictionary: if t != "battle" and t != "boss": continue var depth := int(node.get("node_depth", -1)) - var row: Dictionary = db.resolve_enemy_rowset(1, depth) + var row: Dictionary = db.resolve_enemy_rowset(1, depth)[0] # 行集首行(行集多行批 T2 机械适配) var lo := int(row.get("unit_count", 1)) var hi := int(row.get("unit_count_max", lo)) var rolled := lo @@ -418,7 +418,7 @@ func _scan_spawns(db, seed0: int, n_seeds: int) -> Dictionary: bad += 1 continue var n: int = settlement.presentation_snapshot().enemies.size() - var row: Dictionary = db.resolve_enemy_rowset(1, int(depth)) + var row: Dictionary = db.resolve_enemy_rowset(1, int(depth))[0] # 行集首行(同上) var lo := int(row.get("unit_count", 1)) var hi := int(row.get("unit_count_max", lo)) if n < 1 or n < lo or n > hi: diff --git a/game/smoke/skill_rhythm_probe.gd b/game/smoke/skill_rhythm_probe.gd index fab457c1..e0378012 100644 --- a/game/smoke/skill_rhythm_probe.gd +++ b/game/smoke/skill_rhythm_probe.gd @@ -13,8 +13,8 @@ func _initialize() -> void: var db = load("res://core/config_db.gd").new() db.load_tables("res://data/tables", false) - # ① 表侧 - var row = db.resolve_enemy_rowset(1, 1) + # ① 表侧(2026-10-07 行集多行批 T2 机械适配——行集数组取首行,零内容期行为零变化) + var row = db.resolve_enemy_rowset(1, 1)[0] print("[1] row(1,1) display=", row.get("display_name"), " unit_count=", row.get("unit_count"), " pool=", row.get("pool")) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 353c03e0..3cbc7597 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -1994,7 +1994,7 @@ func _smoke_difficulty_table() -> void: var enemy_system = load("res://core/unit_container.gd").new(_db) check(enemy_system.spawn(1, 0, int(_db.hero.max_hp)), "spawn 受理") var unit = enemy_system.all_enemies()[0] - var d0: Dictionary = _db.resolve_enemy_rowset(1, 0) + var d0: Dictionary = _db.resolve_enemy_rowset(1, 0)[0] # 行集数组口径取首行(零内容期每坐标恰一行,T2 机械适配) check(int(unit.hp) == int(d0.hp) and int(unit.atk) == int(d0.atk), "空表零修饰(hp=%d atk=%d = 行值 %d/%d)" % [int(unit.hp), int(unit.atk), int(d0.hp), int(d0.atk)]) # ② 修饰行运行时注入(debug 通道——R3 override 同族):套乘生效于 hp/atk _db.difficulty.rows.append({"level_index": 1, "node_depth": 0, "hp_mult": 1.5, "atk_mult": 2.0}) @@ -2308,7 +2308,7 @@ func _smoke_encounter_roll() -> void: scan_bad += 1 continue var n: int = settlement_scan.presentation_snapshot().enemies.size() - var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v)) + var erow2: Dictionary = _db.resolve_enemy_rowset(1, int(depth_v))[0] # 行集首行(T2 机械适配) var lo2: int = int(erow2.get("unit_count", 1)) var hi2: int = int(erow2.get("unit_count_max", lo2)) if n < 1 or n < lo2 or n > hi2: @@ -3236,7 +3236,7 @@ func _smoke_skill_pool_death_void() -> void: for s in range(1, 400): var rg = load("res://core/rng_service.gd").new() rg.set_master_seed(s) - var erow: Dictionary = _db.resolve_enemy_rowset(1, 2) + var erow: Dictionary = _db.resolve_enemy_rowset(1, 2)[0] # 行集首行(T2 机械适配) var entries: Array = [] for entry in erow.pool: entries.append({"skill_id": String(entry.skill_id), "weight": int(entry.weight)}) @@ -3531,9 +3531,9 @@ func _smoke_level_graph() -> void: if not bg_keys.has(key) or not en_keys.has(key): cross_ok = false check(cross_ok, "跨表行完整:L2 全部战斗/Boss 节点在 board_gen + enemy 精确行(7 节点)") - var l2_spider: Dictionary = _db.enemy.resolve_rowset(2, 4) + var l2_spider: Dictionary = _db.resolve_enemy_rowset(2, 4)[0] # floor 版退役改 ceil 行集首行(坐标精确命中零行为差——P2 甲) check(String(l2_spider.get("enemy_id", "")) == "giant_spider", "巨蜘蛛首遇行集 (2,4) = giant_spider(固定 2 只——首遇教学位不加骰)") - var l2_boss: Dictionary = _db.enemy.resolve_rowset(2, 9) + var l2_boss: Dictionary = _db.resolve_enemy_rowset(2, 9)[0] # 同上(P2 甲) check(String(l2_boss.get("enemy_id", "")) == "drider" and String(l2_boss.get("armor_type", "")) == "ward", "蛛化卓尔精灵 Boss 行集 (2,9) = drider(魔法甲——护甲类型重配批 2026-09-18:关卡 2 唯一非轻甲;商店房批重编号 (2,8)→(2,9))") # ---- 第三关内容批(2026-09-20)L3 灰矮人矿城断言段 ---- @@ -3563,10 +3563,10 @@ func _smoke_level_graph() -> void: cross_ok3 = false check(cross_ok3, "跨表行完整:L3 全部战斗/Boss 节点在 board_gen + enemy 精确行(7 节点)") # ⑩ 定点:L3 首遇行集与 Boss 行集(克制面重轻错开——重甲灰矮人首次登台) - var l3_lead: Dictionary = _db.enemy.resolve_rowset(3, 0) + var l3_lead: Dictionary = _db.resolve_enemy_rowset(3, 0)[0] # 同上(P2 甲) check(String(l3_lead.get("enemy_id", "")) == "duergar" and String(l3_lead.get("armor_type", "")) == "heavy", "灰矮人首遇行集 (3,0) = duergar(重甲首次登台——固定 1 只,首遇教学位不加骰)") - var l3_king: Dictionary = _db.enemy.resolve_rowset(3, 9) + var l3_king: Dictionary = _db.resolve_enemy_rowset(3, 9)[0] # 同上(P2 甲) check(String(l3_king.get("enemy_id", "")) == "duergar_king" and String(l3_king.get("armor_type", "")) == "heavy", "灰矮人石君王 Boss 行集 (3,9) = duergar_king(重甲收束「轻甲→魔法甲→重甲」教学梯度;商店房批重编号 (3,8)→(3,9))") # ⑪ L3 成长代理档首遇绿档:模拟 GP 商店成长后的入关参数(atk_pct=200 / hp_pct=100)· 完美档多种子首遇战稳过 diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index 53cd86fd..4533217f 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -56,8 +56,9 @@ func _test_progress_resolution(db) -> void: and int(got.get("node_depth", -99)) == int(probe_row.node_depth) and String(got.get("board_type", "")) == String(probe_row.get("board_type", ""))) - # enemy 行集同通道:同 (L,D) 精确行存在(关卡节点地图 前置——battle 节点双表精确行契约) - var en_got: Dictionary = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth)) + # enemy 行集同通道:同 (L,D) 行集数组非空(关卡节点地图 前置——battle 节点双表精确行集契约; + # 2026-10-07 行集多行批 T2 机械适配——返回形态单行改行集数组,精确性断言换代随批 T4) + var en_got: Array = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth)) Kit.check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty()) # 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)