From 07f3eb55e32300f43b0f9463248586b603e8679d Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 22 Oct 2026 04:17:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=95=8C=E4=BA=BA=E8=A1=A8=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6=E5=B8=A6):=20=E9=81=AD=E9=81=87=E8=A1=8C=E9=9B=86?= =?UTF-8?q?=E5=AF=BB=E5=9D=80=E6=94=B9=E4=B8=BA=20(=E5=85=B3=E5=8D=A1,=20r?= =?UTF-8?q?ow=5Fkind,=20=E6=B7=B1=E5=BA=A6=E5=B8=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 敌人行集不再按 (level_index, node_depth) 精确点寻址,改为 (level_index, row_kind, depth_band): 带切法单点 EnemyTable.band_of 取首个不小于请求深度的带(偏难一侧),高于顶带返回 -1 交 resolver 取顶带并发 resolve_depth_clamp(四元组去重不变);resolver 与 ConfigDb 门面同步 加 row_kind 第三参;unit_container.spawn 与 BattleOrchestrator 透传行类(battle/boss)。 敌人表 20 行键迁移(原 node_depth 数值就地成为新带键,值列零变更),新增 row_kind 列; schema 校验随键换代:row_kind 封闭枚举、dup_key/rowset_rule 新键串、每关 battle 带 0 锚 与每关 boss 行集 ≥1;跨表校验 ④ 敌侧改带寻址,新增同日图多 Boss 同带报错与顶带消解报错。 读数:基线冒烟 1156 项 0 失败;本轮冒烟 1161 项 0 失败;单元 35 套件全绿;探针 --click 全绿。 --- game/core/battle_orchestrator.gd | 8 +-- game/core/config_db.gd | 4 +- game/core/config_db_cross_validator.gd | 39 ++++++++++---- game/core/config_db_resolver.gd | 70 +++++++++++++++++++++----- game/core/unit_container.gd | 8 +-- game/data/schemas/enemy_table.gd | 68 +++++++++++++++++-------- game/data/tables/enemy.tres | 64 +++++++++++++++-------- game/presentation/battle_narrator.gd | 2 +- 8 files changed, 191 insertions(+), 72 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 6ee113f6..1515056b 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -324,9 +324,11 @@ func begin_battle(enc: Dictionary) -> bool: # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 # 既有口径;atk 区间逐行同构 = 行值语义完整,属「每行一骰」语义内的 # 行字段区间骰逐行化,非新增随机源形态)。 - var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) + # 行类维度:节点身份 battle/boss 决定行集键域(缺省 battle 只服务测试直呼面——生产路径恒传 node_type) + var enc_kind := String(enc.get("node_type", "battle")) + var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth), enc_kind) if erowset_pre.is_empty(): - _fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)]) + _fatal("敌人表 行集解析为空(%d,%d,%s)" % [int(enc.level_index), int(enc.node_depth), enc_kind]) return false var count_overrides: Array[Variant] = [] var atk_overrides: Array[Variant] = [] @@ -354,7 +356,7 @@ func begin_battle(enc: Dictionary) -> bool: _hero_atk = int(enc.get("hero_atk", int(db.hero.atk))) if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), count_overrides, _hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides, - String(enc.get("attack_type", "")), String(enc.get("armor_type", ""))): # 两根类型轴(空串=锻造配置默认值回退,行为连续) + String(enc.get("attack_type", "")), String(enc.get("armor_type", "")), enc_kind): # 两根类型轴(空串=锻造配置默认值回退,行为连续)+行类维(battle/boss) _fatal("敌人系统 spawn 被拒") return false if not _install_new_board(): diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 95749262..3ec2c319 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -190,8 +190,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) -> Array[Dictionary]: - return _resolver.resolve_enemy_rowset(L, D) # 行集数组形态(非单行 Dictionary) +func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]: + return _resolver.resolve_enemy_rowset(L, D, row_kind) # 行集数组形态(非单行 Dictionary;D 维带寻址) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index c9521c37..7bb4b95f 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -51,8 +51,9 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: for s in required_sources: if not st_tab.source_label_map.has(s): push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) - # ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— - # 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦) + # ④ LevelTable 节点悬空引用(battle/boss 节点:board_gen 侧须有精确行;enemy 侧按 + # (关卡序号, 行类) 带寻址——落带即设计语义,节点深度高于顶带键=顶 clamp 消解属调参事故,加载期直接拦。 + # 同关同一张图多终点 Boss 节点解析带键须互异(共带=两终点命中同一 Boss 行集,「走哪条路打谁」落空)) var lv: LevelTable = loaded.get("level") var bg: BoardGenTable = loaded.get("board_gen") var en: EnemyTable = loaded.get("enemy") @@ -60,9 +61,16 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: var bg_keys := {} for row in bg.rows: bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_keys := {} + var en_bands := {} # "%d_%s" % [L, row_kind] → 升序带键数组 for row in en.rows: - en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + var ek := "%d_%s" % [int(row.level_index), String(row.get("row_kind", ""))] + if not en_bands.has(ek): + en_bands[ek] = [] + var eb := int(row.get("depth_band", -1)) + if not (en_bands[ek] as Array).has(eb): + (en_bands[ek] as Array).append(eb) + for ek in en_bands.keys(): + (en_bands[ek] as Array).sort() for lrow in lv.rows: var li: int = int(lrow.level_index) # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) @@ -73,16 +81,29 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: else: for tpl in tpls: node_lists.append(tpl.get("nodes", [])) - for nodes_arr in node_lists: - for node in nodes_arr: + for tpl_i in range(node_lists.size()): + var boss_bands := {} # 本图已占 Boss 带键 + for node in node_lists[tpl_i]: var t: String = String(node.get("type", "")) if t != "battle" and t != "boss": continue - var key := "%d_%d" % [li, int(node.get("node_depth", -1))] + var nd: int = int(node.get("node_depth", -1)) + var key := "%d_%d" % [li, nd] if not bg_keys.has(key): push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) - if not en_keys.has(key): - push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) + var nk := "%d_%s" % [li, t] + if not en_bands.has(nk): + push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%d) 在 enemy 无该行类行集" % [li, t, nd]) + continue + var hit_band: int = EnemyTable.band_of(en_bands[nk], nd) + if hit_band < 0: + push_diag.call("error", "level_node_dangling", "level %d %s 节点深度 %d 高于 (level %d, %s) 顶带键 %d——顶 clamp 消解" % [ + li, t, nd, li, t, int((en_bands[nk] as Array)[-1])]) + continue + if t == "boss": + if boss_bands.has(hit_band): + push_diag.call("error", "rowset_rule", "level %d 模板 %d 多终点 Boss 节点同带(band %d)——共带使「走哪条路打谁」落空" % [li, tpl_i, hit_band]) + boss_bands[hit_band] = true # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) var skt = loaded.get("skill") if skt != null and en != null: diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index 78a83b29..8bd98da7 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -30,11 +30,63 @@ func resolve_board_gen(L: int, D: int) -> Dictionary: return _resolve_progress(_board_gen_rows_get.call(), L, D, "board_gen") -func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]: - # 行集数组形态:ceil 命中 (l*,d*) 后 - # 返回该坐标全部行、按表文件行序(行序即契约——同坐标多行文件内相邻由 EnemyTable.validate 兜底)。 - # 空集表达 = 空数组(生产表下 ceil 命中坐标恒有行,空集仅表空时可达——validate 已拦)。 - return _resolve_progress(_enemy_rows_get.call(), L, D, "enemy", true) +func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary]: + # 带寻址:L 维沿进度行解析现状(ceil 向上取整保证偏难、越最大行 clamp), + # D 维经 EnemyTable.band_of 落带——命中该 (L, row_kind) 的某一个 depth_band, + # 返回该带全部行、按表文件行序(行序即契约——同带键多行文件内相邻由 EnemyTable.validate 兜底)。 + # 空集表达 = 空数组(band_keys 空时可达,交消费侧既有空行集 fatal 路径——validate 已拦无 Boss 行集)。 + if L < 1 or D < 0: + _diag.call("fatal", "request_oob", "enemy 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [L, D]) + return [] + if not EnemyTable.ROW_KINDS.has(row_kind): + _diag.call("fatal", "request_oob", "enemy 请求行类非法 (%d,%d,\"%s\")——row_kind 域 {battle, boss}" % [L, D, row_kind]) + return [] + var rows: Array = _enemy_rows_get.call() + var level_keys: Array[int] = [] + for row in rows: + var lv := int(row.get("level_index", 0)) + if not level_keys.has(lv): + level_keys.append(lv) + if level_keys.is_empty(): + _diag.call("fatal", "structural", "enemy 行集为空——锚行校验应已拦截") + return [] + level_keys.sort() + var l_star := _ceil_key(level_keys, L) + if l_star < 0: + l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶 + _warn_resolve("enemy", "resolve_level_clamp", "enemy 请求 (%d,%d) clamp 解析(超出最大关卡取最大值)→ 关卡 %d" % [L, D, l_star], l_star, D) + elif l_star != L: + _warn_resolve("enemy", "resolve_level_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ 关卡 %d" % [L, D, l_star], l_star, D) + var band_keys: Array[int] = [] + for row in rows: + if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind: + var bv := int(row.get("depth_band", -1)) + if not band_keys.has(bv): + band_keys.append(bv) + if band_keys.is_empty(): + return [] + band_keys.sort() + var d_star := EnemyTable.band_of(band_keys, D) + if d_star < 0: + d_star = int(band_keys[-1]) # 越顶 clamp 顶带(调参事故面,跨表校验另报) + _warn_resolve("enemy", "resolve_depth_clamp", "enemy 请求 (%d,%d) clamp 解析(超出顶带取顶带)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star) + elif d_star != D: + _warn_resolve("enemy", "resolve_depth_ceil", "enemy 请求 (%d,%d) ceil 解析(向上取整保证偏难)→ (%d,band %d)" % [L, D, l_star, d_star], l_star, d_star) + var all_out: Array[Dictionary] = [] + for row in rows: + if int(row.get("level_index", 0)) == l_star and String(row.get("row_kind", "")) == row_kind \ + and int(row.get("depth_band", -1)) == d_star: + all_out.append(row) + return all_out + + +# 解析告警单点(类别 + 命中键四元组去重——R1 既有去重注册表) +func _warn_resolve(table_id: String, category: String, message: String, l_key: int, d_key: int) -> void: + var dedup_key := "%s|%s|%d|%d" % [table_id, category, l_key, d_key] + if _warn_dedup.has(dedup_key): + return + _warn_dedup[dedup_key] = true + _diag.call("warning", category, message) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) @@ -51,7 +103,7 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: return {} -func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all := false) -> Variant: +func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Variant: if L < 1 or D < 0: _diag.call("fatal", "request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) return {} @@ -98,12 +150,6 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all _warn_dedup[dedup_key] = true var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" _diag.call("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 fadeac2c..2a3416c6 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -35,14 +35,14 @@ func _init(db_ref: ConfigDb) -> void: # ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。 func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [], atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = [], - hero_attack_type: String = "", hero_armor_type: String = "") -> bool: + hero_attack_type: String = "", hero_armor_type: String = "", row_kind: String = "battle") -> bool: if _spawned: push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)") return false - # 行集数组口径(resolver 返回该坐标全部行、文件序) - var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth) + # 行集数组口径(resolver 按 (L,band) 带寻址返回全部行、文件序) + var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth, row_kind) if rowset.is_empty(): - push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth]) + push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d,%s)" % [level_index, node_depth, row_kind]) assert(false, "遭遇行悬空(R1 前置守卫)") return false var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk) diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index 697ef155..bfd185e0 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -1,12 +1,16 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION -## 敌人表 schema:逐 (level_index, node_depth, enemy_id) 行集的类型化读取(行集可多行混编;垂直切片验证:完整战斗逐行归因)。 -## 行序即契约:同坐标多行必须文件内相邻——resolver 返回序/生成序/叙述取首行全依赖文件行序。 +## 敌人表 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] +# 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集 +const ROW_KINDS := ["battle", "boss"] + # 护甲类型封闭枚举(克制矩阵本体在 SettlementTable; # 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲) const ARMOR_TYPES := ["light", "ward", "heavy"] @@ -35,19 +39,23 @@ func validate() -> Array: errs.append({"category": "empty_table", "message": "EnemyTable 为空"}) return errs var seen := {} - var last_rk := "" - var closed_rk := {} + var last_rowset_key := "" + var closed_rowset := {} var anchor_ok := {} + var boss_ok := {} for row in rows: var L: int = int(row.get("level_index", 0)) - var D: int = int(row.get("node_depth", -1)) + 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_%d_%s" % [L, D, eid] + 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 D < 0: - errs.append({"category": "domain", "message": "进度键越域 (%d,%d)" % [L, D]}) + 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})" % [eid, rk]}) if eid == "": errs.append({"category": "domain", "message": "enemy_id 空"}) if String(row.get("display_name", "")) == "": @@ -118,23 +126,41 @@ 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]}) - # 行集构成纪律②(行序即契约,敌人系统 v0.15):同坐标多行必须 - # 文件内相邻。迭代中记录上一行坐标;当前坐标曾出现过但 ≠ 上一行坐标 = 同坐标行被隔断,报错。 - # 同坐标多敌种混编合法;同坐标同敌种第二行仍由 + # 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须 + # 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。 + # 同带键多敌种混编合法;同带键同敌种第二行仍由 # dup_key 拦截(键含 enemy_id)。错误类别沿用 rowset_rule(定位面不变)。 - var rk := "%d_%d" % [L, D] - 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 + var rowset_key := "%d_%s_%d" % [L, rk, band] + if rowset_key != last_rowset_key: + if closed_rowset.has(rowset_key): + errs.append({"category": "rowset_rule", "message": "(%d,%s,%d) 行集行序违例:同带键多行必须文件内相邻(行序即契约——resolver 返回序/生成序/叙述取首行全依赖文件行序)" % [L, rk, band]}) + closed_rowset[rowset_key] = true + last_rowset_key = rowset_key + if rk == "battle" and band == 0: + anchor_ok[L] = true + elif rk == "boss": + boss_ok[L] = true + if not anchor_ok.has(L): + anchor_ok[L] = false for L in anchor_ok.keys(): if not anchor_ok[L]: - errs.append({"category": "anchor", "message": "EnemyTable level %d 缺 depth-0 锚行" % 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 +# 深度带切法单点(唯一实现,禁第二份算法):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 ceil 版行集数组:ceil 保证偏难, -# 禁双算法并存〔状态记录 312〕;坐标须全精确命中)。 +# ConfigDbResolver.resolve_enemy_rowset 带寻址行集数组:D 维经本表 band_of 落带(偏难一侧), +# 禁双算法并存〔状态记录 312〕;L 维 ceil/clamp 与越顶 clamp 语义归 resolver)。 diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index df5bd973..897cd197 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -6,7 +6,8 @@ script = ExtResource("1") rows = [{ "level_index": 1, -"node_depth": 0, +"row_kind": "battle", +"depth_band": 0, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, @@ -27,7 +28,8 @@ rows = [{ "armor_class": 0 }, { "level_index": 1, -"node_depth": 1, +"row_kind": "battle", +"depth_band": 1, "enemy_id": "goblin", "display_name": "地精一伙", "unit_count": 2, @@ -48,7 +50,8 @@ rows = [{ "armor_class": 1 }, { "level_index": 1, -"node_depth": 2, +"row_kind": "battle", +"depth_band": 2, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 1, @@ -71,7 +74,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 1, -"node_depth": 3, +"row_kind": "battle", +"depth_band": 3, "enemy_id": "hobgoblin", "display_name": "大地精", "unit_count": 2, @@ -95,7 +99,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 1, -"node_depth": 4, +"row_kind": "boss", +"depth_band": 4, "enemy_id": "ogre_chief", "display_name": "食人魔酋长", "unit_count": 1, @@ -122,7 +127,8 @@ rows = [{ "armor_class": 8 }, { "level_index": 2, -"node_depth": 0, +"row_kind": "battle", +"depth_band": 0, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 1, @@ -145,7 +151,8 @@ rows = [{ "armor_class": 1 }, { "level_index": 2, - "node_depth": 1, + "row_kind": "battle", + "depth_band": 1, "enemy_id": "bugbear", "display_name": "熊地精猎群", "unit_count": 2, @@ -169,7 +176,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 4, +"row_kind": "battle", +"depth_band": 4, "enemy_id": "giant_spider", "display_name": "巨蜘蛛", "unit_count": 2, @@ -192,7 +200,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 5, +"row_kind": "battle", +"depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精重击手", "unit_count": 1, @@ -215,7 +224,8 @@ rows = [{ "armor_class": 8 }, { "level_index": 2, -"node_depth": 5, +"row_kind": "battle", +"depth_band": 5, "enemy_id": "giant_spider", "display_name": "巨蜘蛛巢群", "unit_count": 2, @@ -242,7 +252,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 6, +"row_kind": "battle", +"depth_band": 6, "enemy_id": "giant_spider", "display_name": "巨蜘蛛巢群", "unit_count": 2, @@ -269,7 +280,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 2, -"node_depth": 7, +"row_kind": "battle", +"depth_band": 7, "enemy_id": "giant_spider", "display_name": "巨蜘蛛巢群", "unit_count": 2, @@ -296,7 +308,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 2, -"node_depth": 9, +"row_kind": "boss", +"depth_band": 9, "enemy_id": "drider", "display_name": "蛛化卓尔精灵", "unit_count": 1, @@ -326,7 +339,8 @@ rows = [{ "armor_class": 10 }, { "level_index": 3, -"node_depth": 0, +"row_kind": "battle", +"depth_band": 0, "enemy_id": "duergar", "display_name": "灰矮人", "unit_count": 1, @@ -352,7 +366,8 @@ rows = [{ "armor_class": 2 }, { "level_index": 3, -"node_depth": 1, +"row_kind": "battle", +"depth_band": 1, "enemy_id": "hook_horror", "display_name": "钩爪恐魔", "unit_count": 2, @@ -375,7 +390,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 3, -"node_depth": 4, +"row_kind": "battle", +"depth_band": 4, "enemy_id": "hook_horror", "display_name": "钩爪恐魔", "unit_count": 2, @@ -399,7 +415,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 3, -"node_depth": 5, +"row_kind": "battle", +"depth_band": 5, "enemy_id": "duergar", "display_name": "灰矮人重装手", "unit_count": 1, @@ -425,7 +442,8 @@ rows = [{ "armor_class": 9 }, { "level_index": 3, -"node_depth": 6, +"row_kind": "battle", +"depth_band": 6, "enemy_id": "hook_horror", "display_name": "钩爪恐魔巢群", "unit_count": 2, @@ -449,7 +467,8 @@ rows = [{ "armor_class": 3 }, { "level_index": 3, -"node_depth": 7, +"row_kind": "battle", +"depth_band": 7, "enemy_id": "duergar", "display_name": "灰矮人小队", "unit_count": 2, @@ -476,7 +495,8 @@ rows = [{ "armor_class": 4 }, { "level_index": 3, -"node_depth": 9, +"row_kind": "boss", +"depth_band": 9, "enemy_id": "duergar_king", "display_name": "灰矮人石君王", "unit_count": 1, @@ -669,3 +689,7 @@ rows = [{ ; N3 敌方攻击类型轴开列(2026-10-13 迭代 24 卡①,承接锻造工坊实装批迁移面⑥):20 行全填 normal 挂敌种行—— ; 不做差异化(pierce/magic 属新设计另立项)。全局平移效应=主角穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0, ; 锻造甲路线首次具备战斗意义;克制矩阵数值零触碰(settlement.tres armor_effectiveness 不改)。 +; +; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集; +; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。 +; hobgoblin 行为缺口行:无立绘、不在敌种表——留任在役,补齐三件套(立绘+语义模板+敌种表行)归敌种内容批。 diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index aee45931..1422f766 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -73,7 +73,7 @@ static func boss_name(runner, db) -> String: if runner != null: var node: Dictionary[Variant, Variant] = runner.current_node() if String(node.get("type", "")) == "boss": - var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) + var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss") if not rowset.is_empty(): return String(rowset[0].get("display_name", "")) return "遗迹守卫"