feat(m3-iter5-T5): 关卡节点图生成特性批·台阶一全落地——15张骨架(3关×5,模板0=现行图回滚档;L1单链排列变体/L2L3网状图变体,战斗id恒∈遭遇表行集,商店必经位承袭)+node_map扩签(标签不参与派生只进日志,黄金向量零动)+首次冒险固定脚本(存档布尔位唯一新增域,老档迁移=true,收束点置位幂等)+池内全模板加载校验(validate抽出与跨表校验模板感知)+MC分层出数(RUNSUM level_templates逐节点模板名,抽查五模板全被抽中);冒烟558→576(新增17断言:池参数/零scope强制/抽签确定性/N=1等价/首冒分支/收束置位幂等);生成器tools/t5_build_templates.py含生成前自校验

This commit is contained in:
DKChan committed 2026-09-28 23:20:00 +08:00
1 parent 8ffc5552f1
commit 3f73e16366
14 files changed
+471 -207

No files matched your search

+58 -36
View File
@@ -38,43 +38,65 @@ func validate() -> Array:
var heal: int = int(row.get("rest_heal_pct", -1))
if heal < 0 or heal > 100:
errs.append({"category": "domain", "message": "level %d rest_heal_pct 越域 [0,100]" % L})
var nodes: Array = row.get("nodes", [])
if nodes.is_empty():
errs.append({"category": "domain", "message": "level %d 节点链为空" % L})
# 模板池(关卡节点图生成 v0.4——M-3 迭代 5 T5):row.templates 非空 = 池形态,池内全模板加载期校验
# (违规致命——不只抽中的那张);空 = 旧单图形态(row.nodes 直读,向后兼容零迁移)。
var node_lists: Array = [] # [ [上下文标签, nodes 数组], ... ]
var templates: Array = row.get("templates", [])
if templates.is_empty():
node_lists.append(["", row.get("nodes", [])])
else:
for j in templates.size():
var tpl = templates[j]
if typeof(tpl) != TYPE_DICTIONARY or (tpl.get("nodes", []) as Array).is_empty():
errs.append({"category": "domain", "message": "level %d 模板[%d] 缺 nodes 或为空" % [L, j]})
continue
node_lists.append(["模板[%d]=%s" % [j, String(tpl.get("display_name", ""))], tpl.get("nodes", [])])
for nl in node_lists:
for e in _validate_level_nodes(L, nl[0], nl[1]):
errs.append(e)
return errs
# 单节点链校验体(validate 抽出——row.nodes 与池内每张模板同跑一套;ctx = 消息上下文标签)
func _validate_level_nodes(L: int, ctx: String, nodes: Array) -> Array:
var errs: Array = []
var tag: String = "" if ctx.is_empty() else "〔%s〕" % ctx
if nodes.is_empty():
errs.append({"category": "domain", "message": "level %d%s 节点链为空" % [L, tag]})
return errs
var boss_count := 0
var battle_count := 0
var is_graph := false
for node in nodes:
if node.has("next"):
is_graph = true
var t: String = String(node.get("type", ""))
if not NODE_TYPES.has(t):
errs.append({"category": "enum", "message": "level %d%s 未知节点类型: %s" % [L, tag, t]})
continue
var boss_count := 0
var battle_count := 0
var is_graph := false
for node in nodes:
if node.has("next"):
is_graph = true
var t: String = String(node.get("type", ""))
if not NODE_TYPES.has(t):
errs.append({"category": "enum", "message": "level %d 未知节点类型: %s" % [L, t]})
continue
if t == "battle" or t == "boss":
if int(node.get("node_depth", -1)) < 0:
errs.append({"category": "domain", "message": "level %d %s 节点缺 node_depth" % [L, t]})
battle_count += 1
if t == "boss":
boss_count += 1
if boss_count != 1:
errs.append({"category": "structural", "message": "level %d Boss 节点数 = %d(须恰一)" % [L, boss_count]})
if battle_count < 2:
errs.append({"category": "structural", "message": "level %d 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, battle_count]})
if not is_graph:
# 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义)
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
errs.append({"category": "structural", "message": "level %d 末节点非 Boss(单链以 Boss 收尾)" % L})
# 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命)
for e in _validate_graph(L, nodes, is_graph):
errs.append(e)
if t == "battle" or t == "boss":
if int(node.get("node_depth", -1)) < 0:
errs.append({"category": "domain", "message": "level %d%s %s 节点缺 node_depth" % [L, tag, t]})
battle_count += 1
if t == "boss":
boss_count += 1
if boss_count != 1:
errs.append({"category": "structural", "message": "level %d%s Boss 节点数 = %d(须恰一)" % [L, tag, boss_count]})
if battle_count < 2:
errs.append({"category": "structural", "message": "level %d%s 战斗节点(含 Boss)= %d(≥2 才构成关卡)" % [L, tag, battle_count]})
if not is_graph:
# 单链形态纪律(第一关零迁移):末节点必须是 Boss(关底收尾——冒险结构 关卡定义)
if String(nodes[nodes.size() - 1].get("type", "")) != "boss":
errs.append({"category": "structural", "message": "level %d%s 末节点非 Boss(单链以 Boss 收尾)" % [L, tag]})
# 图合法性(单链与图同一套算法——单链 = 出边恰一的退化形态,违规同样加载致命)
for e in _validate_graph(L, nodes, is_graph, tag):
errs.append(e)
return errs
# 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 →
# 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。
func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> Array:
var errs: Array = []
var ids: Array = [] # 每节点 id(下标对齐)
var nexts: Array = [] # 每节点出边 id 数组
@@ -83,11 +105,11 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
var node: Dictionary = nodes[i]
var nid: int = int(node.get("node_depth", i)) if is_graph else i
if is_graph and not node.has("node_depth"):
errs.append({"category": "structural", "message": "level %d 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, i]})
errs.append({"category": "structural", "message": "level %d%s 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, tag, i]})
nid = i # 降级继续(后续校验仍有意义)
ids.append(nid)
if id_to_idx.has(nid):
errs.append({"category": "dup_key", "message": "level %d 节点编号重复: %d(关卡内唯一定位地址)" % [L, nid]})
errs.append({"category": "dup_key", "message": "level %d%s 节点编号重复: %d(关卡内唯一定位地址)" % [L, tag, nid]})
id_to_idx[nid] = i
var nx: Array = []
if node.has("next"):
@@ -100,9 +122,9 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
for i in nodes.size():
for tid in nexts[i]:
if not id_to_idx.has(tid):
errs.append({"category": "dangling_next", "message": "level %d 节点 %d 出边指向不存在编号: %d" % [L, ids[i], tid]})
errs.append({"category": "dangling_next", "message": "level %d%s 节点 %d 出边指向不存在编号: %d" % [L, tag, ids[i], tid]})
elif tid <= ids[i]:
errs.append({"category": "structural", "message": "level %d 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, ids[i], tid]})
errs.append({"category": "structural", "message": "level %d%s 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, tag, ids[i], tid]})
# ④ 入口唯一(无入边节点恰一——唯一入口)
var has_inbound := {}
for i in nodes.size():
@@ -113,7 +135,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool) -> Array:
if not has_inbound.has(ids[i]):
entries.append(i)
if entries.size() != 1:
errs.append({"category": "structural", "message": "level %d 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, entries.size()]})
errs.append({"category": "structural", "message": "level %d%s 入口节点数 = %d(须恰一——无入边节点唯一)" % [L, tag, entries.size()]})
# ⑤ Boss 恒唯一终点:无出边 + 编号最大(图形态;单链由"末节点 Boss"既有校验覆盖终点性)
for i in nodes.size():
if String(nodes[i].get("type", "")) != "boss":
+13 -151
View File
@@ -4,155 +4,17 @@
[resource]
script = ExtResource("1")
rows = [{
"level_index": 1,
"display_name": "第一层 · 地精洞窟",
"rest_heal_pct": 30,
"nodes": [{
"node_depth": 0,
"type": "battle"
}, {
"node_depth": 1,
"type": "battle"
}, {
"type": "reward"
}, {
"node_depth": 2,
"type": "battle"
}, {
"node_depth": 3,
"type": "battle"
}, {
"type": "rest"
}, {
"node_depth": 4,
"type": "boss"
}]
}, {
"level_index": 2,
"display_name": "第二层 · 幽暗地域",
"rest_heal_pct": 30,
"nodes": [{
"node_depth": 0,
"type": "battle",
"next": [1, 2]
}, {
"node_depth": 1,
"type": "battle",
"next": [3, 4]
}, {
"node_depth": 2,
"type": "reward",
"next": [4, 5]
}, {
"node_depth": 3,
"type": "rest",
"next": [6]
}, {
"node_depth": 4,
"type": "battle",
"next": [6, 7]
}, {
"node_depth": 5,
"type": "battle",
"next": [7]
}, {
"node_depth": 6,
"type": "battle",
"next": [8]
}, {
"node_depth": 7,
"type": "battle",
"next": [8]
}, {
"node_depth": 8,
"type": "shop",
"next": [9]
}, {
"node_depth": 9,
"type": "boss"
}]
}, {
"level_index": 3,
"display_name": "第三层 · 灰矮人矿城",
"rest_heal_pct": 30,
"nodes": [{
"node_depth": 0,
"type": "battle",
"next": [1, 2]
}, {
"node_depth": 1,
"type": "battle",
"next": [3, 4]
}, {
"node_depth": 2,
"type": "reward",
"next": [4, 5]
}, {
"node_depth": 3,
"type": "rest",
"next": [6]
}, {
"node_depth": 4,
"type": "battle",
"next": [6, 7]
}, {
"node_depth": 5,
"type": "battle",
"next": [7]
}, {
"node_depth": 6,
"type": "battle",
"next": [8]
}, {
"node_depth": 7,
"type": "battle",
"next": [8]
}, {
"node_depth": 8,
"type": "shop",
"next": [9]
}, {
"node_depth": 9,
"type": "boss"
}]
}]
rows = [
{"level_index": 1, "display_name": "第一层 · 地精洞窟", "rest_heal_pct": 30, "templates": [{"display_name": "地精洞窟·主线洞道(现行)", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"type": "reward"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"type": "rest"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·休整前移", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"type": "rest"}, {"node_depth": 2, "type": "battle"}, {"type": "reward"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·先赏后战", "nodes": [{"node_depth": 0, "type": "battle"}, {"type": "reward"}, {"node_depth": 1, "type": "battle"}, {"type": "rest"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·连战直入", "nodes": [{"node_depth": 0, "type": "battle"}, {"node_depth": 1, "type": "battle"}, {"type": "reward"}, {"type": "rest"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}, {"display_name": "地精洞窟·缓入长坡", "nodes": [{"node_depth": 0, "type": "battle"}, {"type": "rest"}, {"node_depth": 1, "type": "battle"}, {"type": "reward"}, {"node_depth": 2, "type": "battle"}, {"node_depth": 3, "type": "battle"}, {"node_depth": 4, "type": "boss"}]}]},
{"level_index": 2, "display_name": "第二层 · 幽暗地域", "rest_heal_pct": 30, "templates": [{"display_name": "幽暗地域·四岔环廊(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·奖励窄门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4]}, {"node_depth": 3, "type": "rest", "next": [5]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·直入深层", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [4]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·休整远路", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [2, 4]}, {"node_depth": 2, "type": "reward", "next": [3]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "幽暗地域·双线归束", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [4, 5]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [6]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]},
{"level_index": 3, "display_name": "第三层 · 灰矮人矿城", "rest_heal_pct": 30, "templates": [{"display_name": "灰矮人矿城·主竖井(现行)", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4, 5]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [6, 7]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·奖励窄门", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3, 4]}, {"node_depth": 2, "type": "reward", "next": [4]}, {"node_depth": 3, "type": "rest", "next": [5]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·直入深层", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [4]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·休整远路", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [2, 4]}, {"node_depth": 2, "type": "reward", "next": [3]}, {"node_depth": 3, "type": "rest", "next": [6]}, {"node_depth": 4, "type": "battle", "next": [5, 6]}, {"node_depth": 5, "type": "battle", "next": [7]}, {"node_depth": 6, "type": "battle", "next": [8]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}, {"display_name": "灰矮人矿城·双线归束", "nodes": [{"node_depth": 0, "type": "battle", "next": [1, 2]}, {"node_depth": 1, "type": "battle", "next": [3]}, {"node_depth": 2, "type": "reward", "next": [3, 4]}, {"node_depth": 3, "type": "rest", "next": [4, 5]}, {"node_depth": 4, "type": "battle", "next": [6]}, {"node_depth": 5, "type": "battle", "next": [6]}, {"node_depth": 6, "type": "battle", "next": [7]}, {"node_depth": 7, "type": "battle", "next": [8]}, {"node_depth": 8, "type": "shop", "next": [9]}, {"node_depth": 9, "type": "boss", "next": []}]}]}
]
; 切片登记(provisional——C1/C3 未设计,结构位与数值均为 SLICE-TUNED):
; 节点链 = 用户 2026-09-11 拍板的线性第一关(战×2 → 随机主动道具 → 战×2 → 血量恢复点 → Boss)。
; node_depth 轴 = 节点深度(难度 = f(关卡序号, 节点深度)——game-concept 锁定口径);Boss = Dmax 行(A1 第三轴登记约定)。
; rest_heal_pct = 30:恢复点回复 max_hp 的百分比(随生命天赋同步放大,防高生命档恢复失效)。
; Boss 板以 12×12 加密板区分。
;
; 分支节点图批(2026-09-17 用户拍板——关卡节点地图 v1.0 图版第一次落地,P1 批):
; L2 行集 = 网状图首落地:入口层 1 节点(0) → 中间三层(2/3/2 节点) → Boss 层 1 节点(8),总 9 节点;
; 真实岔路 4 处(0→[1,2] 入口分流 · 1→[3,4] 恢复或战 · 2→[4,5] 奖励后分流 · 4→[6,7] 深层分流);
; 含恢复节点 1(节点 3,rest_heal_pct 行字段兑付)与奖励节点 1(节点 2,直通空态——功能道具渠道 P1 未定);
; 普通战斗节点 6(0/1/4/5/6/7)+ Boss(8,编号最大、恒唯一终点)。连线只朝前进方向(编号严格递增),
; 单入口(节点 0 无入边),全部节点沿连线可达 Boss——图合法性加载校验强制(level_table.gd)。
; L1 单链零迁移:无 next 键 = 单链形态(隐式出边 = 数组顺序下一项),经同一套图校验(退化形态)。
; 敌种生态 = DND 幽暗地域(Underdark)方向:熊地精兄弟(bugbear 复用,池差异化改造)+ 巨蜘蛛
; giant_spider(新敌种,群行倾向)+ 蛛化卓尔精灵 drider(Boss 专属池,蜘蛛主题呼应)。行值全 provisional,
; 投值归 难度曲线与Boss。
;
; 层名改名批(2026-09-20 用户拍板):第一层「苔藓回廊」退役(湿地植物意象与地精巢穴不契合),
; 改名「裂颚洞窟」——致敬 DND 5e《凡戴尔的失落矿坑》地精部族「裂颚」(Cragmaw)的洞窟巢穴:
; 地精盘踞的浅层洞窟、通道向下延伸,天然连通第二层幽暗地域。仅 display_name 字面变更,
; 节点结构 / 行值零改动。
;
; 层名二改批(2026-09-20 用户拍板):「裂颚洞窟」→「地精洞窟」——不绑定 DND 专名部族
; (规避 IP 指涉),直取地精巢穴意象;洞窟纵深连通幽暗地域的取向不变。
; 仅 display_name 字面变更,节点结构 / 行值零改动。
;
; 第三关内容批(2026-09-20 用户拍板走 proposal-level3-content-2026-09-20.md 推荐):
; L3 行集复用 L2 九节点图模板(6 战 + Boss + 奖励 + 恢复,真实岔路 4 处);灰矮人矿城主题
; (DND 幽暗地域最深层方向)。仅行集落地,正式三关序列推进仍登记 Alpha 验收条件
; (测试直入口 ≠ 序列生效)。行值全 provisional 归 难度曲线与Boss。
;
; 商店房批(2026-09-20 用户拍板——GP+商店房实施批,关卡节点地图 v1.1 四类→五类):编排规则锚 =
; 每关恰 1 商店房、Boss 前一位必经位(参考哈迪斯商店房位置);L2/L3 行集 rewiring:原 Boss 行 (8, boss)
; 改为 (8, shop, next [9])、新增 (9, boss)——图六检全绿(Boss 恒最大编号 9、无出边恒终点;节点 6/7 出边
; [8] 字面不变、指向现为商店房,全路径穿商店房 8);L1 行集零改动(教学期最小修订面,L1 掉落攒进 L2 首店)。
; 必经位(全路径穿 8)由冒烟断言承载、非加载校验——level_table.gd 零新增图合法性检查。
; 连带:board_gen/enemy 补 (2,9)/(3,9) 精确行(跨表校验 ④ 强制,行值照抄原 Boss (L,8) 行)。
; 行值全 provisional 归 难度曲线与Boss。
; 关卡节点图生成 v0.4 台阶一内容批(M-3 迭代 5 T5,2026-09-28——15 张骨架 = 3 关 × 5,拍板点④):
; 模板 0 = 现行固定图(回滚档成员——首次冒险脚本/池缩 N=1 时逐位等价迁移前行为);
; L1 五模板 = 单链排列变体(图形态两不可全:Boss 须最大编号 vs Boss 遭遇深度 4——战斗深度序单调
; 保持难度坡,变化面 = 奖励/恢复位次);L2/L3 五模板 = 网状图变体(战斗节点编号恒 ∈ 遭遇表行集
; {0,1,4,5,6,7}、Boss 9 最大编号、商店房 8→Boss 必经位承袭;遭遇表按编号共用——零新增敌人数值)。
; 池内全模板加载期校验(level_table.gd validate + config_db 跨表校验模板感知);
; 行为读 LevelRunner._resolve_template_row(node_map 标签恰一 scope 抽签 / template_pick 强制)。
; 生成器 = tools/t5_build_templates.py(生成前自校验,一次性内容批工具随批入库留档)。