iter54-卡3:精英专属池批(elite 行类与字段互锁/dangling_base/effective_pool 池继承合并/ENCOUNTER_ROW_KINDS 寻址门暂不接线/goblin_elite 落表+存量冒烟同步+图鉴导出认得精英行)——对照表 R1-R7 全绿(elite_row_test 六组/五件存量套件直跑绿/生产表 error 诊断 0),T3.8 里程碑出口 --code 六闸全绿【验收通过】

This commit is contained in:
DKChan committed 2026-11-07 03:05:12 +08:00
1 parent 4c424af358
commit f9c9caf56b
14 files changed
+540 -40

No files matched your search

+123 -8
View File
@@ -8,8 +8,10 @@ extends Resource
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
# 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集
const ROW_KINDS := ["battle", "boss"]
# 行类封闭枚举:表内允许出现的行类(battle/boss 对齐关卡节点 type 词表,elite=精英行)
const ROW_KINDS := ["battle", "boss", "elite"]
# 开战寻址可读的行类——elite 接线前不在此列(敌人系统 v1.9 暂不接线;resolver 行类门单点消费)
const ENCOUNTER_ROW_KINDS := ["battle", "boss"]
# 护甲类型封闭枚举(克制矩阵本体在 SettlementTable;
# 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲)
@@ -49,6 +51,7 @@ func validate() -> Array:
errs.append_array(_validate_row_numbers(row, eid2))
errs.append_array(_validate_row_atk_range(row, eid2))
errs.append_array(_validate_row_pool(row, eid2))
errs.append_array(_validate_elite_fields(row, eid2))
errs.append_array(_validate_row_types(row, eid2))
# 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须
# 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。
@@ -67,6 +70,7 @@ func validate() -> Array:
boss_ok[L2] = true
if not anchor_ok.has(L2):
anchor_ok[L2] = false
errs.append_array(_validate_elite_refs())
errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok))
return errs
@@ -85,7 +89,7 @@ func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]:
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]})
errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss, elite})" % [eid, rk]})
if eid == "":
errs.append({"category": "domain", "message": "enemy_id 空"})
if String(row.get("display_name", "")) == "":
@@ -136,25 +140,99 @@ func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]:
## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。
## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。
func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]:
var no_errs: Array[Dictionary] = []
if String(row.get("row_kind", "")) == "elite":
return no_errs # elite 行不带 pool 键(pool_extra 承载附加技——条目域校验随 _validate_elite_fields)
var errs: Array[Dictionary] = []
var pool: Array[Variant] = row.get("pool", [])
if pool.is_empty():
errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid})
errs.append_array(_validate_pool_entries(pool, eid, "pool"))
return errs
## 池条目域共用校验(pool 与 pool_extra 共用——权重上限 9999 不出现第二份,契约 3 单点);
## field 承载报错定位段("pool"=既有文案逐字不变,"pool_extra"=附加技清单定位)。
func _validate_pool_entries(entries: Array, eid: String, field: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var pool_ids := {}
for entry_v in pool:
for entry_v in entries:
if not (entry_v is Dictionary):
errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]})
errs.append({"category": "domain", "message": "%s %s 条目非字典: %s" % [eid, field, str(entry_v)]})
continue
var entry: Dictionary[Variant, Variant] = entry_v
var sid: String = String(entry.get("skill_id", ""))
if sid == "":
errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid})
errs.append({"category": "domain", "message": "%s %s 条目 skill_id 空" % [eid, field]})
elif pool_ids.has(sid):
errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]})
errs.append({"category": "dup_key", "message": "%s %s 内 skill_id 重复: %s" % [eid, field, sid]})
pool_ids[sid] = true
var w: int = int(entry.get("weight", 0))
if w <= 0 or w > 9999:
errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]})
errs.append({"category": "domain", "message": "%s %s 权重越域 (0,9999]: %d" % [eid, field, w]})
return errs
## 精英行字段互锁(敌人系统 v1.9 :281/:282/:283):elite 行必备 base_ref/pool_extra、禁 pool、
## enemy_id 不与 base_ref 同名;非 elite 行禁带 base_ref/pool_extra。base_ref 引用存在性归 _validate_elite_refs。
func _validate_elite_fields(row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
if String(row.get("row_kind", "")) == "elite":
var base_ref := String(row.get("base_ref", ""))
if base_ref == "":
errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 缺或空(基础敌种引用必带)" % eid})
if base_ref == eid:
errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 与自身 enemy_id 同名(禁)" % eid})
if not row.has("pool_extra") or (row.get("pool_extra") as Array).is_empty():
errs.append({"category": "cross_field", "message": "%s elite 行 pool_extra 缺键或空(附加技清单非空必带)" % eid})
if row.has("pool"):
errs.append({"category": "cross_field", "message": "%s elite 行带 pool 键(禁——池走继承+附加)" % eid})
if row.has("pool_extra") and (row.get("pool_extra") as Array).size() > 0:
errs.append_array(_validate_pool_entries(row.get("pool_extra"), eid, "pool_extra"))
else:
if row.has("base_ref"):
errs.append({"category": "cross_field", "message": "%s 非 elite 行带 base_ref 键(禁——基础敌种引用为 elite 行专属)" % eid})
if row.has("pool_extra"):
errs.append({"category": "cross_field", "message": "%s 非 elite 行带 pool_extra 键(禁——附加技清单为 elite 行专属)" % eid})
return errs
## 精英行跨行引用校验(敌人系统 v1.9 :148/:282):base_ref 指向同关 battle 行类敌种(缺/空已由
## _validate_elite_fields 报——此处跳过);指向 elite 行判别同门 _min_band_row;附加技与基础池
## skill_id 交集非空报 dup_key(敌人系统.md:149 附加技与基础池重复拒绝)。
func _validate_elite_refs() -> Array[Dictionary]:
var errs: Array[Dictionary] = []
for row in rows:
if String(row.get("row_kind", "")) != "elite":
continue
var base_ref := String(row.get("base_ref", ""))
if base_ref == "":
continue
var level := int(row.get("level_index", 0))
var eid := String(row.get("enemy_id", ""))
var base := _min_band_row(level, base_ref, "battle")
if base.is_empty():
if not _min_band_row(level, base_ref, "elite").is_empty():
errs.append({"category": "dangling_base", "message": "%s base_ref 指向 elite 行: %s(基础敌种须为 battle 行类)" % [eid, base_ref]})
else:
errs.append({"category": "dangling_base", "message": "%s base_ref 同关 battle 行类无此敌种: %s" % [eid, base_ref]})
continue
errs.append_array(_extra_overlap_errs(row, base, eid))
return errs
## 附加技与基础池重叠判(_validate_elite_refs 交集段提出——分支预算内聚)
func _extra_overlap_errs(row: Dictionary, base_row: Dictionary, eid: String) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var base_ids := {}
for entry_v in base_row.get("pool", []):
if entry_v is Dictionary:
base_ids[String(entry_v.get("skill_id", ""))] = true
for entry_v in row.get("pool_extra", []):
if entry_v is Dictionary:
var sid := String(entry_v.get("skill_id", ""))
if sid != "" and base_ids.has(sid):
errs.append({"category": "dup_key", "message": "%s pool_extra 与基础池 skill_id 重复: %s" % [eid, sid]})
return errs
@@ -193,6 +271,43 @@ func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[
return errs
# 基础行查找单点(按 (关, 敌种, 行类) 取深度带最小行——精英有效池与「指向 elite 判别」共用,
# 禁第二份查找算法):无匹配返回空字典。行字段一律 row.get 读取(夹具行可缺键)。
func _min_band_row(level: int, eid: String, kind: String) -> Dictionary:
var best := {}
var best_band := -1
for row in rows:
if int(row.get("level_index", -1)) != level or String(row.get("row_kind", "")) != kind \
or String(row.get("enemy_id", "")) != eid:
continue
var band := int(row.get("depth_band", -1))
if best.is_empty() or band < best_band:
best = row
best_band = band
return best
## 精英有效池(按需现算、零缓存零写回——敌人系统.md:149 继承不复制):非 elite 行 = 自身 pool
## 字典条目副本;elite 行 = 基础行(同关 battle 行类基础敌种深度带最小行)pool 条目 + 本行
## pool_extra 条目(均只取 is Dictionary 条目并 duplicate——返回值改写不回灌表内行)。
func effective_pool(row: Dictionary) -> Array[Dictionary]:
var out: Array[Dictionary] = []
if String(row.get("row_kind", "")) != "elite":
for entry_v in row.get("pool", []):
if entry_v is Dictionary:
out.append((entry_v as Dictionary).duplicate())
return out
var base := _min_band_row(int(row.get("level_index", 0)), String(row.get("base_ref", "")), "battle")
if not base.is_empty():
for entry_v in base.get("pool", []):
if entry_v is Dictionary:
out.append((entry_v as Dictionary).duplicate())
for entry_v in row.get("pool_extra", []):
if entry_v is Dictionary:
out.append((entry_v as Dictionary).duplicate())
return out
# 深度带切法单点(唯一实现,禁第二份算法):band_keys = 该 (level_index, row_kind) 的
# depth_band 键升序数组;返回首个 ≥ D 的键(请求落带取偏难一侧)。
# D 高于全部键 → 返回 -1——resolver 侧 clamp 顶带+warning,跨表校验侧判「顶 clamp 消解」违例;
+21 -1
View File
@@ -207,6 +207,25 @@ rows = [{
"armor_type": "light",
"armor_class": 8
}, {
"level_index": 1,
"row_kind": "elite",
"depth_band": 0,
"enemy_id": "goblin_elite",
"display_name": "精英·地精",
"unit_count": 1,
"hp": 20,
"atk": 2,
"atk_min": 1,
"atk_max": 3,
"base_ref": "goblin",
"pool_extra": [{
"skill_id": "heavy",
"weight": 2
}],
"attack_type": "normal",
"armor_type": "light",
"armor_class": 2
}, {
"level_index": 2,
"row_kind": "battle",
"depth_band": 0,
@@ -943,6 +962,7 @@ rows = [{
; 不做差异化(pierce/magic 属新设计另立项)。全局平移效应=主角穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0,
; 锻造甲路线首次具备战斗意义;克制矩阵数值零触碰(settlement.tres armor_effectiveness 不改)。
;
; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集;
; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss, elite}——battle=战斗节点配池,boss=终点行集,elite=精英行(独立 enemy_id、池走继承+附加,暂不接线任何节点寻址);
; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。
; (1,3) 大地精行 2026-11-04 用户裁退役,改地精+熊地精混编(行序熊地精在前、行值整行复用 (1,2)/(1,0)、静态骰和 3);沿革见 git 提交史
; 精英专属池批首只精英=地精精英(1,elite,0,goblin_elite),行值 provisional(投值所有权归难度曲线与Boss 统一修正),来源=2026-11-06 用户点头草案 v0