Files
gplan-2/game/data/schemas/level_table.gd
T

222 lines
11 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
# Date: 2026-09-11(2026-09-17 分支节点图批:nodes 升级图结构——单链=出边恰一的退化形态)
# 数据配置 LevelTable schema(关卡节点地图 v1.0 承载面)。
# 节点类型 closed enum:battle(战斗,须带 node_depth)/ reward(直通占位点)/ rest(血量恢复点)/ boss(关底 Boss,须带 node_depth)
# / shop(商店房——GP+商店房实施批 2026-09-21 五类落地:同 reward/rest 无战斗行、无 node_depth 要求,
# 图形态仍须编号化;零新增图合法性检查,必经位由冒烟断言承载非加载校验)。
# 图形态(行集内任一节点带 "next" 出边表即整行按图校验):节点编号 = node_depth(关卡内唯一定位地址,
# 全节点须带,reward/rest 亦然);next = [出边节点编号...](连线只朝前进方向:目标编号严格大于自身)。
# 单链形态(零 "next" 键——第一关现状零迁移):隐式出边 = 数组顺序下一项,节点编号 = 数组下标
# (仅校验期内部使用;node_depth 仍只对 battle/boss 必填)——单链经同一套图合法性算法。
# 图合法性(违规加载致命,validate 报错由 ConfigDB 收敛为 fail-fast):
# 编号唯一 · 入口唯一(无入边节点恰一)· Boss 恒唯一终点(无出边且编号最大)· 连线只朝前进方向(⇒无环)
# · 从入口可达全部节点 · 全部节点沿连线可达 Boss。
class_name LevelTable
extends Resource
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 商店房批 2026-09-21 四→五类(关卡节点地图 v1.1)
# 行单点读取(2026-10-02 修复批 T5 表访问收口——LevelRunner._find_level_row 直扫收口;键 = level_index)
func row_by_level_index(level_index: int) -> Dictionary:
for row in rows:
if int(row.level_index) == level_index:
return row
return {}
func validate() -> Array:
var errs: Array[Dictionary] = []
if rows.is_empty():
errs.append({"category": "empty_table", "message": "LevelTable 为空"})
return errs
var seen := {}
for row in rows:
var L: int = int(row.get("level_index", 0))
if seen.has(L):
errs.append({"category": "dup_key", "message": "LevelTable level_index 重复: %d" % L})
seen[L] = true
if L < 1:
errs.append({"category": "domain", "message": "level_index 须 ≥1"})
if String(row.get("display_name", "")) == "":
errs.append({"category": "domain", "message": "level %d display_name 空" % L})
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})
# 模板池(关卡节点图生成 v0.4——M-3 迭代 5 T5):row.templates 非空 = 池形态,池内全模板加载期校验
# (违规致命——不只抽中的那张);空 = 旧单图形态(row.nodes 直读,向后兼容零迁移)。
var node_lists: Array[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[Dictionary] = []
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
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)
# U2 目检新规则(迭代 5 返工批 2026-09-29 两条,加载期同检):①单链前三位纯战斗(奖励/休整禁入前三)
if not is_graph and nodes.size() >= 3:
for i in 3:
if String(nodes[i].get("type", "")) != "battle":
errs.append({"category": "structural", "message": "level %d%s 第 %d 位非战斗(单链前三位纯战斗——U2 目检规则)" % [L, tag, i + 1]})
return errs
# 图合法性核心(返回错误列表):id 解析(图=node_depth / 单链=数组下标)→ 出边表 →
# 唯一性/前进方向/入口唯一/Boss 终点/双向可达六检。参考 关卡节点地图 v1.0 分支节点图规则。
func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> Array:
var errs: Array[Dictionary] = []
var ids: Array[int] = [] # 每节点 id(下标对齐)
var nexts: Array[Array] = [] # 每节点出边 id 数组
var id_to_idx := {}
for i in nodes.size():
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%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%s 节点编号重复: %d(关卡内唯一定位地址)" % [L, tag, nid]})
id_to_idx[nid] = i
var nx: Array[int] = []
if node.has("next"):
for v in node.get("next", []):
nx.append(int(v))
elif i < nodes.size() - 1:
nx.append(i + 1) # 单链隐式出边 = 数组下一项(id 即下标)
nexts.append(nx)
# ② 出边引用存在 + 前进方向(目标 id 严格递增 ⇒ 无环——环必含非递增边)
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%s 节点 %d 出边指向不存在编号: %d" % [L, tag, ids[i], tid]})
elif tid <= ids[i]:
errs.append({"category": "structural", "message": "level %d%s 连线 %d→%d 违反前进方向(目标编号须严格更大)" % [L, tag, ids[i], tid]})
# ④ 入口唯一(无入边节点恰一——唯一入口)
var has_inbound := {}
for i in nodes.size():
for tid in nexts[i]:
has_inbound[tid] = true
var entries: Array[int] = []
for i in nodes.size():
if not has_inbound.has(ids[i]):
entries.append(i)
if entries.size() != 1:
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":
continue
if not nexts[i].is_empty():
errs.append({"category": "structural", "message": "level %d Boss 节点 %d 带出边(终点不可外出)" % [L, ids[i]]})
if is_graph and ids[i] != ids.max():
errs.append({"category": "structural", "message": "level %d Boss 编号 %d 非最大 %d(终点编号最大)" % [L, ids[i], ids.max()]})
if entries.size() == 1:
# ⑥ 从入口可达全部(无孤儿)· ⑦ 全部节点可达 Boss(逆向)——邻接表统一下标空间
var fwd := []
var back := []
for i in nodes.size():
fwd.append([])
back.append([])
for i in nodes.size():
for tid in nexts[i]:
if id_to_idx.has(tid):
fwd[i].append(id_to_idx[tid])
back[id_to_idx[tid]].append(i)
var reach := _reachable(entries[0], fwd, nodes.size())
for i in nodes.size():
if not reach.has(i):
errs.append({"category": "structural", "message": "level %d 节点 %d 从入口不可达(孤儿节点)" % [L, ids[i]]})
var boss_idx := -1
for i in nodes.size():
if String(nodes[i].get("type", "")) == "boss":
boss_idx = i
if boss_idx >= 0:
var to_boss := _reachable(boss_idx, back, nodes.size())
for i in nodes.size():
if not to_boss.has(i):
errs.append({"category": "structural", "message": "level %d 节点 %d 沿连线不可达 Boss" % [L, ids[i]]})
# ⑧ U2 目检新规则(迭代 5 返工批 2026-09-29):BFS 同深度层节点数上限 5(分支宽度天花板)
var layer_of := {}
var bfs: Array = [entries[0]]
layer_of[entries[0]] = 0
while not bfs.is_empty():
var cur: int = bfs.pop_front()
for tid in nexts[cur]:
var tidx: int = int(id_to_idx.get(tid, -1))
if tidx >= 0 and not layer_of.has(tidx):
layer_of[tidx] = int(layer_of[cur]) + 1
bfs.append(tidx)
var width := {}
for i in nodes.size():
if layer_of.has(i):
width[layer_of[i]] = int(width.get(layer_of[i], 0)) + 1
for d in width.keys():
if int(width[d]) > 5:
errs.append({"category": "structural", "message": "level %d BFS 第 %d 层 %d 节点(同层上限 5——U2 目检规则)" % [L, d, int(width[d])]})
return errs
# BFS 可达集(邻接表 adj: 节点下标 → 下标数组)
func _reachable(start: int, adj: Array, n: int) -> Dictionary:
var seen := {}
if start < 0 or start >= n:
return seen
var queue: Array = [start]
seen[start] = true
while not queue.is_empty():
var cur: int = queue.pop_front()
if cur >= adj.size():
continue
for nb in adj[cur]:
if not seen.has(nb):
seen[nb] = true
queue.append(nb)
return seen