## VERTICAL SLICE - NOT FOR PRODUCTION ## 冒险结构编排器:三关序列、生命/GP 账本、三选一停点与三结局结算唯一所有者,headless 可跑完整冒险(垂直切片验证:一条命走完三关);令牌池与凭证判定归 CurioDeck 件。 ## Core 层禁 UI 引用;数值零字面量只读 db;core↔core 连信号一律方法 Callable,禁 lambda 捕获 self(RefCounted 成环)。 ## 主种子恰一次注入归本类(LevelRunner 不重播种);set_save_manager 缺省 null = 断点面整体静默(模拟/冒烟零扰动)。 ## 呈现层须在 level_started 后重取 runner()(每关新建实例)——先重取再开选路口,否则被空防御丢弃。 class_name AdventureRunner extends RefCounted signal level_started(level_index: int, display_name: String) ## 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 signal curio_choice_presented(payload: Dictionary) ## 专长三选一停点(非末关的关卡完成恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared} signal curio_resolved(result: Dictionary) ## 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index} signal adventure_finished(summary: Dictionary) ## 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/完成几关/止步第几关/钥石合计/主种子(+additive keystone_drops) var db: ConfigDb var rng: RngServiceCore enum State { IDLE, RUNNING, SETTLING } # 会话状态机(内部 enum,外部 String 契约经 getter 词表) var _state: State = State.IDLE var state: String: # 外部契约面(只读——外部零写点实证;词表 "idle"/"running"/"settling") get: return State.keys()[_state].to_lower() var _stats: HeroStats # 主角数值账(生命/类型轴/特性集——公式单点,拆件批 T1 建件、领域拆分批三件化) var _wallet: GpWallet # GP 钱包件(单写原语——LevelRunner 与 ShopSession 读写单点) var _journal: GainJournal # 加算日记件(获得记录三层/钥石/真视/已购档单写原语) var adventure_counters: Dictionary[Variant, Variant] = {} # 冒险桶(效果引擎 adv_ 计数器宿主字典——跨战持久) var level_index := 0 # 当前关卡序号(1 起) var levels_total := 0 # 序列总关数(读 level 表行数) var master_seed := 0 # 本局主种子(摘要第五件事) var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成) var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离) var _treasure_unlocked := {} # aux 解锁门控集(params.treasure_unlocked 接收面) var _attunement: Array[Variant] = [] # 同调装配集(params.attunement 接收面,start_level 外下传 LevelRunner) # ---- 三选一停点(编排迁 CurioStop 件——停点三态/刷新三源/凭证出箱/信号面)---- var _curio: CurioDeck = null # 令牌池件(_init 建;纯池——多重集/抽签/cap/凭证判定唯一宿主) var _stop: CurioStop = null # 停点件(_init 建;每冒险一次——关卡换实例不换件) var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源账与付费报价;宿主预算注入与断点片段直收) var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源) var _bp_store: BreakpointStore # 断点组装与回放件(_init 建;六成员+27 键登记单源+读写侧机组随件——宿主读写经件) var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面) var _tree_rules: Dictionary[Variant, Variant] = {} # 树规则参数字典(Fork 激活侧 effects_* 透传;下传 enc.tree_rules 消费面) var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool) func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: db = db_ref rng = rng_ref _journal = GainJournal.new() _wallet = GpWallet.new() _stats = HeroStats.new(db, {}, _journal) # 数值账消费日记汇总(同源单件注入) _curio = CurioDeck.new(db, rng) _refresh_bank = RefreshBank.new(db) _stop = CurioStop.new(db, {"deck": _curio, "stats": _stats, "journal": _journal, "bank": _refresh_bank}, _snapshot_breakpoint) _loadout = AttuneLoadout.new(db, _attunement) adventure_inventory = TreasureItems.new(db) _bp_store = BreakpointStore.new({"db": db, "rng": rng, "stats": _stats, "wallet": _wallet, "journal": _journal, "curio": _curio, "stop": _stop, "bank": _refresh_bank, "loadout": _loadout}) # 件出口(组件交出非委托壳——呈现/测量面取主角账三件与令牌池单点;领域拆分批接口块) func stats() -> HeroStats: return _stats func wallet() -> GpWallet: return _wallet func journal() -> GainJournal: return _journal func deck() -> CurioDeck: return _curio func refresh_bank() -> RefreshBank: return _refresh_bank # 件出口(停点件——呈现/测量面取停点编排单点;拆件批接口块) func stop() -> CurioStop: return _stop # ---- 冒险生命周期 ---- # 冒险开始(主种子注入恰一次——全冒险唯一 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) _stats.rebase_override(params.get("hero_override", {})) # 永久强化 flat 注入(规范化归数值账) _journal.reset_adventure_accounts() # 日记域账面清零(汇总/明细/已购档/钥石/真视——先于置满) _wallet.clear_wallet() # 钱包清零 _stats.reset_adventure_accounts() # 生命置满基础上限+特性持有集清零(口径不含加算区——日记先清) _stats.attack_type = String(params.get("attack_type", "")) # 两根类型轴快照(契约 8 锁死:开局后不随存档选中变化) _stats.armor_type = String(params.get("armor_type", "")) summary = {} _reset_curio_state() levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数) level_index = 1 _bp_store._pending_next_level = 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 _refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(装配集冒险期间锁死) _refresh_bank.set_refresh_y(maxi(0, int(params.get("attune_refresh_y", 0)))) # additive(Y 冒险周期池) _loadout.set_attunement(_attunement) # 装配集注入(活引用——种行面) adventure_inventory.reset() # 冒险级库存重置(新局空库存) _bp_store._loadout_seeded = false # 同调行投行延至首关 bind 时点(库存就位后) adventure_counters = {} # 冒险桶清零(效果引擎 adv_ 计数器宿主——冒险开始归零) # 刷新练习开局注入(refresh_stock_grant 装配即得一次性加仓: # 装配集 kind 分发扫描;只入存量账、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕) for iid_v2 in _attunement: var rp_row: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v2)) if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"): _refresh_bank.add_refresh_stock(int(rp_row.get("magnitude", 0))) # 特性开局注入(MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派 # 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕) var feat_seed: Array[Variant] = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else [] _tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典 if not feat_seed.is_empty(): for r in feat_seed: _stats.feats.append(String(r)) _state = State.RUNNING return _start_current_level() # 断点续玩(「继续上局」入口;bp = save.breakpoint_snapshot() 快照,params 同 start_adventure # 的注入参数面):账面(生命/GP/加算区/已购档/钥石累计)逐字段回填 + rng.restore_adventure 回填 # 「主种子+计数器」→ _start_current_level 续玩路径(模板随快照强制、重建到停点节点,零重抽)。 # 快照缺字段按防御默认(空快照拒绝——调用方先 has_breakpoint)。 func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: if state != "idle": push_error("[冒险结构] resume_adventure 拒绝——state=%s(非 idle 不可续玩)" % state) return false 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))) # ---- 断点回放(R2 整族归件——件侧语句入 _bp_store、宿主侧语句按原语句序留本壳,序逐步保形)---- _bp_store.restore_ledger(bp, params) # 续玩账面回填——主角账三件片段各还原各键(件侧;clamp 类夹 ≥0;日记在档=推导比对载入、缺键=旧档汇总直载) summary = {} levels_total = db.run.level.level_count() # 表访问收口(关数读 level 表行数) level_index = int(bp.get("level_index", 1)) _bp_store._pending_next_level = false _reset_curio_state() # 续玩三选一态回填前置清理(池与件计数归件、停点态归本类;桶归宿主字典) _bp_store.restore_curio_state(bp) # 续玩三选一态回填(池余库/获取计数/特性持有集——件侧;dual_merge/keep_on_null 策略见 BREAKPOINT_FIELDS) _bp_store.apply_resume_params(params) # 续玩参数面——数值账规范化与越界钳制(件侧) _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 _refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start) _bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面) _tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传) # 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典) var bp_counters = _bp_store._bp_read("adventure_counters", bp) if bp_counters is Dictionary: adventure_counters = (bp_counters as Dictionary).duplicate(true) return _resume_pending_or_start(bp) # 续玩收束段(宿主侧——停点直回/已兑付重发/无停点续玩起步;R2 回放段的尾半,语句序与 # resume_adventure 内联时逐句相同):三选一停点态与待推进位经件读写,选路停点快照单跳传递。 func _resume_pending_or_start(bp: Dictionary) -> bool: var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary if not bp_pending.is_empty(): _bp_store._pending_next_level = true _state = State.RUNNING _stop.restore_pending(bp_pending) # 选择停点直回(续玩不重跑关卡——信号经停点件中继) return true if not bp_result.is_empty(): _bp_store._pending_next_level = true _stop.restore_resolved(bp_result) # 已兑付不重兑付只重发(置态+重发收编一步——信号面归停点件) _state = State.RUNNING return true _bp_store._resume_bp = bp.duplicate(true) # 充能读侧以行集为源(键值仅校验读数——回填时点在首关 bind 行集就位后,经 _loadout_on_level_inventory) _bp_store._loadout_seeded = false _loadout.set_attunement(_attunement) adventure_inventory.reset() _state = State.RUNNING return _start_current_level() func _reset_curio_state() -> void: _curio.reset() # 池与件获取计数(池半归件;停点态归停点件下行清理) _stop.reset_adventure_state() _bp_store._loadout_seeded = false _stats.feats = [] adventure_counters = {} # 调试浮窗百分比通道(测试工具覆盖——每关新 runner 都要应用;正式侧归元进度。单点归数值账) func set_hero_override(atk_pct: int, hp_pct: int) -> void: _stats.set_debug_override(atk_pct, hp_pct) if _runner != null: _runner.set_hero_override(atk_pct, hp_pct) # 冒险账本侧生效上限(公式单点归数值账——置满/回血封顶与战斗内上限同式同源) func effective_max_hp() -> int: return _stats.effective_max_hp() # 断点承载注入(组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默) func set_save_manager(sm: SaveManagerCore) -> void: _bp_store._save_manager = sm # 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取) func runner() -> LevelRunner: return _runner # 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = HeroStats.rest_heal(公式单点) func apply_rest(pct: int, hp: int, max_hp: int) -> int: return HeroStats.rest_heal(pct, hp, max_hp) # 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进) func continue_to_next_level() -> bool: if not _bp_store._pending_next_level: push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进") return false if _stop.curio_stop_pending(): push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)") return false _bp_store._pending_next_level = false _stop.clear_resolved() level_index += 1 return _start_current_level() # 停点空候选消化(池空=三选一环节整体跳过——呈现层跳开选择面;编排归停点件) # ---- 提升三选一(选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)---- # 候选令牌匹配(身份键恒等——属性加成型特性 kind+tier、规则改写型特性 feat_id) func _find_token(candidates: Array, kind: String, tier: String, feat_id: String = "") -> Dictionary: return _curio.find_token(candidates, kind, tier, feat_id) # 玩家点选兑付(state 守卫留应用层——编排归停点件;呈现层直调面 T5 改 stop()) # 特性持有集读面(效果接线/求值/摘要唯一取数口——副本防外部改写) func feats_held() -> Array: return _stats.feats_held() # ---- 商店共享池消费面(纯池——库存=牌库剩余令牌)---- # 库存余量(池消耗即上限;奖励摘牌与商店购买同库承载,全冒险一次性) # 终局池快照(_settle 清账前调用——复位会重置计数,摘要 additive 键须在此前取数;读面直连令牌池件) func _pool_stock_snapshot() -> Dictionary: return _curio.stock_snapshot() # ---- 奖励节点三选一(与关卡完成三选一共享同池同权重同次数上限——编排归停点件)---- # 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载, # 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。 func node_curio_candidates() -> Dictionary: return _stop.node_curio_candidates() func present_node_curio(candidates: Array) -> void: _stop.present_node_curio(candidates) # 点选兑付(节点域——编排归停点件;呈现层直调面 T5 改 stop()) # 刷新件余量基数(kind 分发查询——装配集内 effect_kinds 含 bonus_refresh 的行 magnitude 合计; # 刷新件类加件零 id 分支;停点件 X 重置基数经 start/resume 预算注入) func _attune_refresh_x_total() -> int: var total := 0 for iid_v in _attunement: var arow: Dictionary[Variant, Variant] = db.crossrun.treasure_items.row_by_id(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"): total += int(arow.get("magnitude", 0)) return total # 同调行投行分派(关卡域库存就位时点——每关一次;首关=种行或旧档行集为源、后续关=快照补行) func _loadout_on_level_inventory() -> void: if _runner.treasure_inventory == null: _runner.treasure_inventory = TreasureItems.new(db) # 惰性建(承原 sync_attune_inventory 同款——种行落点就位) var inv = _runner.treasure_inventory if inv == null: return if not _bp_store._loadout_seeded: _bp_store._loadout_seeded = true var bp_charges: Variant = _bp_store._bp_read("attunement_charges", _bp_store._resume_bp, null) if not _bp_store._resume_bp.is_empty() else null if bp_charges is Dictionary: _loadout.restore_fragment({"attunement_charges": bp_charges}, inv) # 旧档读侧:行集为源+键值校验读数 else: _loadout.seed_for_adventure(inv) # 冒险开始全量种行(不筛周期——C 区投影行数不变) # ---- 三选一刷新与凭证(编排归停点件——本类留组合根委托读面)---- # 当前可刷新次数(X 优先 + Y 池 + 刷新存量账——呈现层余量读数口) # 刷新存量账读数(T4——刷新券加仓余量单独读面;呈现层与断点消费) # 节点停点在途态读回(组合根重 present 用——candidates 值拷贝) # 三选一刷新(免费位 X/Y——组合根唯一调用口;无停点/无余量 false 零副作用) # 三选一 GP 付费刷新(对齐商店 MC-1——组合根先经 curio_stop_pending+钱包支出再调,本口只做计数与重抽) # 本次 GP 付费刷新价(与商店货架刷新同源——单点调参) # 三选一停点在途谓词(付费路径扣费前置校验——无停点禁扣钱,防空转烧钱包) # —— 专长获取专长凭证四口径判定(实装归 CurioDeck 件:调用即一次展示判定与记账)—— # 已拿上限判定(身份维度;缺省放行=白名单外 kind 兜底) # Boss 钥石逐杀掉落(掉落渠道 = Boss/精英击杀;首杀/复杀两档读 meta.tres boss_keystone; # 首杀判据 = 存档 boss_first_kill 标记位〔断点未注入 = 恒首杀——模拟/冒烟零扰动;标记仍在收束 # apply_adventure_summary 逐关卡完成关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因) func _boss_keystone_drop(li: int) -> void: var first := true if _bp_store._save_manager != null: first = not _bp_store._save_manager.meta_state.career_state().boss_first_kill.has(str(li)) var row: Dictionary[Variant, Variant] = db.crossrun.meta.boss_keystone_row(li) var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0)) _journal.record_keystone_drop({"level": li, "first": first, "amount": amount}) # 逐杀明细+累计单写原语 # 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。 # 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: # 关卡对象构造注入一次给齐(拆件批 T3——ctx 键集见 LevelRunner._init 头注; # 凭证出箱在组包时点取走〔节点级一次性——未用即过期语义不变〕) var outbox: Dictionary[Variant, Variant] = _stop.voucher_outbox() var pool_wiring: Dictionary[Variant, Variant] = {"deck": _curio.deck, "consume": _stop.consume_pool_purchase, "ticket": _curio.feat_ticket_consider, "cap": _curio.cap_ok} if _shop_mode == "pool" else {} var ctx: Dictionary[Variant, Variant] = { "stats": _stats, "wallet": _wallet, "journal": _journal, "loadout": _loadout, "adv_bucket": adventure_counters, "treasure_inventory": adventure_inventory, "attunement": _attunement, "tree_rules": _tree_rules, "shop_mode": _shop_mode, "treasure_unlocked": _treasure_unlocked, "hero_attack_type": _stats.attack_type, "hero_armor_type": _stats.armor_type, "voucher_rap": int(outbox.get("rap", 0)), "voucher_time_sand": int(outbox.get("time_sand", 0)), "pool_wiring": pool_wiring, } _runner = LevelRunner.new(db, rng, ctx) _bind_level_runner() _stop.rebind_level_runner(_runner, level_index) # 停点件每关注入时点(GP 件入账回调换代——防跨关调死对象) # 断点续玩分支:图由主种子+参数档重生成同图(产图与计数器号位无关,停点零重抽语义随之消解), # resume_state 带停点句柄与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。 var resume_state: Dictionary[Variant, Variant] = {} if not _bp_store._resume_bp.is_empty(): resume_state = {"node_id": int(_bp_store._bp_read("node_id", _bp_store._resume_bp, -1)), "visited_ids": _bp_store._bp_read("visited_ids", _bp_store._resume_bp, []) as Array} # 接口①开始关卡:生命经主角账共享交接(账面当前值即开局生命——冒险开始置满 / 大奖励回满已写好); # 主角账三件与冒险桶注入(构造注入 ctx——拆件批 T3 收口形态)。 # defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在 # 该回调重取 runner 实例),后入口候选呈现;顺序反了选路口会被呈现层的 runner 空防御静默丢弃。 # 呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。 var ok: bool = _runner.start_level_external(level_index, apply_rest, true, resume_state) if ok: _emit_level_started() _present_level_entry(resume_state) _bp_store._resume_bp = {} return ok # 关卡行进器接线(每关新实例:结算受理连接 + 能力件骑库存注入——信号转发族已退役, # 呈现/模拟器在 level_started 重取点直连 runner 信号〔is_connected 守卫恰好一次〕) func _bind_level_runner() -> void: # core↔core 接线只许方法 Callable(S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环) _runner.level_finished.connect(_on_level_finished) _runner.choices_presented.connect(_on_level_choices_presented) # 选路停点快照触发(原转发兼快照双职的核心侧受理) _loadout_on_level_inventory() # 同调行投行分派(种行/旧档行集为源/每关快照补行) # 关卡开始广播(装配已随构造注入进 ctx——本函数只剩信号广播;信号顺序契约: # 关卡开始信号先于入口候选呈现〔defer_entry 两步式以本发射为界〕) func _emit_level_started() -> void: level_started.emit(level_index, String(_runner.level_row.get("display_name", ""))) # 入口呈现(入口停点与中途停点两态;restore_treasure_items 先于呈现=C 区库存就位) func _present_level_entry(resume_state: Dictionary) -> void: if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0: _runner.present_entry_choice() else: _runner.restore_treasure_items(_bp_store._bp_read("aux_items", _bp_store._resume_bp, []) as Array) _runner.present_resumed_choice() # 接口④关卡结束(结果 + 生命终值 + 节点位置——胜负皆报;结算受理只算一次) func _on_level_finished(payload: Dictionary) -> void: if state != "running": push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state) return _bp_store._last_level_payload = payload.duplicate(true) var result := String(payload.get("result", "")) if result == "defeat": _stats.hero_hp = 0 # 倒下记 0(生命账本战后回写口径) _settle("failed") return if result != "victory": push_error("[冒险结构] 关卡结束结果出域: %s" % result) return _bp_store._last_treasure_cleared = int(payload.get("aux_cleared", 0)) # additive(末关 summary 取数面) # Boss 钥石逐杀掉落(击杀掉落渠道,末关照常入账;首杀/复杀读 meta.tres boss_keystone) _boss_keystone_drop(level_index) if level_index < levels_total: # 关卡完成兑付(非末关恰一次;三层奖励框架):编排归停点件——推进位先于 emit 置位 #(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位); # 停点件内部分回血先行 → 加权洗牌掷 3 → 三选一停点(顺序即契约——回血按当刻账本与上限结算, # 选择停点零自动推进)。载荷_base 随附钱包读数(停点件零钱包依赖——GP 件入账回调仍走 grant_curio_gp) _bp_store._pending_next_level = true var stop_base: Dictionary[Variant, Variant] = payload.duplicate(true) stop_base["gp"] = _wallet.gp _stop.open_level_clear(stop_base) return # 末关的关卡完成:不发三选一与部分回血(没有下一战可受益)、Boss 钥石照常入账——冒险完成结算 _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": _journal.keystone_gained(), "master_seed": master_seed, "aux_cleared_last": _bp_store._last_treasure_cleared, # additive 扩签(末关清空反馈;非末关走 bonus 载荷) "keystone_drops": _journal.keystone_drop_log(), # additive 扩签(逐杀掉落明细;settle 入账明细/复盘归因数据源) "vision_used_delta": _journal.vision_used(), # additive 扩签(真视使用本局计数;settle 聚合入档) "pool_stock": _pool_stock_snapshot(), # additive 扩签(终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面) "relics": _stats.feats_held(), # additive 扩签(特性终局持有集:清账前快照,成型判读取数面) } # 败退明细(数值验证工具取数面——tools/balance_verifier.py 止步节点分布数据源; # 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知) if result == "failed" and not _bp_store._last_level_payload.is_empty(): summary["defeat"] = { "level_index": level_index, "node_id": int(_bp_store._last_level_payload.get("node_id", -1)), "node_type": String(_bp_store._last_level_payload.get("node_type", "")), } if _runner != null: summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(终局面板取数源=runner 钱包单点) _runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现) _reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化) if _bp_store._save_manager != null: _bp_store._save_manager.clear_breakpoint() # 三结局清断点(退出冒险=放弃流程+清除断点) _bp_store._save_manager.write_to_disk() _state = State.SETTLING adventure_finished.emit(summary) _state = State.IDLE # 选路停点快照受理(level.choices_presented 直订——选路停点=唯一可恢复停点形态;战斗中不存) func _on_level_choices_presented(_candidates: Array) -> void: _snapshot_breakpoint() # ---- 断点快照委托(组装与读侧策略机组归 breakpoint_store 件——六成员+27 键登记单源随件; # 本壳一行转发:CurioStop 构造注入的 Callable 目标与本类 _on_level_choices_presented 调用点零改)---- func _snapshot_breakpoint() -> void: _bp_store.snapshot(state, level_index, master_seed, _runner, adventure_counters)