T2组B·schema与解析换代(敌人表行集多行批——P1甲/P2甲落码): enemy_table.gd 恰一敌种校验退役改行集构成纪律②行序相邻校验(同坐标行不相邻即rowset_rule报错,类别沿用定位面不变;同敌种第二行仍dup_key拦)+UNIT_COUNT_MAX语义注释改单种数(单行域[1,3]不变,行集总数≤4归spawn守卫·骰后值加载期不可判)+floor版resolve_rowset退役(双算法销号,工作日志312);config_db_resolver.gd resolve_enemy_rowset改返回行集数组Array[Dictionary](ceil命中坐标全部行,文件序=契约序,空集=空数组;_resolve_progress加return_all参数board_gen单行通道不动);config_db.gd门面签名同步;全仓消费点机械适配取首行(T2行为零变化:spawn/orchestrator/narrator/smoke七处/config_db_test/skill_rhythm_probe/damage_probe两处;floor四调用点迁ceil坐标全精确命中零行为差);轻闸=冒烟805/0三闸全过+run_all 17套全绿——零内容期断言逐位零漂移

This commit is contained in:
DKChan committed 2026-10-07 02:05:30 +08:00
1 parent ab5ddad438
commit 8690230573
10 files changed
+68 -44

No files matched your search

+8 -1
View File
@@ -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
+2 -2
View File
@@ -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——组合事故非配置事故)
+12 -3
View File
@@ -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
+5 -2
View File
@@ -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:
+20 -20
View File
@@ -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 待裁项〕;原冒烟四调用点随批迁移,坐标全精确命中零行为差)。
+7 -3
View File
@@ -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 "遗迹守卫"
+2 -2
View File
@@ -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:
+2 -2
View File
@@ -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"))
+7 -7
View File
@@ -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)· 完美档多种子首遇战稳过
@@ -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:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重)