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:
1 parent
8ffc5552f1
commit
3f73e16366
14 files changed
+471
-207
No files matched your search
@@ -62,7 +62,8 @@ var _runner: LevelRunner # 当前关卡行进器(每关新建
|
||||
var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认
|
||||
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
|
||||
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
|
||||
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref) -> void:
|
||||
@@ -91,6 +92,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
|
||||
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零)
|
||||
state = "running"
|
||||
return _start_current_level()
|
||||
@@ -164,7 +166,8 @@ func _start_current_level() -> bool:
|
||||
# 该回调重取 runner 实例),后 present_entry_choice()(入口候选呈现;顺序反了选路口会被呈现层
|
||||
# 的 runner 空防御静默丢弃)。呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。
|
||||
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
|
||||
Callable(self, "apply_rest"), _base_override, true)
|
||||
Callable(self, "apply_rest"), _base_override, true,
|
||||
0 if _first_script else -1) # 首次脚本 = 每关固定模板 0 零抽签(v0.4 拍板点①);否则逐关抽签
|
||||
if ok:
|
||||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||||
_runner.present_entry_choice()
|
||||
|
||||
+18
-9
@@ -169,15 +169,24 @@ func _cross_validate(loaded: Dictionary) -> void:
|
||||
en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true
|
||||
for lrow in lv.rows:
|
||||
var li: int = int(lrow.level_index)
|
||||
for node in lrow.nodes:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
|
||||
if not bg_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
# 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes)
|
||||
var node_lists: Array = []
|
||||
var tpls: Array = lrow.get("templates", [])
|
||||
if tpls.is_empty():
|
||||
node_lists.append(lrow.get("nodes", []))
|
||||
else:
|
||||
for tpl in tpls:
|
||||
node_lists.append(tpl.get("nodes", []))
|
||||
for nodes_arr in node_lists:
|
||||
for node in nodes_arr:
|
||||
var t: String = String(node.get("type", ""))
|
||||
if t != "battle" and t != "boss":
|
||||
continue
|
||||
var key := "%d_%d" % [li, int(node.get("node_depth", -1))]
|
||||
if not bg_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key])
|
||||
if not en_keys.has(key):
|
||||
push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key])
|
||||
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
|
||||
var skt = loaded.get("skill")
|
||||
if skt != null and en != null:
|
||||
|
||||
@@ -110,7 +110,7 @@ func restore_hero_base() -> void:
|
||||
|
||||
# ---- 关卡生命周期 ----
|
||||
|
||||
func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}) -> bool:
|
||||
func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}, template_pick: int = -1) -> bool:
|
||||
# 单关测试模式(旧路径——既有语义一字不改):私有账本、内置恢复数学、自调 new_adventure(漂移①
|
||||
# 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure)
|
||||
# base_override_in = 永久强化 flat 注入尾参(换算率标定批 2026-09-23——缺省 {} 全零白板,
|
||||
@@ -124,7 +124,6 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
return false
|
||||
level_row = row
|
||||
node_index = -1
|
||||
level_result = ""
|
||||
_awaiting_choice = false
|
||||
@@ -143,6 +142,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
break
|
||||
hero_hp = effective_max_hp()
|
||||
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
|
||||
level_row = _resolve_template_row(row, template_pick) # 池解析在播种后(node_map 抽签须已播种——EC7)
|
||||
_running = true
|
||||
var entry := _entry_node()
|
||||
if entry.is_empty():
|
||||
@@ -166,7 +166,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
|
||||
# _enter_node(entry)——与旧自动进入逐位等价(测量面零漂移红线)。
|
||||
func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary,
|
||||
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {},
|
||||
defer_entry: bool = false) -> bool:
|
||||
defer_entry: bool = false, template_pick: int = -1) -> bool:
|
||||
var row := _find_level_row(level_index)
|
||||
if row.is_empty():
|
||||
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
|
||||
@@ -177,7 +177,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
|
||||
base_override = base_override_ref
|
||||
_shop_purchased = purchased_ref
|
||||
_rest_handler = rest_handler
|
||||
level_row = row
|
||||
level_row = _resolve_template_row(row, template_pick)
|
||||
node_index = -1
|
||||
level_result = ""
|
||||
_awaiting_choice = false
|
||||
@@ -218,6 +218,30 @@ func _find_level_row(level_index: int) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
# ---- 模板池解析(关卡节点图生成 v0.4——M-3 迭代 5 T5)----
|
||||
# row.templates 非空 = 池形态:template_pick >= 0 = 强制取该模板(首次脚本模式/冒烟固定图,零 RNG 消费);
|
||||
# -1 = 抽签(node_map 用途标签恰一 scope,均匀取模)。空池 = 旧单图形态原样返回(向后兼容零迁移)。
|
||||
# 解析产物 = row 副本换 nodes(下游 nodes()/校验/呈现全透明);active_template_name 供 MC 分层出数。
|
||||
var active_template_name := ""
|
||||
|
||||
func _resolve_template_row(row: Dictionary, template_pick: int) -> Dictionary:
|
||||
var templates: Array = row.get("templates", [])
|
||||
active_template_name = ""
|
||||
if templates.is_empty():
|
||||
return row
|
||||
var idx := 0
|
||||
if template_pick >= 0:
|
||||
idx = clampi(template_pick, 0, templates.size() - 1)
|
||||
else:
|
||||
var stream: RandomNumberGenerator = rng.open_stream("node_map")
|
||||
idx = stream.randi_range(0, templates.size() - 1)
|
||||
var tpl: Dictionary = templates[idx]
|
||||
active_template_name = String(tpl.get("display_name", ""))
|
||||
var out := row.duplicate(true)
|
||||
out["nodes"] = tpl.get("nodes", [])
|
||||
return out
|
||||
|
||||
|
||||
func nodes() -> Array:
|
||||
return level_row.get("nodes", [])
|
||||
|
||||
|
||||
@@ -116,6 +116,8 @@ func refresh_workshop_unlocks() -> Array:
|
||||
# 明细 {big_reward, boss_drops: [{level, first, amount}], total, new_unlocks};
|
||||
# 入账与标记经 save.apply_adventure_summary(boss_drops 副本随摘要入 recent_summaries)。
|
||||
func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
|
||||
save.mark_first_adventure_done()
|
||||
var drops: Array = []
|
||||
for li in range(1, int(summary.get("levels_cleared", 0)) + 1):
|
||||
var first: bool = not save.boss_first_kill_marked(li)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
extends Node
|
||||
|
||||
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
|
||||
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop"]
|
||||
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5——标签不参与派生只进事件日志,派生/黄金向量零改动)
|
||||
|
||||
# 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
|
||||
@@ -54,10 +54,23 @@ func _default_data() -> Dictionary:
|
||||
"level_clear_counts": {"1": 0, "2": 0, "3": 0},
|
||||
"boss_first_kill": {},
|
||||
"recent_summaries": [],
|
||||
"first_adventure_done": false,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ---- 首次冒险布尔位(关卡节点图生成 v0.4——唯一新增存档字段,M-3 迭代 5 T5)----
|
||||
# false = 首次脚本模式(全关零抽签取池内模板 0=现行固定图);置位只在冒险收束挂点(MetaProgression.settle_adventure)。
|
||||
# 老档迁移:缺字段 = 该档已有游玩史 → true(不重放教学脚本)。
|
||||
|
||||
func is_first_adventure_done() -> bool:
|
||||
return bool(_data["meta"].get("first_adventure_done", true))
|
||||
|
||||
|
||||
func mark_first_adventure_done() -> void:
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
|
||||
|
||||
# ---- 读盘(启动时调用)----
|
||||
# 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。
|
||||
# 损坏兜底:原文件改名 <path>.corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。
|
||||
@@ -91,6 +104,9 @@ func load_from_disk() -> bool:
|
||||
parsed = stepped
|
||||
v = int(parsed.get("version", v + 1))
|
||||
_data = _normalize(parsed)
|
||||
# 老档迁移(关卡节点图生成 v0.4 布尔位——缺字段 = 已有游玩史,不重放教学脚本)
|
||||
if not _data["meta"].has("first_adventure_done"):
|
||||
_data["meta"]["first_adventure_done"] = true
|
||||
return true
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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(生成前自校验,一次性内容批工具随批入库留档)。
|
||||
@@ -242,7 +242,9 @@ func _start_adventure() -> void:
|
||||
logger.start_session(seed_val)
|
||||
adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
|
||||
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}}):
|
||||
# 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签
|
||||
var first_script: bool = not save_manager.is_first_adventure_done()
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script}):
|
||||
_fatal_screen("冒险启动失败——见输出日志")
|
||||
return
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ func _initialize() -> void:
|
||||
"encounter_max_hp_seq": st["encounter_max_hp_seq"],
|
||||
"reward_events": st["reward_events"],
|
||||
"shop_gp_seq": st["shop_gp_seq"],
|
||||
"level_templates": st.get("level_templates", []),
|
||||
"wallet_count": (st["wallet_events"] as Array).size(),
|
||||
}
|
||||
var rs := String(sum.get("result", ""))
|
||||
|
||||
@@ -25,7 +25,7 @@ extends RefCounted
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现
|
||||
|
||||
|
||||
static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min") -> Dictionary:
|
||||
static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min", template_pick: int = -1) -> Dictionary:
|
||||
var own_session := logger == null
|
||||
if own_session:
|
||||
logger = load("res://presentation/playtest_logger.gd").new()
|
||||
@@ -47,7 +47,7 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
|
||||
runner.node_entered.connect(_on_node_entered.bind(runner, st, logger))
|
||||
runner.level_finished.connect(_on_level_finished.bind(st, logger))
|
||||
runner.set_hero_override(atk_pct, hp_pct)
|
||||
if not runner.start_level(level_index, seed_val):
|
||||
if not runner.start_level(level_index, seed_val, {}, template_pick):
|
||||
st.result = "start_failed"
|
||||
runner.restore_hero_base()
|
||||
if own_session:
|
||||
@@ -118,6 +118,8 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
"visited_ids": [], "choices": 0, "last_battle_enemies": [],
|
||||
"summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [],
|
||||
"shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面)
|
||||
|
||||
"level_templates": [], # 逐关图模板名(关卡节点图生成 v0.4 分层出数)
|
||||
}
|
||||
adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows))
|
||||
adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
|
||||
@@ -125,7 +127,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr))
|
||||
adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger))
|
||||
adr.set_hero_override(atk_pct, hp_pct)
|
||||
if not adr.start_adventure(seed_val, {"hero_override": hero_override}):
|
||||
if not adr.start_adventure(seed_val, {"hero_override": hero_override, "first_script": false}): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数)
|
||||
st.result = "start_failed"
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
@@ -152,6 +154,10 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
var r = adr.runner()
|
||||
var t := String(node.get("type", ""))
|
||||
st.visited_ids.append(int(r.node_id_of(node)))
|
||||
if not st.has("level_templates"):
|
||||
st["level_templates"] = []
|
||||
st["level_templates"].append(r.active_template_name) # MC 分层出数(关卡×图模板——v0.4 测量对账条款)
|
||||
|
||||
if t == "battle" or t == "boss":
|
||||
st.battles += 1
|
||||
st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇)
|
||||
|
||||
@@ -107,6 +107,84 @@ func _run_core_smoke(db, rng) -> void:
|
||||
_smoke_adventure()
|
||||
_smoke_save()
|
||||
_smoke_meta()
|
||||
_smoke_node_map_templates()
|
||||
|
||||
|
||||
# ---- 关卡节点图生成·模板池冒烟(M-3 迭代 5 T5——v0.4 §P0.6 断言面)----
|
||||
func _smoke_node_map_templates() -> void:
|
||||
# ① 扩签:node_map 入用途标签封闭词表(派生零改动——标签只进事件日志)
|
||||
var svc = load("res://core/rng_service.gd").new()
|
||||
check((svc.PURPOSE_TAGS as Array).has("node_map"), "node_map 标签入 rng_service 封闭词表(扩签通道=改代码+冒烟守卫)")
|
||||
# ② 池参数:三关各 5 模板、模板 0 存在(回滚档成员)
|
||||
for li in range(1, 4):
|
||||
var row := {}
|
||||
for r in _db.level.rows:
|
||||
if int(r.level_index) == li:
|
||||
row = r
|
||||
var tpls: Array = row.get("templates", [])
|
||||
check(tpls.size() == 5, "L%d 模板池 = 5 张(拍板点④——首版池规模)" % li)
|
||||
check(not tpls.is_empty() and String(tpls[0].get("display_name", "")).ends_with("(现行)"),
|
||||
"L%d 模板 0 = 现行固定图(回滚档成员)" % li)
|
||||
# ③ 强制取 0 = 模板 0 节点表逐位相等 + 零 scope 消费
|
||||
var rng0 = load("res://core/rng_service.gd").new()
|
||||
rng0.new_adventure(910001)
|
||||
var runner0 = load("res://core/level_runner.gd").new(_db, rng0)
|
||||
var c_before: int = rng0.counter()
|
||||
check(runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, {},
|
||||
Callable(self, "_smoke_rest_stub"), {}, true, 0), "强制 template_pick=0 起关受理")
|
||||
check(rng0.counter() == c_before, "强制取模板零 RNG 消耗(零抽签——首次脚本语义)")
|
||||
var tpl0: Dictionary = {}
|
||||
for r in _db.level.rows:
|
||||
if int(r.level_index) == 2:
|
||||
tpl0 = r.get("templates", [])[0]
|
||||
check(runner0.level_row.get("nodes", []) == tpl0.get("nodes", []),
|
||||
"强制 0 的生效节点表 = 模板 0 逐位相等(回滚档成立)")
|
||||
# ④ 抽签确定性:同种子两实例同模板(同种子同操作序列 → 逐位同图)
|
||||
var names := []
|
||||
for i in range(2):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.new_adventure(910002)
|
||||
var rn = load("res://core/level_runner.gd").new(_db, rg)
|
||||
rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, {},
|
||||
Callable(self, "_smoke_rest_stub"), {}, true, -1)
|
||||
names.append(rn.active_template_name)
|
||||
check(names[0] == names[1] and String(names[0]) != "",
|
||||
"抽签确定性:同种子逐位同图(实测 %s)" % String(names[0]))
|
||||
# ⑤ N=1 池与现行固定图等价:单模板行过图校验(抽签取模 1 恒 0 = 回滚档)
|
||||
var lt = load("res://data/schemas/level_table.gd").new()
|
||||
lt.rows = [{"level_index": 9, "display_name": "t", "rest_heal_pct": 30,
|
||||
"templates": [tpl0]}]
|
||||
check(lt.validate().is_empty(), "N=1 池表通过图校验(回滚档形态合法)")
|
||||
# ⑥ 首次脚本分支:first_script=true 全关模板 0;false = 抽签(scope 恰一差异)
|
||||
var rngF = load("res://core/rng_service.gd").new()
|
||||
rngF.new_adventure(910003)
|
||||
var adrF = load("res://core/adventure_runner.gd").new(_db, rngF)
|
||||
adrF.start_adventure(910003, {"first_script": true})
|
||||
check(String(adrF.runner().active_template_name).ends_with("(现行)"),
|
||||
"首次脚本模式:L1 取模板 0(全关零抽签——存档布尔位 false 语义)")
|
||||
var cF: int = rngF.counter()
|
||||
var rngT = load("res://core/rng_service.gd").new()
|
||||
rngT.new_adventure(910003)
|
||||
var adrT = load("res://core/adventure_runner.gd").new(_db, rngT)
|
||||
adrT.start_adventure(910003, {"first_script": false})
|
||||
check(rngT.counter() == cF + 1, "抽签模式恰多消费 1 scope(node_map 逐关恰一——L1 起关抽签)")
|
||||
check(String(adrT.runner().active_template_name) != "",
|
||||
"抽签模式 active_template_name 有值(MC 分层出数取数面在位)")
|
||||
# ⑦ 收束置位:新档 false → settle 置 true 恰一次(幂等)
|
||||
DirAccess.remove_absolute("user://smoke_node_map_save.json") # 测试隔离——上轮 settle 写盘的残留档先删
|
||||
var sv = load("res://core/save_manager.gd").new("user://smoke_node_map_save.json")
|
||||
sv.load_from_disk()
|
||||
check(sv.is_first_adventure_done() == false, "新档首次冒险布尔位 = false(默认域)")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
mp.settle_adventure({"levels_cleared": 1, "keystone_total": 10, "cleared": false, "result": "failed"})
|
||||
check(sv.is_first_adventure_done() == true, "冒险收束点置位(三结局同写——写入只在收束挂点)")
|
||||
mp.settle_adventure({"levels_cleared": 0, "keystone_total": 0, "cleared": false, "result": "failed"})
|
||||
check(sv.is_first_adventure_done() == true, "置位幂等(重复收束不翻转)")
|
||||
|
||||
|
||||
# 冒烟恢复执行体(LevelRunner 注入用——与 AdventureRunner.apply_rest 同式占位)
|
||||
func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int:
|
||||
return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp)
|
||||
|
||||
|
||||
# ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)----
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":29,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":24,"route":"min","seed":78000,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":9,"choices":9,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":57,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":3,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":44,"route":"min","seed":78001,"shop_gp_seq":[],"stopped_at":2,"visited_count":11,"wallet_count":10}
|
||||
{"atk_pct":0,"battles":4,"choices":4,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":18,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)"],"levels_cleared":0,"mine_hits":0,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":10,"route":"min","seed":78002,"shop_gp_seq":[],"stopped_at":1,"visited_count":5,"wallet_count":5}
|
||||
{"atk_pct":0,"battles":8,"choices":9,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25],"hero_dmg":74,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":3,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":56,"route":"min","seed":78003,"shop_gp_seq":[],"stopped_at":2,"visited_count":11,"wallet_count":9}
|
||||
{"atk_pct":0,"battles":4,"choices":5,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":16,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":5,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":10,"route":"min","seed":78004,"shop_gp_seq":[],"stopped_at":1,"visited_count":6,"wallet_count":5}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":28,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":18,"route":"min","seed":78005,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":3,"choices":3,"cleared":false,"encounter_max_hp_seq":[20,20,20],"hero_dmg":24,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":15,"route":"min","seed":78006,"shop_gp_seq":[],"stopped_at":1,"visited_count":4,"wallet_count":4}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":17,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":19,"route":"min","seed":78007,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":26,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":4,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":23,"route":"min","seed":78008,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":36,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":27,"route":"min","seed":78009,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":4,"choices":5,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":28,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡"],"levels_cleared":0,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":10,"route":"min","seed":78010,"shop_gp_seq":[],"stopped_at":1,"visited_count":6,"wallet_count":5}
|
||||
{"atk_pct":0,"battles":3,"choices":4,"cleared":false,"encounter_max_hp_seq":[20,20,20],"hero_dmg":24,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战"],"levels_cleared":0,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":14,"route":"min","seed":78011,"shop_gp_seq":[],"stopped_at":1,"visited_count":5,"wallet_count":4}
|
||||
{"atk_pct":0,"battles":4,"choices":5,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":23,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":2,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":11,"route":"min","seed":78012,"shop_gp_seq":[],"stopped_at":1,"visited_count":6,"wallet_count":5}
|
||||
{"atk_pct":0,"battles":11,"choices":13,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25],"hero_dmg":73,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":52,"route":"min","seed":78013,"shop_gp_seq":[69],"stopped_at":2,"visited_count":15,"wallet_count":12}
|
||||
{"atk_pct":0,"battles":10,"choices":12,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25],"hero_dmg":76,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":6,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":48,"route":"min","seed":78014,"shop_gp_seq":[76],"stopped_at":2,"visited_count":14,"wallet_count":11}
|
||||
{"atk_pct":0,"battles":3,"choices":3,"cleared":false,"encounter_max_hp_seq":[20,20,20],"hero_dmg":27,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移"],"levels_cleared":0,"mine_hits":0,"policy":"normal","refreshes":0,"result":"failed","reward_events":[],"rounds":6,"route":"min","seed":78015,"shop_gp_seq":[],"stopped_at":1,"visited_count":4,"wallet_count":4}
|
||||
{"atk_pct":0,"battles":9,"choices":9,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":56,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":4,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":49,"route":"min","seed":78016,"shop_gp_seq":[],"stopped_at":2,"visited_count":11,"wallet_count":10}
|
||||
{"atk_pct":0,"battles":9,"choices":10,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":60,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":1,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":45,"route":"min","seed":78017,"shop_gp_seq":[],"stopped_at":2,"visited_count":12,"wallet_count":10}
|
||||
{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":55,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":3,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":31,"route":"min","seed":78018,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8}
|
||||
{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":42,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":4,"policy":"normal","refreshes":0,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":35,"route":"min","seed":78019,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":23,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[],"rounds":21,"route":"min","seed":78020,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":30,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[],"rounds":20,"route":"min","seed":78021,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":11,"choices":13,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30],"hero_dmg":91,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","幽暗地域·休整远路","灰矮人矿城·主竖井(现行)","灰矮人矿城·主竖井(现行)"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":60,"route":"min","seed":78022,"shop_gp_seq":[59],"stopped_at":3,"visited_count":16,"wallet_count":12}
|
||||
{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":55,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":42,"route":"min","seed":78023,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8}
|
||||
{"atk_pct":0,"battles":8,"choices":8,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25],"hero_dmg":69,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":46,"route":"min","seed":78024,"shop_gp_seq":[],"stopped_at":2,"visited_count":10,"wallet_count":9}
|
||||
{"atk_pct":0,"battles":10,"choices":11,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25],"hero_dmg":64,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":52,"route":"min","seed":78025,"shop_gp_seq":[],"stopped_at":2,"visited_count":13,"wallet_count":11}
|
||||
{"atk_pct":0,"battles":11,"choices":13,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25],"hero_dmg":105,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":58,"route":"min","seed":78026,"shop_gp_seq":[90],"stopped_at":2,"visited_count":15,"wallet_count":12}
|
||||
{"atk_pct":0,"battles":11,"choices":12,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25],"hero_dmg":98,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","地精洞窟·休整前移","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层","幽暗地域·直入深层"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":4,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":71,"route":"min","seed":78027,"shop_gp_seq":[95],"stopped_at":2,"visited_count":14,"wallet_count":12}
|
||||
{"atk_pct":0,"battles":5,"choices":6,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20],"hero_dmg":18,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[],"rounds":19,"route":"min","seed":78028,"shop_gp_seq":[],"stopped_at":1,"visited_count":7,"wallet_count":6}
|
||||
{"atk_pct":0,"battles":4,"choices":4,"cleared":false,"encounter_max_hp_seq":[20,20,20,20],"hero_dmg":25,"hp_pct":0,"keystone_total":0,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)"],"levels_cleared":0,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[],"rounds":14,"route":"min","seed":78029,"shop_gp_seq":[],"stopped_at":1,"visited_count":5,"wallet_count":5}
|
||||
{"atk_pct":0,"battles":13,"choices":17,"cleared":true,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30,30,30],"hero_dmg":118,"hp_pct":0,"keystone_total":30,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路"],"levels_cleared":3,"mine_hits":0,"policy":"perfect","refreshes":5,"result":"completed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":92,"route":"min","seed":78030,"shop_gp_seq":[57,107],"stopped_at":0,"visited_count":20,"wallet_count":14}
|
||||
{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":56,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":34,"route":"min","seed":78031,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8}
|
||||
{"atk_pct":0,"battles":9,"choices":11,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":66,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","地精洞窟·缓入长坡","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":42,"route":"min","seed":78032,"shop_gp_seq":[49],"stopped_at":2,"visited_count":13,"wallet_count":10}
|
||||
{"atk_pct":0,"battles":12,"choices":15,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30,30],"hero_dmg":145,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路","灰矮人矿城·休整远路"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":5,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":78,"route":"min","seed":78033,"shop_gp_seq":[58],"stopped_at":3,"visited_count":18,"wallet_count":13}
|
||||
{"atk_pct":0,"battles":9,"choices":10,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":55,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":42,"route":"min","seed":78034,"shop_gp_seq":[],"stopped_at":2,"visited_count":12,"wallet_count":10}
|
||||
{"atk_pct":0,"battles":12,"choices":14,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,30,30,30],"hero_dmg":108,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)","灰矮人矿城·双线归束","灰矮人矿城·双线归束","灰矮人矿城·双线归束","灰矮人矿城·双线归束"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":70,"route":"min","seed":78035,"shop_gp_seq":[65],"stopped_at":3,"visited_count":17,"wallet_count":13}
|
||||
{"atk_pct":0,"battles":9,"choices":10,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25],"hero_dmg":70,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","地精洞窟·先赏后战","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门","幽暗地域·奖励窄门"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":2,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":46,"route":"min","seed":78036,"shop_gp_seq":[],"stopped_at":2,"visited_count":12,"wallet_count":10}
|
||||
{"atk_pct":0,"battles":7,"choices":7,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25],"hero_dmg":51,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·四岔环廊(现行)","幽暗地域·四岔环廊(现行)"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":1,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":36,"route":"min","seed":78037,"shop_gp_seq":[],"stopped_at":2,"visited_count":9,"wallet_count":8}
|
||||
{"atk_pct":0,"battles":14,"choices":15,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25,25,30,30,30],"hero_dmg":122,"hp_pct":0,"keystone_total":20,"level_templates":["地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","地精洞窟·连战直入","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","灰矮人矿城·直入深层","灰矮人矿城·直入深层","灰矮人矿城·直入深层"],"levels_cleared":2,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25},{"from_level":2,"hp_bonus":5,"keystone_gained":10,"keystone_total":20,"new_max_hp":30}],"rounds":88,"route":"min","seed":78038,"shop_gp_seq":[94],"stopped_at":3,"visited_count":18,"wallet_count":15}
|
||||
{"atk_pct":0,"battles":10,"choices":11,"cleared":false,"encounter_max_hp_seq":[20,20,20,20,20,25,25,25,25,25],"hero_dmg":76,"hp_pct":0,"keystone_total":10,"level_templates":["地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","地精洞窟·主线洞道(现行)","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束","幽暗地域·双线归束"],"levels_cleared":1,"mine_hits":0,"policy":"perfect","refreshes":3,"result":"failed","reward_events":[{"from_level":1,"hp_bonus":5,"keystone_gained":10,"keystone_total":10,"new_max_hp":25}],"rounds":50,"route":"min","seed":78039,"shop_gp_seq":[],"stopped_at":2,"visited_count":13,"wallet_count":11}
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""T5 骨架池生成器(M-3 迭代 5——一次性内容批工具,生成 game/data/tables/level.tres 模板池)。
|
||||
|
||||
15 张骨架 = 3 关 × 5(拍板点④),模板 0 = 现行固定图(回滚档成员)。
|
||||
约束:战斗/Boss node_depth = 遭遇表行;图形态 id = node_depth → L2/L3 战斗 ∈ {0,1,4,5,6,7}、Boss=9;
|
||||
L1 图形态两不可全(Boss 须最大 id vs 深度 4)→ L1 单链排列变体(战斗深度序单调)。
|
||||
商店房每关恰 1、Boss 前必经位(L1 设计态 = 零商店——商店房批教学期修订面)。
|
||||
"""
|
||||
L1_BATTLES = [0, 1, 2, 3]
|
||||
L1_BOSS = 4
|
||||
L2_BATTLES = [0, 1, 4, 5, 6, 7]
|
||||
|
||||
Q = '"' # 引号常量(避免嵌套三引号)
|
||||
|
||||
|
||||
def chain(seq):
|
||||
nodes = []
|
||||
for t, d in seq:
|
||||
n = {"type": t}
|
||||
if d is not None:
|
||||
n["node_depth"] = d
|
||||
nodes.append(n)
|
||||
return nodes
|
||||
|
||||
|
||||
def graph(spec):
|
||||
nodes = []
|
||||
for i in sorted(spec.keys()):
|
||||
t, d, nxt = spec[i]
|
||||
nodes.append({"node_depth": i, "type": t, "next": list(nxt)})
|
||||
return nodes
|
||||
|
||||
|
||||
L1_TEMPLATES = [
|
||||
("地精洞窟·主线洞道(现行)", chain([("battle", 0), ("battle", 1), ("reward", None),
|
||||
("battle", 2), ("battle", 3), ("rest", None), ("boss", 4)])),
|
||||
("地精洞窟·休整前移", chain([("battle", 0), ("battle", 1), ("rest", None),
|
||||
("battle", 2), ("reward", None), ("battle", 3), ("boss", 4)])),
|
||||
("地精洞窟·先赏后战", chain([("battle", 0), ("reward", None), ("battle", 1), ("rest", None),
|
||||
("battle", 2), ("battle", 3), ("boss", 4)])),
|
||||
("地精洞窟·连战直入", chain([("battle", 0), ("battle", 1), ("reward", None), ("rest", None),
|
||||
("battle", 2), ("battle", 3), ("boss", 4)])),
|
||||
("地精洞窟·缓入长坡", chain([("battle", 0), ("rest", None), ("battle", 1), ("reward", None),
|
||||
("battle", 2), ("battle", 3), ("boss", 4)])),
|
||||
]
|
||||
|
||||
|
||||
def w0():
|
||||
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [3, 4]), 2: ("reward", 2, [4, 5]),
|
||||
3: ("rest", 3, [6]), 4: ("battle", 4, [6, 7]), 5: ("battle", 5, [7]),
|
||||
6: ("battle", 6, [8]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
|
||||
|
||||
|
||||
def w1():
|
||||
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [3, 4]), 2: ("reward", 2, [4]),
|
||||
3: ("rest", 3, [5]), 4: ("battle", 4, [5, 6]), 5: ("battle", 5, [7]),
|
||||
6: ("battle", 6, [8]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
|
||||
|
||||
|
||||
def w2():
|
||||
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [4]), 2: ("reward", 2, [3, 4]),
|
||||
3: ("rest", 3, [6]), 4: ("battle", 4, [5, 6]), 5: ("battle", 5, [7]),
|
||||
6: ("battle", 6, [7]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
|
||||
|
||||
|
||||
def w3():
|
||||
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [2, 4]), 2: ("reward", 2, [3]),
|
||||
3: ("rest", 3, [6]), 4: ("battle", 4, [5, 6]), 5: ("battle", 5, [7]),
|
||||
6: ("battle", 6, [8]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
|
||||
|
||||
|
||||
def w4():
|
||||
return graph({0: ("battle", 0, [1, 2]), 1: ("battle", 1, [3]), 2: ("reward", 2, [3, 4]),
|
||||
3: ("rest", 3, [4, 5]), 4: ("battle", 4, [6]), 5: ("battle", 5, [6]),
|
||||
6: ("battle", 6, [7]), 7: ("battle", 7, [8]), 8: ("shop", 8, [9]), 9: ("boss", 9, [])})
|
||||
|
||||
|
||||
L2_WIRINGS = [("幽暗地域·四岔环廊(现行)", w0()), ("幽暗地域·奖励窄门", w1()),
|
||||
("幽暗地域·直入深层", w2()), ("幽暗地域·休整远路", w3()), ("幽暗地域·双线归束", w4())]
|
||||
L3_WIRINGS = [("灰矮人矿城·主竖井(现行)", w0()), ("灰矮人矿城·奖励窄门", w1()),
|
||||
("灰矮人矿城·直入深层", w2()), ("灰矮人矿城·休整远路", w3()), ("灰矮人矿城·双线归束", w4())]
|
||||
|
||||
|
||||
def validate(name, nodes, battles, boss_depth, has_shop):
|
||||
errs = []
|
||||
is_graph = any("next" in n for n in nodes)
|
||||
ids = ([n["node_depth"] for n in nodes] if is_graph else list(range(len(nodes))))
|
||||
if len(set(ids)) != len(ids):
|
||||
errs.append("编号重复")
|
||||
adj = {}
|
||||
for i, n in enumerate(nodes):
|
||||
if is_graph:
|
||||
nxt = list(n.get("next", []))
|
||||
else:
|
||||
nxt = [i + 1] if i < len(nodes) - 1 else []
|
||||
adj[ids[i]] = nxt
|
||||
for src, nxt in adj.items():
|
||||
for tid in nxt:
|
||||
if tid not in ids:
|
||||
errs.append("悬空出边 %d→%d" % (src, tid))
|
||||
elif tid <= src:
|
||||
errs.append("非前进边 %d→%d" % (src, tid))
|
||||
if is_graph:
|
||||
inbound = set()
|
||||
for nxt in adj.values():
|
||||
inbound |= set(nxt)
|
||||
entries = [x for x in ids if x not in inbound]
|
||||
if len(entries) != 1:
|
||||
errs.append("入口数 %d" % len(entries))
|
||||
reach, frontier = set(), list(entries)
|
||||
while frontier:
|
||||
cur = frontier.pop()
|
||||
if cur in reach:
|
||||
continue
|
||||
reach.add(cur)
|
||||
frontier += list(adj.get(cur, []))
|
||||
if reach != set(ids):
|
||||
errs.append("存在不可达节点: %s" % (set(ids) - reach))
|
||||
bt = sorted(n["node_depth"] for n in nodes if n["type"] in ("battle", "boss"))
|
||||
if bt != sorted(battles + [boss_depth]):
|
||||
errs.append("战斗深度集合不符: %s" % bt)
|
||||
shops = [n for n in nodes if n["type"] == "shop"]
|
||||
if len(shops) != (1 if has_shop else 0):
|
||||
errs.append("商店房数 %d" % len(shops))
|
||||
if has_shop:
|
||||
sid = shops[0]["node_depth"]
|
||||
if is_graph:
|
||||
if sid != boss_depth - 1 or adj[sid] != [boss_depth]:
|
||||
errs.append("商店房非 Boss 前必经位")
|
||||
elif nodes[-2]["type"] != "shop":
|
||||
errs.append("单链商店房非 Boss 前一位")
|
||||
return errs
|
||||
|
||||
|
||||
all_ok = True
|
||||
for name, nodes in L1_TEMPLATES:
|
||||
e = validate(name, nodes, L1_BATTLES, L1_BOSS, False)
|
||||
print(("PASS" if not e else "FAIL"), name, e)
|
||||
all_ok &= not e
|
||||
for name, nodes in L2_WIRINGS + L3_WIRINGS:
|
||||
e = validate(name, nodes, L2_BATTLES, 9, True)
|
||||
print(("PASS" if not e else "FAIL"), name, e)
|
||||
all_ok &= not e
|
||||
print("ALL OK" if all_ok else "HAS FAILURES")
|
||||
if not all_ok:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def nodes_json(nodes):
|
||||
parts = []
|
||||
for n in nodes:
|
||||
kv = []
|
||||
if "node_depth" in n:
|
||||
kv.append('"node_depth": %d' % n["node_depth"])
|
||||
kv.append('"type": "%s"' % n["type"])
|
||||
if "next" in n:
|
||||
kv.append('"next": [%s]' % ", ".join(str(x) for x in n["next"]))
|
||||
parts.append("{" + ", ".join(kv) + "}")
|
||||
return "[" + ", ".join(parts) + "]"
|
||||
|
||||
|
||||
def tpl_json(name, nodes):
|
||||
return '{"display_name": "%s", "nodes": %s}' % (name, nodes_json(nodes))
|
||||
|
||||
|
||||
def row_json(li, dn, templates):
|
||||
return ('{"level_index": %d, "display_name": "%s", "rest_heal_pct": 30, "templates": [%s]}'
|
||||
% (li, dn, ", ".join(tpl_json(n, ns) for n, ns in templates)))
|
||||
|
||||
|
||||
rows = [row_json(1, "第一层 · 地精洞窟", L1_TEMPLATES),
|
||||
row_json(2, "第二层 · 幽暗地域", L2_WIRINGS),
|
||||
row_json(3, "第三层 · 灰矮人矿城", L3_WIRINGS)]
|
||||
|
||||
header = (
|
||||
"[gd_resource type=\"Resource\" script_class=\"LevelTable\" load_steps=2 format=3]\n"
|
||||
"\n"
|
||||
"[ext_resource type=\"Script\" path=\"res://data/schemas/level_table.gd\" id=\"1\"]\n"
|
||||
"\n"
|
||||
"[resource]\n"
|
||||
"script = ExtResource(\"1\")\n"
|
||||
"rows = [\n")
|
||||
|
||||
footer = (
|
||||
"\n]\n"
|
||||
"\n"
|
||||
"; 关卡节点图生成 v0.4 台阶一内容批(M-3 迭代 5 T5,2026-09-28——15 张骨架 = 3 关 × 5,拍板点④):\n"
|
||||
"; 模板 0 = 现行固定图(回滚档成员——首次冒险脚本/池缩 N=1 时逐位等价迁移前行为);\n"
|
||||
"; L1 五模板 = 单链排列变体(图形态两不可全:Boss 须最大编号 vs Boss 遭遇深度 4——战斗深度序单调\n"
|
||||
"; 保持难度坡,变化面 = 奖励/恢复位次);L2/L3 五模板 = 网状图变体(战斗节点编号恒 ∈ 遭遇表行集\n"
|
||||
"; {0,1,4,5,6,7}、Boss 9 最大编号、商店房 8→Boss 必经位承袭;遭遇表按编号共用——零新增敌人数值)。\n"
|
||||
"; 池内全模板加载期校验(level_table.gd validate + config_db 跨表校验模板感知);\n"
|
||||
"; 行为读 LevelRunner._resolve_template_row(node_map 标签恰一 scope 抽签 / template_pick 强制)。\n"
|
||||
"; 生成器 = tools/t5_build_templates.py(生成前自校验,一次性内容批工具随批入库留档)。\n")
|
||||
|
||||
with open("game/data/tables/level.tres", "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(header + ",\n".join(rows) + footer)
|
||||
print("level.tres written")
|
||||
Reference in new issue
Block a user