259 lines
15 KiB
GDScript
259 lines
15 KiB
GDScript
## 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.debug_overlay.atk_pct() > 0 or root.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.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.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.log_view.append_bb(root.Narrator.reward_line())
|
||
elif t == "rest":
|
||
var healed: int = int(info.get("healed", 0))
|
||
root._open_modal(root.node_overlay)
|
||
root.node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
|
||
root.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._open_modal(root.shop_overlay)
|
||
if root.run_scope.runner.is_shop_pool():
|
||
root.shop_overlay.open_shelf(root.run_scope.runner.shop_shelf(), int(root.run_scope.runner.gp))
|
||
root.shop_overlay.set_visit_info(int(root.run_scope.runner.shop_bp_visit_buys()), float(root.db.shop_price_a())) # 爬价规则行
|
||
root.shop_overlay.set_refresh_display(root.run_scope.runner.shop_refresh_free_left(), root.run_scope.runner.shop_refresh_cost(), int(root.run_scope.runner.gp) >= root.run_scope.runner.shop_refresh_cost()) # MC-1
|
||
else:
|
||
root.shop_overlay.open_shop(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp))
|
||
root.shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
|
||
root.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.stage, root.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._end_overlay.open_victory(text, "返回冒险", root.board_view.get_global_rect(),
|
||
"本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账
|
||
else:
|
||
root._end_overlay.open_defeat(text, "返回", root.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._open_modal_after_playback(root._end_overlay)
|
||
root._refresh_presentations()
|
||
|
||
static func _on_battle_end_continue(root: Variant) -> void:
|
||
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档)
|
||
root._close_modal(root._end_overlay)
|
||
root._end_overlay.close()
|
||
# 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
|
||
if root._bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller)
|
||
return
|
||
# 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
|
||
if root._settlement_stop_pending:
|
||
root._settlement_stop_pending = false
|
||
_enter_level_screen(root)
|
||
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
|
||
root.level_screen.open_choices(root.run_scope.pending_candidates, root.run_scope.runner)
|
||
root.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.debug_overlay.reset_sliders()
|
||
root._start_overlay.visible = true
|
||
root._refresh_presentations()
|
||
|
||
static func _on_inter_next_pressed(root: Variant) -> void:
|
||
root._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._close_modal(root.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.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._settlement_stop_pending = true
|
||
root.run_scope.pending_candidates = candidates.duplicate()
|
||
root._end_overlay.open_victory(root.Narrator.battle_end_text("victory", false), "下一节点",
|
||
root.board_view.get_global_rect(),
|
||
"战利品 GP +%d · 合计 %d" % [root.run_scope.last_gain_amount, int(root.run_scope.runner.gp)])
|
||
root._open_modal_after_playback(root._end_overlay)
|
||
root._refresh_presentations()
|
||
return
|
||
_enter_level_screen(root)
|
||
root.level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(root.db, root.run_scope.runner.feats_held())))
|
||
root.level_screen.open_choices(candidates, root.run_scope.runner)
|
||
root.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.level_screen.visible:
|
||
root._close_modal(root.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._battle_root.visible = false
|
||
root._open_modal(root.level_screen)
|
||
root._sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态)
|
||
|
||
static func _exit_level_screen(root: Variant) -> void:
|
||
root.run_scope.level_screen_active = false
|
||
root._battle_root.visible = true
|
||
if root.level_screen.visible:
|
||
root._close_modal(root.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.board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
|
||
|
||
|
||
# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)----
|
||
|