# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完—— # 主种子恰一次注入、生命/GP/加算区跨关同账、大奖励非末关恰兑付一次、三结局摘要五件事成立(P0) # Date: 2026-09-21(冒险结构实现批——设计权威 design/gdd/systems/冒险循环/冒险结构.md) # 冒险结构编排器(Core 层零 UI 依赖——headless 可跑完整冒险):一局的关卡序列、生命账本、GP 冒险 # 账本、统一冒险加算区、关卡大奖励、三结局结算与主种子接线的唯一所有者。 # # 接口契约四件套落点(冒险结构.md「与关卡节点地图的边界」——本类是正式侧承载): # ① 开始关卡 = start_level_external(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接); # ② 战斗结果上报 = runner 经共享账本属性直写(LevelRunner._on_battle_finished 回写 hero_hp 到 # _ledger——零直调战斗域,战后生命终值随账本回流本类); # ③ 治疗封顶回报 = apply_rest(恢复点节点兑付时 LevelRunner 经 _rest_handler 回调——按比例回血并 # 封顶的执行归本类); # ④ 关卡结束 = _on_level_finished(LevelRunner.level_finished 到达——结果 + 生命终值 + 节点位置)。 # # 接口漂移清偿登记(06 清单①②③⑥⑦——LevelRunner 侧同批登记):①种子域归本类 # (start_adventure 调 rng.new_adventure 全冒险恰一次;LevelRunner 冒险模式不重播种); # ②生命账本归本类(共享 _ledger);③恢复兑付执行归本类(_rest_handler);⑥收束先清库后宣布—— # 现行无关卡域库存(格面资源批 2026-09-15 后为空集),战斗结束 retire_mapping + despawn_all 先于 # battle_finished 即事实清库,不另设空接口;⑦失败结算归本类(LevelRunner 只上报结果)。 # # 收束次序登记(大奖励兑付时点——冒险结构.md「关卡大奖励」):关卡节点地图先清库、后宣布关卡 # 结束(次序不可换);大奖励在宣布之后、下一关开始之前兑付——本类 _on_level_finished 兑付、 # continue_to_next_level 开始下一关,两步间为呈现层大奖励屏(冒烟/自动路径立即确认)。 # 结算受理只算一次(契约):state != "running" 的关卡结束宣布与放弃请求一律拒绝并留诊断, # 账本与摘要零变化。 # 停点前移批(2026-09-23,用户拍板「关卡开始即出节点图选路、选节点才进战斗」):每关开始 = # 入口选路停点——_start_current_level 以 defer_entry 起关,level_started 之后才 present_entry_choice # (信号顺序契约:呈现层先重取 runner 实例,选路口才不会被空防御丢弃);pick 入口候选走同一 # _enter_node(entry)、呈现零 RNG 消耗——与旧「start 直进首战」逐位等价,测量面零漂移。 # 关卡切换(continue_to_next_level)后新 LevelRunner 实例重新走 defer 流程;abandon 在入口停点 # 天然非战斗期(current_battle 为空)——受理语义与选路停点一致。 # 纪律:ADR-0001 S6——连 runner 信号一律 _forward_* 方法 Callable(禁 lambda 捕获 self 成环); # Core 层禁 UI 引用;数值零字面量——level_clear_hp_bonus / keystone_per_level 只读 db.adventure, # 基础上限只读 db.hero。 class_name AdventureRunner extends RefCounted signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 signal big_reward_paid(reward: Dictionary) # 大奖励兑付(非末关通关恰一次)——{from_level, hp_bonus, new_max_hp, keystone_gained, keystone_total};呈现层大奖励屏/冒烟立即确认 signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子 # ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)---- signal node_entered(node: Dictionary, info: Dictionary) signal choices_presented(candidates: Array) signal step_row(row: Dictionary) signal round_started(round_index: int) signal board_changed() signal wallet_changed(event: String, gp: int, extra: Dictionary) var db var rng var state := "idle" # idle / running / settling(冒险结构.md 三态) var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享) var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(商店购买与大奖励同一本账) var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入) var level_index := 0 # 当前关卡序号(1 起) var levels_total := 0 # 序列总关数(读 level 表行数) var keystone := 0 # 钥石入账累计(摘要第三件事的账面) var master_seed := 0 # 本局主种子(摘要第五件事) var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成) 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} var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) var _last_aux_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面) var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.aux_unlocked 接收面) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref # ---- 冒险生命周期 ---- # 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮: # 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步)。 # params.hero_override = 永久强化 flat 注入(元进度实现批——缺省 {} = 全零白板,与 B1 空参数路径逐位一致; # -1/缺键 = 表值兜底口径,规范化为 0 flat) func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: if state != "idle": push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state) return false master_seed = seed_val rng.new_adventure(seed_val) ledger = {"hero_hp": 0, "gp": 0} bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} purchased = {} keystone = 0 summary = {} levels_total = db.level.rows.size() level_index = 1 _pending_next_level = false _base_override = _normalize_override(params.get("hero_override", {})) _first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入) _aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {} # additive(迭代 7 T5) ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零) state = "running" return _start_current_level() # 注入参数规范化:三字段缺键/-1 = 表值兜底(→ 0 flat);负值一律 clamp 0(白板 = 全零合法态) func _normalize_override(raw: Dictionary) -> Dictionary: var out := {"atk": 0, "max_hp": 0, "armor_class": 0} for k in out.keys(): out[k] = maxi(0, int(raw.get(k, 0))) return out # TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度) func set_hero_override(atk_pct: int, hp_pct: int) -> void: _override = [atk_pct, hp_pct] if _runner != null: _runner.set_hero_override(atk_pct, hp_pct) # 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取) func runner() -> LevelRunner: return _runner # 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶;与 LevelRunner.rest_heal_amount # 同式,同一公式两个落点是临时态,注释指认,统一归并归后续批) func apply_rest(pct: int, hp: int, max_hp: int) -> int: return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) # 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;呈现层大奖励屏 / 冒烟立即确认) func continue_to_next_level() -> bool: if not _pending_next_level: push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进") return false _pending_next_level = false level_index += 1 return _start_current_level() # 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。 # MVP 无界面入口,语义先行预留;结算受理只算一次——非 running 态拒绝) func abandon() -> bool: if state != "running": push_error("[冒险结构] abandon 拒绝——state=%s(非进行中不可放弃)" % state) return false if _runner != null and _runner.current_battle() != null: push_error("[冒险结构] abandon 拒绝——战斗进行中(战斗域单事务在飞)") return false _settle("abandoned") return true # ---- 关卡推进(私有——序列编排单一出口)---- func _start_current_level() -> bool: _runner = LevelRunner.new(db, rng) # core↔core 接线只许方法 Callable(ADR-0001 S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环) _runner.node_entered.connect(_forward_node_entered) _runner.choices_presented.connect(_forward_choices_presented) _runner.step_row.connect(_forward_step_row) _runner.round_started.connect(_forward_round_started) _runner.board_changed.connect(_forward_board_changed) _runner.wallet_changed.connect(_forward_wallet_changed) _runner.level_finished.connect(_on_level_finished) _runner.set_hero_override(int(_override[0]), int(_override[1])) # 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好); # _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)。 # 停点前移批 2026-09-23:defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在 # 该回调重取 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, 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() return ok # 接口④关卡结束(结果 + 生命终值 + 节点位置——胜负皆报;结算受理只算一次) func _on_level_finished(payload: Dictionary) -> void: if state != "running": push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state) return _last_level_payload = payload.duplicate(true) var result := String(payload.get("result", "")) if result == "defeat": ledger["hero_hp"] = 0 # 倒下记 0(生命账本战后回写口径) _settle("failed") return if result != "victory": push_error("[冒险结构] 关卡结束结果出域: %s" % result) return _last_aux_cleared = int(payload.get("aux_cleared", 0)) # additive(迭代 7 T4——末关 summary 取数面) if level_index < levels_total: # 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约) var hp_bonus: int = int(db.adventure.level_clear_hp_bonus) bonus["max_hp"] = int(bonus.get("max_hp", 0)) + hp_bonus ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus["max_hp"]) keystone += int(db.adventure.keystone_per_level) # 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位) _pending_next_level = true big_reward_paid.emit({ "from_level": level_index, "hp_bonus": hp_bonus, "new_max_hp": int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus["max_hp"]), "keystone_gained": int(db.adventure.keystone_per_level), "keystone_total": keystone, "aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数) }) return # 末关通关:钥石照记、不发上限提升(没有下一战可受益)——冒险完成结算 keystone += int(db.adventure.keystone_per_level) _settle("completed") func _settle(result: String) -> void: # 冒险摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石入账合计 / 本局主种子 summary = { "result": result, "cleared": result == "completed", "levels_cleared": levels_total if result == "completed" else level_index - 1, "stopped_at": 0 if result == "completed" else level_index, "keystone_total": keystone, "master_seed": master_seed, "aux_cleared_last": _last_aux_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 big_reward 载荷) } # 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源; # 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知) if result == "failed" and not _last_level_payload.is_empty(): summary["defeat"] = { "level_index": level_index, "node_id": int(_last_level_payload.get("node_id", -1)), "node_type": String(_last_level_payload.get("node_type", "")), } if _runner != null: _runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现) state = "settling" adventure_finished.emit(summary) state = "idle" # ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明)---- func _forward_node_entered(node: Dictionary, info: Dictionary) -> void: node_entered.emit(node, info) func _forward_choices_presented(candidates: Array) -> void: choices_presented.emit(candidates) func _forward_step_row(row: Dictionary) -> void: step_row.emit(row) func _forward_round_started(ri: int) -> void: round_started.emit(ri) func _forward_board_changed() -> void: board_changed.emit() func _forward_wallet_changed(event: String, gp_val: int, extra: Dictionary) -> void: wallet_changed.emit(event, gp_val, extra)