refactor(迭代35卡②T2·二口+T3 S1/S2/S6/S8): 消费侧切源落定+断言面换代主体——① level_runner 图源换代(_generator 取代模板解析,_assign_generated_graph 读 level_gen_params 参数档产图并过 LevelTable.validate_graph,违规起关拒绝零兜底)+_entry_nodes 多入口首跳+_node_id 句柄单空间+enc node_depth 取节点 depth 值;② adventure_runner/main/save_manager 模板链路退役(first_script 参数与 template_pick 计算全删,断点 template_pick 键换生成图代差标记键 graph_gen_v2,缺键旧断点 resume 显式拒绝禁静默回放);③ level.tres 三行 templates 键清出(15 张模板数据出表,行级字段留,表尾注释改指参数档+生成器);④ sim/probe 随动(run_level/run_adventure 尾参退役、MC 出数字段 level_templates→level_maps 值=关卡#主种子、探针注释性质化、boss_depth 取节点 depth 键);⑤ 断言面换代:node_map_templates 宿主整段重写为生成图确定性套(纯函数复现/换种子变图/零计数器号位依赖/单一图源/种子 42 两开局同图/断点重生成同图/缺代差键拒绝/表无 templates/收束置位三条保留),breakpoint_resume 新增 P7 六断言(快照无已抽模板键、代差标记、停点句柄与已到序往返、续玩同图、旧断点拒绝、同种子重开局同图),S8 单测随动 first_script 键清退+断点夹具换代+meta 参数面 extra 键样换 treasure_unlocked(语义保持),S2 生成图取数面三宿主替换(level_active_nodes 走参数档+种子 42,消费点深度键换 depth,round_flow 死件 _boss_depth 随删),adventure_feats 覆盖义务段改真实解析链判据(敌侧落带≥请求深度+板侧命中+非空扫描面)、奖励节点段换「图侧含件+真实选路点选呈停兑付」双断言(旧单链必经形状前提随模板消亡,非放宽:经停即呈候选由真实 pick_next_node 入口证明)、商店余额段按每关恰一店投影改三关读数

This commit is contained in:
DKChan committed 2026-10-24 18:59:57 +08:00
1 parent 6566200220
commit 9ee3bea194
22 files changed
+460 -362

No files matched your search

+10 -9
View File
@@ -42,7 +42,7 @@ var _pending_next_level := false # 大奖励已兑付、等 continue_to
var _last_level_payload := {} # 最近一次关卡结束载荷(失败结算的败退节点明细来源——数值验证取数面)
var _override := [0, 0] # 调试浮窗百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
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;全零 = 白板)
# 永久强化 flat 注入(冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
var _last_treasure_cleared := 0 # additive(最近一次关卡末清空计数,summary 取数面)
var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面)
var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner)
@@ -99,7 +99,6 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_base_override = _normalize_override(params.get("hero_override", {}))
hero_attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化)
hero_armor_type = String(params.get("armor_type", ""))
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 缺省 legacy(off 对照零扰动)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
@@ -144,6 +143,11 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
if bp.is_empty():
push_error("[冒险结构] resume_adventure 拒绝——空断点快照")
return false
if int(bp.get("graph_gen_v2", 0)) != 1:
# 旧代差断点(模板池时代的 node_id 与生成图句柄空间可碰撞错位)——显式拒绝,
# 禁按新语义静默回放;旧模板图已随模板池退役,无迁移动作可做(凭空捏图=兜底图红线)。
push_error("[冒险结构] resume_adventure 拒绝——断点缺生成图代差标记(graph_gen_v2),属换代前旧断点")
return false
master_seed = int(bp.get("master_seed", 0))
rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0)))
ledger = {"hero_hp": int(bp.get("hero_hp", 0)), "gp": int(bp.get("gp", 0))}
@@ -191,7 +195,6 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
# 钳制用 flat 已含的保守口径即可,加算区恢复后其余战斗照常)。
ledger["hero_hp"] = mini(int(ledger.get("hero_hp", 0)), maxi(1,
int(round(float(int(db.hero.max_hp) + int(_base_override.get("max_hp", 0))) * (100.0 + float(_override[1])) / 100.0))))
_first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放
_shop_mode = "pool" if String(params.get("shop_mode", "legacy")) == "pool" else "legacy" # 续玩参数面同 start_adventure(缺省 legacy)
_treasure_unlocked = params.get("treasure_unlocked", {}) if params.get("treasure_unlocked", {}) is Dictionary else {}
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
@@ -844,14 +847,12 @@ func _start_current_level() -> bool:
_runner.wallet_changed.connect(_forward_wallet_changed)
_runner.level_finished.connect(_on_level_finished)
_runner.set_hero_override(int(_override[0]), int(_override[1]))
# 断点续玩分支:模板随快照强制(已抽模板零重抽零 RNG)、resume_state 带
# 停点节点与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
# 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解),
# resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。
var resume_state: Dictionary[Variant, Variant] = {}
var template_pick: int = 0 if _first_script else -1 # 首次脚本 = 每关固定模板 0 零抽签;否则逐关抽签
if not _resume_bp.is_empty():
resume_state = {"node_id": int(_resume_bp.get("node_id", -1)),
"visited_ids": _resume_bp.get("visited_ids", []) as Array}
template_pick = int(_resume_bp.get("template_pick", -1))
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)。
# defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
@@ -860,7 +861,7 @@ func _start_current_level() -> bool:
_runner.set_run_ledger(run_ledger) # 共享四账单类下传(run_ledger=四账类,ledger=关卡账)
_runner.sync_attune_inventory() # 能力件骑库存面注入(随账下传同拍)
var ok: bool = _runner.start_level_external(level_index, ledger, purchased,
apply_rest, _base_override, true, template_pick, resume_state)
apply_rest, _base_override, true, resume_state)
if ok:
_runner.set_attunement(_attunement) # 同调装配下传(enc 携带进战斗结算)
_runner.set_feats(_feats) # 特性持有集下传(begin_battle enc.feats 求值面)
@@ -1010,7 +1011,7 @@ func _snapshot_breakpoint() -> void:
"shop_shelf": _runner.shop_bp_shelf() if _runner != null else [], # 货架入断点(离开店/非店停点 = 空架合法态)
"shop_visit_buys": _runner.shop_bp_visit_buys() if _runner != null else 0, # 本次进店已购次数(A 链定价输入)
"attunement_charges": run_ledger.attune_charges.duplicate(true), # 同调主动件充能余量随断点(save v4 域)
"template_pick": _runner.active_template_index if _runner != null else -1,
"graph_gen_v2": 1, # 生成图换代代差标记(缺键 = 换代前旧断点 → resume 显式拒绝)
"master_seed": master_seed,
"rng_counter": rng.counter(),
"keystone": keystone,
+71 -84
View File
@@ -64,7 +64,7 @@ var _phase: int = Phase.IDLE
var level_result := "" # "" / "victory" / "defeat"(外部契约面)
var _candidates: Array[Dictionary] = [] # 当前选路候选(节点引用数组)
var visited: Array[Dictionary] = [] # 已兑付/到达节点序列(呈现层图条数据源;含当前节点)
var _graph_mode := false # 图形态缓存(行集任一节点带 next;单链 = false)
var _generator # 参数化长图生成器(惰性建——NodeMapGenerator 持注入的 RngService)
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
@@ -127,7 +127,7 @@ func restore_hero_base() -> void:
# ---- 关卡生命周期 ----
func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}, template_pick: int = -1) -> bool:
func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = {}) -> bool:
# 单关测试模式:私有账本、内置恢复数学、自调 new_adventure(漂移①
# 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure)
# base_override_in = 永久强化 flat 注入尾参(缺省 {} 全零白板,
@@ -146,34 +146,31 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
_phase = Phase.IDLE
_candidates = []
visited = []
_graph_mode = false
gp = 0 # 冒险钱包清零开局(局内货币生命周期起点)
run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0(单关入口零元进度——注入面见上)
base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat:缺省全零白板(单关入口零元进度——注入面见上)
_shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计)
for k in base_override.keys():
base_override[k] = maxi(0, int(base_override_in.get(k, 0))) # 注入规范化(契约同冒险开始参数面)
for node in nodes():
if node.has("next"):
_graph_mode = true
break
hero_hp = effective_max_hp()
rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构)
level_row.assign(_resolve_template_row(row, template_pick)) # 池解析在播种后(node_map 抽签须已播种——EC7)
_phase = Phase.RUNNING
var entry := _entry_node()
if entry.is_empty():
push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)")
if not _assign_generated_graph(row, seed_val):
_phase = Phase.ENDED
return false
return _enter_node(entry)
_phase = Phase.RUNNING
var entries := _entry_nodes()
if entries.is_empty():
push_error("[关卡节点地图] 入口节点缺失(无入边节点集为空——validate_graph 应已拦)")
_phase = Phase.ENDED
return false
return _enter_node(entries[0])
# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接;
# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased,
# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好)。
# base_override = 永久强化 flat 共享引用(缺省 {} 全零白板,与 B1 空参数路径同值)。
# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)),
# 流程与 start_level 同款(_find_level_row → 生成图装配 → 状态初始化 → _enter_node(入口)),
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
# defer_entry 尾参(缺省 false = 直接进节点)——true 时不自动
# _enter_node(entry),改置选路态(awaiting_choice + 单候选 = 入口节点),由 AdventureRunner 在
@@ -183,7 +180,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,
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {},
defer_entry: bool = false, template_pick: int = -1, resume_state: Dictionary = {}) -> bool:
defer_entry: bool = false, resume_state: Dictionary = {}) -> bool:
var row := _find_level_row(level_index)
if row.is_empty():
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
@@ -193,34 +190,30 @@ func start_level_external(level_index: int, ledger: Dictionary,
base_override = base_override_ref
_shop_purchased = purchased_ref
_rest_handler = rest_handler
level_row.assign(_resolve_template_row(row, template_pick))
if not _assign_generated_graph(row, rng.master_seed):
return false
node_index = -1
level_result = ""
_phase = Phase.IDLE
_candidates = []
visited = []
_graph_mode = false
for node in nodes():
if node.has("next"):
_graph_mode = true
break
_phase = Phase.RUNNING
# 断点续玩路径(defer_entry 之外的第三种停点入口):直接重建到停点所在节点,
# 零 RNG 消耗(模板已随 template_pick 强制、节点状态纯重建不 _enter_node——当场战斗从头打)。
# 零 RNG 消耗(图由主种子+参数档重生成同图、节点状态纯重建不 _enter_node——当场战斗从头打)。
# resume_state = {"node_id": int(-1 = 入口停点), "visited_ids": Array[int]};空字典 = 既有路径逐位不变。
if not resume_state.is_empty():
return _restore_to_stop(int(resume_state.get("node_id", -1)),
resume_state.get("visited_ids", []) as Array)
var entry := _entry_node()
if entry.is_empty():
push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)")
var entries := _entry_nodes()
if entries.is_empty():
push_error("[关卡节点地图] 入口节点缺失(无入边节点集为空——validate_graph 应已拦)")
_phase = Phase.ENDED
return false
if defer_entry:
_phase = Phase.AWAITING_CHOICE # 入口停点(node_index 保持 -1——进节点前无 current_node)
_candidates = [entry]
_phase = Phase.AWAITING_CHOICE # 入口停点(node_index 保持 -1)——候选=全部入口(多入口首跳即选路)
_candidates = entries
return true
return _enter_node(entry)
return _enter_node(entries[0])
# 入口停点呈现(defer_entry=true 的第二步):发出入口候选供呈现层弹
@@ -240,14 +233,14 @@ func present_entry_choice() -> bool:
# 候选 = 停点节点出边(与 choices_presented 停点语义逐位一致)。
func _restore_to_stop(node_id: int, visited_ids: Array) -> bool:
if node_id < 0:
# 入口停点:与 defer_entry 同款形态(node_index 保持 -1,单候选 = 入口节点)
var entry := _entry_node()
if entry.is_empty():
# 入口停点:与 defer_entry 同款形态(node_index 保持 -1,候选 = 全部入口)
var entries := _entry_nodes()
if entries.is_empty():
push_error("[关卡节点地图] 断点恢复失败——入口节点缺失")
_phase = Phase.ENDED
return false
_phase = Phase.AWAITING_CHOICE
_candidates = [entry]
_candidates = entries
return true
var stop: Dictionary[Variant, Variant] = {}
for node in nodes():
@@ -296,32 +289,31 @@ func _find_level_row(level_index: int) -> Dictionary:
return db.level_row(level_index) # 表访问收口(行集直扫归 LevelTable/ConfigDB 访问器)
# ---- 模板池解析(关卡节点图生成 v0.4——M-3)----
# row.templates 非空 = 池形态:template_pick >= 0 = 强制取该模板(首次脚本模式/冒烟固定图,零 RNG 消费);
# -1 = 抽签(node_map 用途标签恰一 scope,均匀取模)。空池 = 旧单图形态原样返回(向后兼容零迁移)。
# 解析产物 = row 副本换 nodes(下游 nodes()/校验/呈现全透明);active_template_name 供 MC 分层出数;
# active_template_index 供断点快照备案(已抽模板续玩零重抽零 RNG 消耗)。
var active_template_name := ""
var active_template_index := -1 # -1 = 非池形态(旧单图)/未解析
func _resolve_template_row(row: Dictionary, template_pick: int) -> Dictionary:
var templates: Array[Variant] = row.get("templates", [])
active_template_name = ""
active_template_index = -1
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[Variant, Variant] = templates[idx]
active_template_name = String(tpl.get("display_name", ""))
active_template_index = idx
var out := row.duplicate(true)
out["nodes"] = tpl.get("nodes", [])
return out
# ---- 图源装配(关卡节点图生成 v1.1——图 = f(参数档, 种子) 纯函数;无模板池、无兜底图)----
# 参数档按关卡序号解析(消费侧职责——生成器不认关卡号),种子取本局口径:单关模式用传入种子、
# 冒险模式用主种子(产图与全局计数器号位无关,断点在停点重生成同图)。产图先过消费侧独立校验
# LevelTable.validate_graph(与产出侧自证同源规格、分工不重叠);违规=起关拒绝,不降级不回退。
func _assign_generated_graph(row: Dictionary, seed_val: int) -> bool:
var level_index: int = int(row.get("level_index", 0))
var params: Dictionary = db.level_gen_params_row(level_index)
if params.is_empty():
push_error("[关卡节点地图] level %d 无参数档行(level_gen_params 缺行——配置错误)" % level_index)
return false
if _generator == null:
_generator = NodeMapGenerator.new(rng)
var graph: Dictionary = _generator.generate(params, seed_val)
if graph.is_empty() or _generator.is_fatal():
push_error("[关卡节点地图] level %d 生成图失败(参数无解或产出不合格——无兜底图)" % level_index)
return false
var bad := LevelTable.validate_graph(graph["nodes"] as Array)
if not bad.is_empty():
for e in bad:
push_error("[关卡节点地图] level %d 生成图违规: %s" % [level_index, String(e["message"])])
return false
var merged := row.duplicate(true)
merged["nodes"] = graph["nodes"]
level_row.assign(merged)
return true
func nodes() -> Array:
@@ -334,37 +326,30 @@ func current_node() -> Dictionary:
return nodes()[node_index]
# ---- 图版行进面(v1.0:出边表选路 · 零自动推进 · 单链 = 隐式出边退化形态)----
# 编号空间(与 LevelTable 校验器同一解析):图形态 = node_depth(全节点编号化);单链 = 数组下标。
# 混用两空间会在单链上碰撞(如 reward 下标 2 vs 战斗 node_depth 2)——节点寻址一律经 _node_id。
# ---- 图版行进面(生成图恒带显式出边;单链退化形态 = 出度 ≤1 的图,无数组序隐式边)----
# 节点身份=生成图句柄 id(图内唯一)。定位信息另算(enc 携带关卡序号 + 节点深度),
# 禁再以深度当身份(句柄/位置两轴语义单一)。
func _node_id(node: Dictionary) -> int:
if _graph_mode:
return int(node.get("node_depth", -1))
return nodes().find(node)
return int(node.get("id", -1))
# 入口节点 = 无入边唯一节点(图合法性校验保证恰一)
func _entry_node() -> Dictionary:
# 入口节点集 = 全部无入边节点(个数=参数档 entry_count;多入口时首跳即选路)
func _entry_nodes() -> Array[Dictionary]:
var inbound := {}
for node in nodes():
for tid in _out_ids(node):
inbound[tid] = true
inbound[int(tid)] = true
var out: Array[Dictionary] = []
for node in nodes():
if not inbound.has(_node_id(node)):
return node
return {}
out.append(node)
return out
# 出边编号表:显式 next 优先;单链(无 next 键)= 数组顺序下一项(退化形态)
# 出边句柄表(生成图恒显式 next)
func _out_ids(node: Dictionary) -> Array:
if node.has("next"):
return node.get("next", [])
var nodes: Array[Variant] = nodes()
var i := nodes.find(node)
if i >= 0 and i < nodes.size() - 1:
return [i + 1]
return []
return node.get("next", [])
# 出边候选(节点引用数组——选路口/呈现层共用读面)
@@ -442,11 +427,13 @@ func type_visible_ids() -> Array[int]:
# 不可达节点不落表(加载期孤儿校验应已拦截)。
func _bfs_layers() -> Dictionary:
var layers := {}
var entry := _entry_node()
if entry.is_empty():
var entries := _entry_nodes()
if entries.is_empty():
return layers
var queue: Array[Dictionary] = [entry]
layers[_node_id(entry)] = 0
var queue: Array[Dictionary] = []
for entry in entries:
layers[_node_id(entry)] = 0
queue.append(entry)
var i := 0
while i < queue.size():
var cur: Dictionary[Variant, Variant] = queue[i]
@@ -510,7 +497,7 @@ func _start_battle(node: Dictionary) -> bool:
# node_entered 先于 begin_battle:呈现层须先拿到 settlement 引用,才能响应 begin 期间发出的
# board_changed(首板安装)并重建棋盘——后发会导致首战棋盘不渲染
node_entered.emit(node, {})
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("depth", 0)),
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp(),
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
"attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退)
@@ -523,7 +510,7 @@ func _start_battle(node: Dictionary) -> bool:
"voucher_rap": _voucher_rap,
"voucher_time_sand": _voucher_time_sand,
"node_type": String(node.get("type", ""))}):
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("depth", -1)))
return false
_voucher_rap = 0 # 节点级一次性:注入即消费(战斗未启动则保留至下次注入前不清)
_voucher_time_sand = 0
@@ -535,7 +522,7 @@ func _forward_step_row(row: Dictionary) -> void:
# battle_index=第几场战斗(关卡域 1 基序——visited 已含当前节点〔_enter_node :437 先追加后启动〕;
# 按已到节点 type=battle 计数,与图形态 node_depth/单链数组下标两编号空间解耦〔混编碰撞免疫〕;
# 按已到节点 type=battle 计数,与句柄 id / 节点深度两轴解耦〔身份与位置互不兼任〕;
# 开局姿态组「每关首轮窗」条件面)
func _battle_index() -> int:
var n := 0