## VERTICAL SLICE - NOT FOR PRODUCTION ## 组合根冒险流程承接件:菜单开局/续玩之后的冒险编排接线、节点事件分发、终局面与选路屏开合。 ## 本件是 main.gd 同名函数的逐字搬运(成员访问改 root 前缀)——main 侧留同名薄委托,探针 main.X 路径零改动。 ## 局态仍全局住在 run_scope 节点:本件零成员态、零信号连接新增(连接语句随原段搬运)。 extends RefCounted # 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根;断点注入同口)。 # 信号直连(拆件批 T3):runner 六信号在 level_started 重取点重连(旧实例连接随引用释放消亡—— # is_connected 守卫恰好一次);三选一信号面归停点件(每冒险一次——接线恰一次)。 static func _wire_adventure(root: Variant) -> void: root.run_scope.adventure = AdventureRunner.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG)) root.run_scope.adventure.adventure_finished.connect(root._on_adventure_finished) root.run_scope.adventure.level_started.connect(root._on_adventure_level_started) root.run_scope.adventure.stop().curio_choice_presented.connect(root._bonus.on_presented) root.run_scope.adventure.stop().curio_resolved.connect(root._bonus.on_resolved) root.run_scope.adventure.set_save_manager(root.save_manager) # 断点承载注入(停点快照写盘;调试/模拟不经过此路径) # 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达; # 本回调返回后才有 choices_presented 到达——入口选路层随即弹出) static func _on_adventure_level_started(root: Variant, level_index: int, display_name: String) -> void: root.run_scope.runner = root.run_scope.adventure.runner() # runner 信号直连(拆件批 T3——六转发族退役后的重取点重连;恰好一次断言 sc1 守卫承载) _connect_runner_once(root) # 开场行传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源: # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 # 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp #(hero 加算区——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件: # 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带 var ov: Dictionary[Variant, Variant] = root.meta.hero_override() var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \ or root.ui.modals.debug_overlay.atk_pct() > 0 or root.ui.modals.debug_overlay.hp_pct() > 0 \ or root.run_scope.runner.journal().bonus_total("atk") > 0 \ or root.run_scope.runner.journal().bonus_total("max_hp") > 0 root.ui.battle.log_view.append_bb(root.Narrator.level_open_line(display_name, int(root.run_scope.adventure.master_seed), root.run_scope.runner.effective_atk(), root.run_scope.runner.effective_max_hp(), boosted)) # runner 六信号重连(is_connected 守卫——未连即接;守卫失败 push_error=重连恰好一次契约承载) static func _connect_runner_once(root: Variant) -> void: var r = root.run_scope.runner if r == null: push_error("[冒险流程] runner 信号重连失败——level_started 后 runner 为空") return var pairs := [ [r.step_row, root._on_step_row], [r.round_started, root._on_round_started], [r.board_changed, root._on_board_changed], [r.node_entered, root._on_runner_node], [r.choices_presented, root._on_choices_presented], [r.wallet_changed, root._on_wallet_changed], ] for pair in pairs: if not (pair[0] as Signal).is_connected(pair[1]): (pair[0] as Signal).connect(pair[1]) static func _on_runner_node(root: Variant, node: Dictionary, info: Dictionary) -> void: # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 if root.run_scope.is_adventure and root.run_scope.adventure != null: root.run_scope.runner = root.run_scope.adventure.runner() var t := String(node.get("type", "")) if t == "battle" or t == "boss": root.run_scope.settlement = root.run_scope.runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) root.logger.attach_battle(root.run_scope.settlement) root.run_scope.boss_active = t == "boss" root._show_board_fx_for_node(t) root.ui.battle.log_view.append_bb(root.Narrator.node_banner(root.run_scope.runner, node, t)) elif t == "reward": # 奖励节点三选一(与关卡完成三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 # 呈现编排归 curio_interact_controller.present_node_curio。 root._bonus.present_node_curio() root.ui.battle.log_view.append_bb(root.Narrator.reward_line()) elif t == "rest": var healed: int = int(info.get("healed", 0)) root.ui.modals.flow.open(root.ui.modals.node_overlay) root.ui.modals.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))) root.ui.battle.log_view.append_bb(root.Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))) elif t == "shop": # 商店房兑付:到达即开店——商品/货架快照与余额经 runner 只读投影喂入; # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡;legacy 面 = 固定三件(off 对照承载) root.ui.modals.flow.open(root.ui.modals.shop_overlay) if root.run_scope.runner.shop.is_shop_pool(): root.ui.modals.shop_overlay.open_shelf(root.run_scope.runner.shop.shop_shelf(), int(root.run_scope.runner.gp)) root.ui.modals.shop_overlay.set_visit_info(int(root.run_scope.runner.shop.shop_bp_visit_buys()), float(root.db.run.shop.price_a)) # 爬价规则行 root.ui.modals.shop_overlay.set_refresh_display(root.run_scope.runner.shop.shop_refresh_free_left(), root.run_scope.runner.shop.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop.shop_refresh_cost()) # MC-1 else: root.ui.modals.shop_overlay.open_shop(root.run_scope.runner.shop.shop_products(), int(root.run_scope.runner.gp)) root.ui.modals.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) root.ui.battle.log_view.append_bb(root.Narrator.shop_line(int(root.run_scope.runner.gp))) root.logger.log_node(node, info, root.run_scope.runner.node_index) root._refresh_presentations() # ---- 冒险模式终局面(三选一选择面 / 冒险结算屏)---- # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 static func _on_adventure_finished(root: Variant, summary: Dictionary) -> void: root.run_scope.level_active = false var result: String = String(summary.get("result", "")) root.run_scope.settlement = null _exit_level_screen(root) # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 root._sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现) root._sync_settlement_visuals() # → 全关 HudPresenter.battle_end_feedback(root.ui.battle.stage, root.ui.battle.board_zone.ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道 # 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary[String, Variant] = {} detail.assign(root.meta.settle_adventure(summary)) var text: String = root.Narrator.adventure_summary_text(summary) if not detail.is_empty(): text += "\n" + root.Narrator.keystone_settle_text(detail, root._bonus.item_name_lookup()) if result == "completed": root.ui.modals.end_overlay.open_victory(text, "返回冒险", root.ui.battle.board_zone.board_view.get_global_rect(), {"gp_summary": "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))}) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 else: root.ui.modals.end_overlay.open_defeat(text, "返回", root.ui.battle.board_zone.board_view.get_global_rect()) root.logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) if root.run_scope.runner != null: root.run_scope.runner.restore_hero_base() root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay) root._refresh_presentations() static func _on_battle_end_continue(root: Variant) -> void: # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档) root.ui.modals.flow.close(root.ui.modals.end_overlay) root.ui.modals.end_overlay.close() # 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller) return # 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) if root.run_scope.settlement_stop_timer.is_armed(): root.run_scope.settlement_stop_timer.disarm() _enter_level_screen(root) root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) root.ui.modals.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner) root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root.run_scope.pending_candidates = [] root._refresh_presentations() return # 会话收束:run_scope 整节点 queue_free()(帧末回收)——新闲时会话即建, # 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd) root._debug.reset() root._begin_session(false) root.ui.modals.debug_overlay.reset_sliders() root.ui.screens.start_overlay.visible = true root._refresh_presentations() static func _on_inter_next_pressed(root: Variant) -> void: root.ui.screens.inter_overlay.visible = false if root.run_scope.is_adventure: # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) if root.run_scope.adventure != null and not root.run_scope.adventure.continue_to_next_level(): root._fatal_screen("关卡推进失败——见输出日志") return if not root._debug.next_layer(): root._fatal_screen("战斗启动失败——见输出日志") static func _on_node_continue(root: Variant) -> void: root.ui.modals.flow.close(root.ui.modals.node_overlay) if root.run_scope.runner != null: # 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 root.run_scope.runner.continue_after_node() static func _on_choices_presented(root: Variant, candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处) if root.run_scope.runner == null: return root.ui.battle.log_view.append_bb(root.Narrator.route_line(candidates, root.run_scope.runner)) # 战斗胜利总结结算(battle.md OQ14):战斗胜利停点先弹 BattleEndOverlay # 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点 # 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点 # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 if root.run_scope.is_adventure and root.run_scope.runner.current_battle() == null \ and String(root.run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): root.run_scope.settlement_stop_timer.arm() root.run_scope.pending_candidates = candidates.duplicate() root.ui.modals.end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点", root.ui.battle.board_zone.board_view.get_global_rect(), {"gp_summary": "战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)]}) root.ui.modals.flow.open_after_playback(root.ui.modals.end_overlay) root._refresh_presentations() return _enter_level_screen(root) root.ui.modals.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held()))) root.ui.modals.level_screen.open_choices(candidates, root.run_scope.runner) root.ui.modals.level_screen.apply_status(level_map_status_payload(root.run_scope.runner)) root._refresh_presentations() static func _on_route_chosen(root: Variant, node_id: int) -> void: # 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈 # 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈 # 打开低层时自动隐藏本屏、关闭自动恢复) if not root.ui.modals.level_screen.visible: root.ui.modals.flow.close(root.ui.modals.level_screen) _exit_level_screen(root) if root.run_scope.runner != null and not root.run_scope.runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) static func _enter_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = true root.ui.screens.battle_root.visible = false root.ui.modals.flow.open(root.ui.modals.level_screen) root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) static func _exit_level_screen(root: Variant) -> void: root.run_scope.level_screen_active = false root.ui.screens.battle_root.visible = true if root.ui.modals.level_screen.visible: root.ui.modals.flow.close(root.ui.modals.level_screen) root._sync_status_entry() # 同上 # 选路屏状态栏装配(视图零核心态拼装——architecture §1.3:读核心态→拼展示文案集中于装配器)。 # 打开本屏/兑付确认回执后由组合根取成品喂入 apply_status(刷新时点与原视图内自刷一致)。 static func level_map_status_payload(runner: LevelRunner) -> Dictionary: var atk_bonus := runner.journal().bonus_total("atk") # 加算区住加算日记(读面经 journal() 出口承接) var ac_bonus := runner.journal().bonus_total("armor_class") var hp_bonus := runner.journal().bonus_total("max_hp") var atk_txt := "%d" % runner.effective_atk() if atk_bonus > 0: atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块) var ac_txt := "%d" % runner.effective_ac() if ac_bonus > 0: ac_txt += " +%d" % ac_bonus var parts: Array[String] = [] if atk_bonus > 0: parts.append("攻 +%d" % atk_bonus) if hp_bonus > 0: parts.append("上限 +%d" % hp_bonus) if ac_bonus > 0: parts.append("AC +%d" % ac_bonus) return { "level_label": String(runner.level_row.get("display_name", "")), "gp_text": "◈ %d" % int(runner.gp), "hp_text": "HP %d/%d" % [int(runner.hero_hp), int(runner.effective_max_hp())], "hp_bar_redraw": true, "attr_text": "攻击 %s · 护甲 AC %s" % [atk_txt, ac_txt], "bonus_text": " · ".join(parts), "bonus_row_visible": not parts.is_empty(), } static func _on_abandon_requested(root: Variant) -> void: # 放弃入口(冒险结构.md 已放开「MVP 无界面入口」后置条款; # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 if not root.run_scope.is_adventure or root.run_scope.adventure == null: return if not root.run_scope.adventure.abandon(): root.logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) root.ui.battle.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") # ---- 营地覆盖层与钥石账本(写调用唯一归组合根)----