172 lines
7.9 KiB
GDScript
172 lines
7.9 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 关卡节点表 schema:逐关行集(关数/显示名/休整回复/迷雾层数)的类型化读取(垂直切片验证:改表即改关卡)。
|
||
## 图合法性只在 `validate_graph` 一处陈述(消费侧起关校验入口——关卡节点图生成 v1.1 产出规格为准);
|
||
## 表加载期不校验图(关卡表不再携带节点数据,图源=参数化生成器)。
|
||
class_name LevelTable
|
||
extends Resource
|
||
|
||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||
@export var info_visible_layers: int = 2 # 信息迷雾可见层数 N(int ≥1;类型仅在邻近 N 层内可见——关卡节点地图 v2.1)
|
||
|
||
const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 五类(关卡节点地图 v2.1)
|
||
|
||
|
||
# 行单点读取(表访问收口——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 info_visible_layers < 1:
|
||
errs.append({"category": "domain", "message": "info_visible_layers 越域 int≥1: %d" % info_visible_layers})
|
||
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})
|
||
return errs
|
||
|
||
|
||
# 图合法性校验单点(消费侧起关入口——生成图到手、进节点前调用;违规=致命,无软回退)。
|
||
# 判据=四检+良构基底:① 入口(无入边)≥1 且终点(无出边)≥1;② 全部节点从某入口可达且可达某终点
|
||
#(每条路径恰从一个入口出发、恰到好处一个终点,无孤儿无断头);③ 终点恒为 Boss(终点 type=boss 且 boss 无出边);
|
||
# ④ 无环(出边目标深度严格大于源)。良构=句柄唯一·出边全命中·type 在五类枚举·battle/boss 带深度。
|
||
# 入口/终点个数与参数档的一致性归产出侧(关卡节点图生成 v1.1),本处不核个数;商店位归冒烟断言把关,本处禁检。
|
||
static func validate_graph(nodes: Array) -> Array:
|
||
if nodes.is_empty():
|
||
return [{"category": "structural", "message": "图节点集为空"}]
|
||
var well := _check_nodes_wellformed(nodes)
|
||
if not (well["errs"] as Array).is_empty():
|
||
return well["errs"]
|
||
var by_id: Dictionary = well["by_id"]
|
||
var edges := _check_edges(by_id, well["indeg"])
|
||
if not (edges["errs"] as Array).is_empty():
|
||
return edges["errs"]
|
||
var stops := _check_terminals(by_id, edges["outs"], edges["indeg"])
|
||
if not (stops["errs"] as Array).is_empty():
|
||
return stops["errs"]
|
||
return _check_reachability(by_id, edges["outs"], stops["entries"], stops["terminals"])
|
||
|
||
|
||
# 良构基底:字段齐·type 在枚举·深度非负·句柄唯一(返回 by_id 与入度骨架供后续检复用)
|
||
static func _check_nodes_wellformed(nodes: Array) -> Dictionary:
|
||
var errs: Array[Dictionary] = []
|
||
var by_id := {}
|
||
var indeg := {}
|
||
for i in nodes.size():
|
||
var node: Dictionary[Variant, Variant] = nodes[i]
|
||
for k in ["id", "type", "depth", "next"]:
|
||
if not node.has(k):
|
||
errs.append({"category": "structural", "message": "节点[%d] 缺字段 %s" % [i, k]})
|
||
var t := String(node.get("type", ""))
|
||
if not NODE_TYPES.has(t):
|
||
errs.append({"category": "enum", "message": "未知节点类型: %s" % t})
|
||
var d: int = int(node.get("depth", -1))
|
||
if d < 0:
|
||
errs.append({"category": "domain", "message": "节点 %s 深度越域(≥0): %d" % [str(node.get("id", i)), d]})
|
||
if t == "battle" and not node.has("depth"):
|
||
errs.append({"category": "structural", "message": "battle 节点 %s 缺深度" % str(node.get("id", i))})
|
||
var nid: int = int(node.get("id", i))
|
||
if by_id.has(nid):
|
||
errs.append({"category": "dup_key", "message": "节点句柄重复: %d" % nid})
|
||
by_id[nid] = node
|
||
if not indeg.has(nid):
|
||
indeg[nid] = 0
|
||
return {"errs": errs, "by_id": by_id, "indeg": indeg}
|
||
|
||
|
||
# 出边良构+无环判据(④):目标句柄存在、目标深度严格大于源;同时产出去边表与入度计数
|
||
static func _check_edges(by_id: Dictionary, indeg: Dictionary) -> Dictionary:
|
||
var errs: Array[Dictionary] = []
|
||
var outs := {}
|
||
for nid in by_id.keys():
|
||
outs[nid] = []
|
||
for nid in by_id.keys():
|
||
for tid in (by_id[nid]["next"] as Array):
|
||
if not by_id.has(int(tid)):
|
||
errs.append({"category": "dangling_next", "message": "节点 %d 出边指向不存在句柄: %s" % [nid, tid]})
|
||
continue
|
||
if int(by_id[int(tid)]["depth"]) <= int(by_id[nid]["depth"]):
|
||
errs.append({"category": "structural", "message": "连线 %d→%d 违反前进方向(目标深度须严格大于源)" % [nid, int(tid)]})
|
||
outs[nid].append(int(tid))
|
||
indeg[int(tid)] = int(indeg[int(tid)]) + 1
|
||
return {"errs": errs, "outs": outs, "indeg": indeg}
|
||
|
||
|
||
# 检①+检③:入口(无入边)≥1、终点(无出边)≥1、终点恒为 Boss 且 Boss 无出边
|
||
static func _check_terminals(by_id: Dictionary, outs: Dictionary, indeg: Dictionary) -> Dictionary:
|
||
var errs: Array[Dictionary] = []
|
||
var entries := []
|
||
var terminals := []
|
||
for nid in by_id.keys():
|
||
if int(indeg[nid]) == 0:
|
||
entries.append(nid)
|
||
if not (outs[nid] as Array).is_empty():
|
||
if String(by_id[nid]["type"]) == "boss":
|
||
errs.append({"category": "structural", "message": "Boss 节点 %d 带出边(终点不可外出)" % nid})
|
||
continue
|
||
terminals.append(nid)
|
||
if String(by_id[nid]["type"]) != "boss":
|
||
errs.append({"category": "structural", "message": "终点 %d 非 Boss(终点恒为 Boss)" % nid})
|
||
if entries.is_empty():
|
||
errs.append({"category": "structural", "message": "入口节点数 = 0(须 ≥1)"})
|
||
if terminals.is_empty():
|
||
errs.append({"category": "structural", "message": "终点节点数 = 0(须 ≥1)"})
|
||
return {"errs": errs, "entries": entries, "terminals": terminals}
|
||
|
||
|
||
# 检②:全部节点从某入口可达且可达某终点(无孤儿、无断头)
|
||
static func _check_reachability(by_id: Dictionary, outs: Dictionary, entries: Array, terminals: Array) -> Array:
|
||
var errs: Array[Dictionary] = []
|
||
var from_entry := {}
|
||
for eid in entries:
|
||
_mark_reachable(int(eid), outs, from_entry)
|
||
var to_terminal := {}
|
||
for tid in terminals:
|
||
_mark_backward(int(tid), outs, to_terminal)
|
||
for nid in by_id.keys():
|
||
if not from_entry.has(nid):
|
||
errs.append({"category": "structural", "message": "节点 %d 从任何入口都不可达(孤儿)" % nid})
|
||
if not to_terminal.has(nid):
|
||
errs.append({"category": "structural", "message": "节点 %d 沿连线到不了任何终点(断头)" % nid})
|
||
return errs
|
||
|
||
|
||
# 正向可达集(含起点)
|
||
static func _mark_reachable(start: int, outs: Dictionary, seen: Dictionary) -> void:
|
||
var queue: Array[Variant] = [start]
|
||
seen[start] = true
|
||
while not queue.is_empty():
|
||
var cur: int = queue.pop_front()
|
||
for tid in outs[cur]:
|
||
if not seen.has(int(tid)):
|
||
seen[int(tid)] = true
|
||
queue.append(int(tid))
|
||
|
||
|
||
# 逆向可达集(含起点)——反向邻接由 outs 现推(图规模小,禁第二份邻接表持有)
|
||
static func _mark_backward(target: int, outs: Dictionary, seen: Dictionary) -> void:
|
||
var queue: Array[Variant] = [target]
|
||
seen[target] = true
|
||
while not queue.is_empty():
|
||
var cur: int = queue.pop_front()
|
||
for nid in outs.keys():
|
||
if (outs[nid] as Array).has(cur) and not seen.has(int(nid)):
|
||
seen[int(nid)] = true
|
||
queue.append(int(nid))
|