Files
gplan-2/game/presentation/main_adventure_flow.gd
T

259 lines
15 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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.run.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_summary": "本局 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_summary": "战利品 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")
# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)----