From b0d92524fecf9d098080426778e8cfd527b6202a Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 25 Oct 2026 05:47:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(iter37=E2=91=A1):=20main=20=E7=BB=84?= =?UTF-8?q?=E5=90=88=E6=A0=B9=E5=88=A4=E7=A6=BB=E2=80=94=E2=80=94=E6=8A=95?= =?UTF-8?q?=E5=BD=B1/=E8=A7=A3=E6=9E=90=E5=9B=9E=E8=B0=83/=E6=96=87?= =?UTF-8?q?=E6=A1=88=E4=B8=89=E6=97=8F=E5=87=BA=E6=A0=B9=EF=BC=8B=E8=A3=85?= =?UTF-8?q?=E9=85=8D=E4=B8=8E=E6=B5=81=E7=A8=8B=E6=89=BF=E6=8E=A5=E5=9B=9B?= =?UTF-8?q?=E4=BB=B6=EF=BC=88=E8=A1=8C=E4=B8=BA=E7=AD=89=E4=BB=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - r1-1 数据投影五函数 → HudPresenter 静态族(status_data/status_curios/career_data/save_select_data/camp_data,依赖面显式参数化) - r1-2 只读解析回调四函数 → 新件 board_resolvers(有态绑定:db+调试浮窗+结算活态取值三参注入,产出三解析 Callable+override 拼装;注入点改引) - r1-3 文案拼装三件 → HudPresenter(gp_summary_text/bonus_summary_text/phase_text) - r1-4 探针直引零改动:_camp_data/_status_data/_gp_summary_text 三件薄委托原签名转发(ui_click/ui_struct 直调面不动) - 范围扩展(用户令「全部改代码」下压 S13 文件长度线):_build_ui 拆八步 → main_ui_build;冒险/覆盖层/调试流程族 → main_adventure_flow / main_overlay_flow / main_debug_flow(逐字搬运,成员访问改 root 前缀);诊断转储接线留在组合根(source 扫描断言零改动) - main.gd 1506→946 行(S13 ≤999 达标);九条基线外漂移经代码压回后扫描 229/229、新违规 0,tools/gdstyle_baseline.txt 零改动 - 等价证据:hud_presenter_test / pacing_queue_test / theme_projection_test(162 项)/ modal_flow / standee_fx_sweep / player_copy_sweep / source_guard / meta_progression_forge / breakpoint_resume / level_shop_economy 全绿;probe_gate --click --full 全绿(click 问题=0);冒烟 17/17;S9 类型闸 PASS;撤销证明(删 gp_summary_text 承接体→Parse Error 红→还原复绿)在案 --- game/presentation/board_resolvers.gd | 67 ++ game/presentation/hud_presenter.gd | 112 ++++ game/presentation/main.gd | 772 ++++------------------- game/presentation/main_adventure_flow.gd | 213 +++++++ game/presentation/main_debug_flow.gd | 105 +++ game/presentation/main_overlay_flow.gd | 155 +++++ game/presentation/main_ui_build.gd | 238 +++++++ game/presentation/views/ui_kit.gd | 2 +- 8 files changed, 997 insertions(+), 667 deletions(-) create mode 100644 game/presentation/board_resolvers.gd create mode 100644 game/presentation/main_adventure_flow.gd create mode 100644 game/presentation/main_debug_flow.gd create mode 100644 game/presentation/main_overlay_flow.gd create mode 100644 game/presentation/main_ui_build.gd diff --git a/game/presentation/board_resolvers.gd b/game/presentation/board_resolvers.gd new file mode 100644 index 00000000..554d8c5e --- /dev/null +++ b/game/presentation/board_resolvers.gd @@ -0,0 +1,67 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 棋盘每格渲染数据的只读解析回调族:奖励徽章身份(挖掘期 Active 相位内)、地形变异数字遮蔽与角标、 +## 调试加成生效值。有态绑定件——构造注入 db、调试浮窗与结算活态取值,产出绑定 Callable 供组合根接线; +## 与无态静态投影族(HudPresenter)职责互斥。 +extends RefCounted + +var _db: ConfigDb +var _debug_overlay: DebugOverlay +var _settlement_getter: Callable + + +func _init(db_ref: ConfigDb, debug_overlay_ref: DebugOverlay, settlement_getter: Callable) -> void: + _db = db_ref + _debug_overlay = debug_overlay_ref + _settlement_getter = settlement_getter + + +# 加成生效值供给(debug_session 注入 Callable——每次从浮窗百分比现算,经 encounter 传参零表写; +# 禁表写路径(双写叠加隐患源头); +# 合成式单点在 UiKit.scaled——与调试浮窗生效值预览共用,本类不持有同式私有实现) +func debug_hero_override() -> Dictionary: + return {"atk": UiKit.scaled(int(_db.hero.atk), _debug_overlay.atk_pct()), + "max_hp": UiKit.scaled(int(_db.hero.max_hp), _debug_overlay.hp_pct())} + + +# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; +# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透) +func identity_lookup(c: Vector2i) -> Variant: + var st: BattleOrchestrator = _settlement_getter.call() + if st == null or st.minesweeper.current_board() == null: + return null + if String(st.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: + return null + return st.query_reward_identity(c) + + +# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动。 +# 变异表直读注入 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession +# 构造传参),免 settlement 内部链式穿透) +func variant_mask_lookup(c: Vector2i) -> bool: + var st: BattleOrchestrator = _settlement_getter.call() + if st == null: + return false + var b = st.minesweeper.current_board() + if b == null: + return false + for vid in b.variant_ids(c): + var vrow: Dictionary[Variant, Variant] = _db.variation.row_by_id(String(vid)) + if String(vrow.get("effect", "")) == "hide_number": + return true + return false + + +# 地形变异 角标解析(伤害型变异未开格视觉标记;读取方按格子变异数据行动,规则本体零特判。 +# 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络。变异表直读注入 db) +func variant_mark_lookup(c: Vector2i) -> String: + var st: BattleOrchestrator = _settlement_getter.call() + if st == null: + return "" + var b = st.minesweeper.current_board() + if b == null: + return "" + for vid in b.variant_ids(c): + var vrow: Dictionary[Variant, Variant] = _db.variation.row_by_id(String(vid)) + if String(vrow.get("effect", "")) == "flat_damage": + return "flat_damage" + return "" diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index 2ce1214f..b7c7d9e7 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -5,6 +5,8 @@ class_name HudPresenter extends RefCounted +const Narrator := preload("res://presentation/battle_narrator.gd") + # 每次结算行/轮次/节点变化/动作返回后调用(组合根全量重查装配) # 钱包 GP/钥石不在本装配面投喂(中/大循环资源迁出战斗屏——信息归属单一展示面原则); @@ -216,3 +218,113 @@ static func _p90(a: Array) -> float: var b := a.duplicate() b.sort() return float(b[mini(b.size() - 1, int(0.9 * b.size()))]) + + +# ---- 组合根判离承接·数据投影族(静态无状态——db/save/meta/runner 现值由调用侧传参) ---- + +# 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装—— +# 快照仅服务单次开层生命周期(open_status 重建各行),跨开层零持有;零写调用。 +# 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息; +# ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退 +# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链) +static func status_data(db: ConfigDb, meta: MetaProgressionCore, runner: Variant, treasure_ui: Variant) -> Dictionary: + return { + "balance": meta.keystone_balance(), + "hero_hp": int(runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命) + "max_hp": runner.effective_max_hp(), + "atk": runner.effective_atk(), + "ac": runner.effective_ac(), + "gp": int(runner.gp), + "curios": status_curios(treasure_ui), + "feats": feat_holdings_lines(db, runner.feats_held()), + } + + +# 秘宝行映射(inventory_projection 现役投影逐行取 name/used 两键——受理序沿库存投影; +# 禁第二份投影:件名换算已在 inventory_projection 收口,此处只做键裁剪) +static func status_curios(treasure_ui: Variant) -> Array: + var out: Array[Variant] = [] + for row in (treasure_ui.inventory_projection() as Array): + var r: Dictionary[Variant, Variant] = row + out.append({"name": String(r.get("name", "")), "used": bool(r.get("used", false))}) + return out + + +# 生涯页快照投影(只读同源——每次开层实时读 SaveManager 计数访问器 + MetaProgression 访问器 +# 现值拼装(状态面板 status_data 同构);快照仅服务单次开层生命周期,跨开层零持有;零写调用。 +# 生涯页只统计终身计数——不统计天赋树与账号成长态, +# 故 forge_levels_total/workshop/tree 三投影不在本投影内) +static func career_data(db: ConfigDb, save_manager: SaveManagerCore, meta: MetaProgressionCore) -> Dictionary: + var clears: Array[Dictionary] = [] + var boss_first := 0 + for i in db.level_count(): + var li: int = i + 1 + clears.append({"display_name": String(db.level_row(li).get("display_name", "")), + "count": save_manager.clear_count(li)}) + if save_manager.boss_first_kill_marked(li): + boss_first += 1 + return { + "total": save_manager.adventure_total(), + "cleared": save_manager.adventure_cleared(), + "clears": clears, + "boss_first_kills": boss_first, + "boss_first_total": db.level_count(), + "boss_kills": save_manager.boss_kill_count(), + "keystone": meta.keystone_balance(), + } + + +# 槽数据快照(只读同源——每次开层实时经卡① 窥视 API 逐槽取数拼装;零写调用)。 +# occupied = has_file ∧ readable:坏槽/高版本槽按空槽呈现(Q2 裁甲——进档/新档均经卡① 兜底链接管); +# 摘要仅三项(裁决 4),has_file 只是空/有档操作集二分判定来处(非摘要项)。 +static func save_select_data(save_manager: SaveManagerCore) -> Dictionary: + var slots: Array[Dictionary] = [] + for i in range(1, save_manager.SLOT_COUNT + 1): + var p := save_manager.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒) + slots.append({"slot": i, + "occupied": bool(p.get("has_file", false)) and bool(p.get("readable", false)), + "keystone": int(p.get("keystone", 0)), + "cleared": int(p.get("cleared", 0)), + "total": int(p.get("total", 0))}) + return {"slots": slots} + + +# 营地投影(承接件单点转发 camp_shop_presenter——组合根只留探针缝薄委托) +static func camp_data(pres: Variant) -> Dictionary: + return pres.camp_data() + + +# ---- 组合根判离承接·文案拼装三件(纯拼装零状态) ---- + +# 战利品合计行(runner 缺位 = 续玩恢复面:resume_adventure 的 bonus_pending 分支在 level_started +# 之前同步 curio_choice_presented.emit〔resume 提前 return〕,本行求值时 runner 尚未赋值; +# 合计改读断点快照 gp 兜底——恢复源快照此刻必在档,语义=续玩恢复面的合计即存档合计) +static func gp_summary_text(payload: Dictionary, runner: Variant, save_manager: SaveManagerCore) -> String: + if runner == null: + var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() + return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(bp.get("gp", 0))] + return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)] + + +# 结算三行账·本关加成行装配(呈现只读投影——runner.bonus 统一加算区同源;零加成=行隐藏) +static func bonus_summary_text(runner: Variant) -> String: + if runner == null: + return "" + var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 加算区住 RunLedger + var parts: Array[String] = [] + if int(b.get("atk", 0)) != 0: + parts.append("攻 %+d" % int(b.get("atk", 0))) + if int(b.get("max_hp", 0)) != 0: + parts.append("上限 %+d" % int(b.get("max_hp", 0))) + if int(b.get("armor_class", 0)) != 0: + parts.append("AC %+d" % int(b.get("armor_class", 0))) + return "本关加成 " + " · ".join(parts) + + +# 相位行(文案装配归 narrator——settlement 缺位 = 非战斗态;相位取数经 presentation_snapshot 成文面) +static func phase_text(settlement: BattleOrchestrator, runner: Variant, boss_active: bool, db: ConfigDb) -> String: + if settlement == null: + return "第 — 轮 · —" + return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result), + String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, + boss_active, Narrator.boss_name(runner, db)) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index e474c8a3..cb5233f0 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -22,6 +22,11 @@ const RowPacer := preload("res://presentation/row_pacer.gd") const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互 const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互 const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影 +const BoardResolversScript := preload("res://presentation/board_resolvers.gd") # 棋盘解析回调族 +const MainUiBuild := preload("res://presentation/main_ui_build.gd") # 装配分步承接件 +const MainAdventureFlow := preload("res://presentation/main_adventure_flow.gd") # 冒险流程承接件 +const MainOverlayFlow := preload("res://presentation/main_overlay_flow.gd") # 覆盖层流程承接件 +const MainDebugFlow := preload("res://presentation/main_debug_flow.gd") # 调试流程承接件 const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景 const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") @@ -188,10 +193,6 @@ func _ready() -> void: settings_store = SettingsStore.new() settings_store.load_settings() settings_store.apply() - _debug = DebugSessionScript.new(db, get_node(AUTOLOAD_PATH_RNG), - _debug_hero_override, _seed_value) - _debug.battle_created.connect(_on_debug_battle_created) - _debug.battle_ready.connect(_on_debug_battle_ready) _build_ui() _begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序) _start_overlay.visible = true @@ -204,199 +205,13 @@ func _ready() -> void: # ==================== 视图接线(三屏壳 + 模态层装配;构建归屏壳/UiAssembler,本类只接线) ==================== func _build_ui() -> void: - # 三屏壳实例化:冒险屏先挂、菜单屏后挂(启动态菜单在顶层=玩家可见—— - # 装配序纪律:菜单先挂会被冒险屏盖死,F5 见战斗界面);最终树序+登记序以 LAYER_CONTRACT - # 为唯一事实源,_apply_layer_contract 在装配尾统一落位。 - # 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差; - # 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、 - # debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。 - _adventure_screen = AdventureScreenScene.instantiate() - add_child(_adventure_screen) - _menu_screen = MenuScreenScene.instantiate() - add_child(_menu_screen) - _start_overlay = _menu_screen.start_overlay - _battle_root = _adventure_screen.battle_root - stage = _adventure_screen.stage - log_view = _adventure_screen.log_view - info_panel = _adventure_screen.info_panel - board_zone = _adventure_screen.board_zone - board_view = _adventure_screen.board_view # 探针兼容别名(= board_zone.board_view) - ap_bar = _adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar) - settlement_fx = _adventure_screen.settlement_fx - level_screen = _adventure_screen.level_screen - - # 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) - _row_pacer = RowPacer.new() - _row_pacer.log_view = log_view - _row_pacer.stage = stage - _row_pacer.board_zone = board_zone - _row_pacer.settlement_fx = settlement_fx - _row_pacer.board_view = board_view - _row_pacer.refresh_request = _refresh_presentations - _row_pacer.pump_ended = _on_pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放) - add_child(_row_pacer) - # 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner 随关卡更替, - # 经现值回调注入——仿 identity_resolver 只读回调先例) - _treasure_ui = TreasureInteractController.new(db, meta, info_panel, board_zone.toast, board_view, - func() -> Variant: return run_scope.settlement, func() -> Variant: return run_scope.runner) - - board_zone.action_requested.connect(_on_board_action) - board_zone.status_badge.pressed.connect(_on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标) - info_panel.treasure_slot_clicked.connect(_treasure_ui.on_slot_clicked) # 两步交互第一步 - board_zone.stop_pressed.connect(_on_stop_pressed) - board_zone.refresh_confirmed.connect(_on_refresh_confirmed) - board_zone.legend_toggled.connect(_on_legend_toggled) - board_zone.hover_forbidden.connect(_on_hover_forbidden) - board_zone.chord_attempted.connect(_on_chord_attempted) - - # 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) - board_view.set_identity_resolver(_identity_lookup) - # 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取) - board_view.set_variant_mask_resolver(_variant_mask_lookup) - board_view.set_variant_mark_resolver(_variant_mark_lookup) # flat_damage 未开格角标 - - logger = PlaytestLogger.new() - logger.ap_per_round = int(db.action_economy.ap_per_round) - - # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 - # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; - # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) - var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(self, db) - legend_overlay = overlays.legend_overlay - node_overlay = overlays.node_overlay - _end_overlay = overlays.end_overlay - shop_overlay = overlays.shop_overlay - _camp_screen = CampScreenScene.instantiate() - add_child(_camp_screen) - camp_overlay = _camp_screen.camp_overlay - # 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child - legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay)) - node_overlay.continue_pressed.connect(_on_node_continue) - level_screen.route_chosen.connect(_on_route_chosen) - level_screen.status_badge.pressed.connect(_on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) - level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口 - _end_overlay.continue_pressed.connect(_on_battle_end_continue) - settings_overlay = SettingsOverlay.new(settings_store) # 最小设置面(音量/全屏/减动效) - add_child(settings_overlay) - settings_overlay.closed.connect(_on_settings_closed) - # 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前; - # 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路) - status_panel = StatusPanel.new() - add_child(status_panel) - status_panel.closed.connect(_on_status_closed) - # 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前; - # 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数 - # 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮) - career_overlay = CareerOverlay.new() - add_child(career_overlay) - career_overlay.closed.connect(_on_career_closed) - # 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号 - #(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。 - save_select_overlay = SaveSelectOverlay.new() - add_child(save_select_overlay) - save_select_overlay.closed.connect(_on_save_select_closed) - save_select_overlay.new_slot_requested.connect(_on_save_select_new) - save_select_overlay.load_slot_requested.connect(_on_save_select_load) - save_select_overlay.delete_slot_requested.connect(_on_save_select_delete) - # 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进; - # 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child) - curio_overlay = CurioPickOverlay.new() - add_child(curio_overlay) - # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, - # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) - _bonus = CurioInteractController.new(CurioFlowContext.new(db, func() -> Variant: return meta, curio_overlay, _end_overlay, board_view, - board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return run_scope.adventure, - func() -> Variant: return run_scope.runner, func() -> Variant: return run_scope.is_adventure, - _open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations, - _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": run_scope.last_gain_amount}))) # 构造参数收拢 context - curio_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed) - curio_overlay.continue_pressed.connect(_bonus.on_continue) - curio_overlay.refresh_requested.connect(_bonus.on_refresh) - - # 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3: - # 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环) - _menu_screen.entersavebtn.pressed.connect(_open_save_select) - # 调试入口=UI 最顶层全局浮层角标(任何 UI 不遮挡——菜单/战斗/地图三枚 - # 屏内角标由全局浮层承载;连接归组合根,树序/登记面见 LAYER_CONTRACT 末位) - debug_badge = UiAssembler.build_debug_badge(self) - add_child(debug_badge) - debug_badge.pressed.connect(_on_debug_badge_pressed) - # 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; - # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) - _camp_pres = CampShopPresenter.new(CampFlowContext.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay, - board_zone.toast, _start_overlay, logger, func() -> Variant: return run_scope.runner, - _open_modal, _close_modal, _refresh_presentations)) # 构造参数收拢 context - shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy) - shop_overlay.refresh_requested.connect(_camp_pres.on_shop_refresh) # MC-1 - shop_overlay.leave_pressed.connect(_camp_pres.on_shop_leave) - camp_overlay.buy_requested.connect(_camp_pres.on_camp_buy) - camp_overlay.attune_toggled.connect(_camp_pres.on_camp_attune_toggle) - camp_overlay.tree_buy_requested.connect(_camp_pres.on_camp_tree_buy) - camp_overlay.tree_fork_toggled.connect(_camp_pres.on_camp_fork_toggle) - camp_overlay.forge_buy_requested.connect(_camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression) - camp_overlay.forge_unlock_requested.connect(_camp_pres.on_camp_forge_unlock) - camp_overlay.forge_select_requested.connect(_camp_pres.on_camp_forge_select) - camp_overlay.closed.connect(_camp_pres.on_camp_closed) - _menu_screen.settingsbtn.pressed.connect(_open_settings) # 最小设置面入口 - _menu_screen.quitbtn.pressed.connect(_quit_game) # 退出游戏提级一级菜单 - - var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(self) - _inter_overlay = inter.overlay - _inter_label = inter.label - inter.next_btn.pressed.connect(_on_inter_next_pressed) - - # 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末 - #(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层; - # 弹窗只在开始菜单域弹出,绘制序恒最上; - # 诊断转储入口在调试浮窗) - confirm_dialog = ConfirmDialog.new() - add_child(confirm_dialog) - # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, - # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) - confirm_dialog.confirmed.connect(_on_confirm_accepted) - confirm_dialog.cancelled.connect(_on_confirm_declined) - - # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; - # 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块; - # 诊断转储入口在本浮窗,玩家画面零常驻调试件)。 - # 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态); - # 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口) - debug_overlay = DebugOverlay.new(db, int(db.hero.atk), int(db.hero.max_hp)) - add_child(debug_overlay) - _seed_edit = debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变) - debug_overlay.closed.connect(_on_modal_closed.bind(debug_overlay)) - debug_overlay.apply_requested.connect(_on_tune_apply) - debug_overlay.simulate_requested.connect(_on_tune_simulate) - debug_overlay.debug_levels_requested.connect(_start_debug_levels) + MainUiBuild.build(self) debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口 - # 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT, - # _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、 - # 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child) - # 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable) - _modal_flow = ModalFlow.new() - _modal_flow.start_visible_get = func() -> bool: return _start_overlay.visible - _modal_flow.inter_visible_get = func() -> bool: return _inter_overlay.visible - _modal_flow.settlement_get = func() -> Variant: return run_scope.settlement - _modal_flow.rows_empty_get = func() -> bool: return _row_pacer.pending_rows().is_empty() - _modal_flow.flush_rows = _flush_row_queue - _modal_flow.on_open_sync = func() -> void: - _sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步 - _sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步 - _modal_flow.on_close_sync = func() -> void: - _sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径) - _sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套) - _modal_flow.replay_click = _replay_locked_click - _modals = _modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言) - # 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表) - _apply_layer_contract() - _tune_sim = TuneSimController.new(db, debug_overlay, int(db.hero.max_hp)) - func _fatal_screen(msg: String) -> void: - var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT) - l.set_anchors_preset(Control.PRESET_CENTER) - add_child(l) + MainUiBuild.fatal_screen(self, msg) + # ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ==================== @@ -499,7 +314,7 @@ func _on_confirm_accepted() -> void: match _confirm_consumer: "delete_slot": save_manager.delete_slot(_confirm_consumer_slot) - save_select_overlay.open_save_select(_save_select_data()) # 就地刷新(该槽回空态) + save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态) _confirm_consumer = "" @@ -537,21 +352,8 @@ func _resume_from_menu() -> void: return -# 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根;断点注入同口) func _wire_adventure() -> void: - run_scope.adventure = AdventureRunner.new(db, get_node(AUTOLOAD_PATH_RNG)) - run_scope.adventure.step_row.connect(_on_step_row) - run_scope.adventure.round_started.connect(_on_round_started) - run_scope.adventure.board_changed.connect(_on_board_changed) - run_scope.adventure.node_entered.connect(_on_runner_node) - run_scope.adventure.choices_presented.connect(_on_choices_presented) - run_scope.adventure.wallet_changed.connect(_on_wallet_changed) - # 三选一信号族接线 - run_scope.adventure.curio_choice_presented.connect(_bonus.on_presented) - run_scope.adventure.curio_resolved.connect(_bonus.on_resolved) - run_scope.adventure.adventure_finished.connect(_on_adventure_finished) - run_scope.adventure.level_started.connect(_on_adventure_level_started) - run_scope.adventure.set_save_manager(save_manager) # 断点承载注入(停点快照写盘;调试/模拟不经过此路径) + MainAdventureFlow._wire_adventure(self) # 冒险开局参数面构造(主菜单开新/续玩两入口共用:同调三键随开局下传。 @@ -596,204 +398,51 @@ func _start_adventure() -> void: return -# 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达; -# 本回调返回后才有 choices_presented 到达——入口选路层随即弹出) -func _on_adventure_level_started(level_index: int, display_name: String) -> void: - run_scope.runner = run_scope.adventure.runner() - # 开场行传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源: - # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 - # 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp - #(run_ledger.bonus——属性值提升入账后显示值已含加算区 flat,注记随动;armor_class/ap 不入条件: - # 开场行只显示 atk/max_hp 两值,注记指向不存在的显示轴反成失真),白板局不带 - var ov: Dictionary[Variant, Variant] = meta.hero_override() - var boosted: bool = int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 \ - or debug_overlay.atk_pct() > 0 or debug_overlay.hp_pct() > 0 \ - or int(run_scope.runner.run_ledger.bonus.get("atk", 0)) > 0 \ - or int(run_scope.runner.run_ledger.bonus.get("max_hp", 0)) > 0 - log_view.append_bb(Narrator.level_open_line(display_name, int(run_scope.adventure.master_seed), - run_scope.runner.effective_atk(), run_scope.runner.effective_max_hp(), boosted)) +func _on_adventure_level_started(level_index: int, display_name: String)-> void: + MainAdventureFlow._on_adventure_level_started(self, level_index, display_name) -func _on_runner_node(node: Dictionary, info: Dictionary) -> void: - # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 - if run_scope.is_adventure and run_scope.adventure != null: - run_scope.runner = run_scope.adventure.runner() - var t := String(node.get("type", "")) - if t == "battle" or t == "boss": - run_scope.settlement = run_scope.runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) - logger.attach_battle(run_scope.settlement) - run_scope.boss_active = t == "boss" - _show_board_fx_for_node(t) - log_view.append_bb(Narrator.node_banner(run_scope.runner, node, t)) - elif t == "reward": - # 奖励节点三选一(与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 - # 呈现编排归 curio_interact_controller.present_node_curio。 - _bonus.present_node_curio() - log_view.append_bb(Narrator.reward_line()) - elif t == "rest": - var healed: int = int(info.get("healed", 0)) - _open_modal(node_overlay) - node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))) - log_view.append_bb(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 对照承载) - _open_modal(shop_overlay) - if run_scope.runner.is_shop_pool(): - shop_overlay.open_shelf(run_scope.runner.shop_shelf(), int(run_scope.runner.gp)) - shop_overlay.set_visit_info(int(run_scope.runner.shop_bp_visit_buys()), float(db.shop_price_a())) # 爬价规则行 - shop_overlay.set_refresh_display(run_scope.runner.shop_refresh_free_left(), run_scope.runner.shop_refresh_cost(), int(run_scope.runner.gp) >= run_scope.runner.shop_refresh_cost()) # MC-1 - else: - shop_overlay.open_shop(run_scope.runner.shop_products(), int(run_scope.runner.gp)) - shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) - log_view.append_bb(Narrator.shop_line(int(run_scope.runner.gp))) - logger.log_node(node, info, run_scope.runner.node_index) - _refresh_presentations() -# ---- 冒险模式终局面(三选一选择面 / 冒险结算屏)---- +func _on_runner_node(node: Dictionary, info: Dictionary)-> void: + MainAdventureFlow._on_runner_node(self, node, info) + + +func _on_adventure_finished(summary: Dictionary)-> void: + MainAdventureFlow._on_adventure_finished(self, summary) + -# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 -func _on_adventure_finished(summary: Dictionary) -> void: - run_scope.level_active = false - var result: String = String(summary.get("result", "")) - run_scope.settlement = null - _exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 - _sync_status_entry() # 离局熄灭状态面板入口角标(防菜单误现) - _sync_settlement_visuals() # → 全关 - HudPresenter.battle_end_feedback(stage, ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道 - # 存档入账:settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— - # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 - var detail: Dictionary[String, Variant] = {} - detail.assign(meta.settle_adventure(summary)) - var text := Narrator.adventure_summary_text(summary) - if not detail.is_empty(): - text += "\n" + Narrator.keystone_settle_text(detail, _bonus.item_name_lookup()) - if result == "completed": - _end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(), - "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))) # GP 行(取数=summary gp_final 清账前快照);钥石明细已在 text(keystone_settle_text)——三行账 - else: - _end_overlay.open_defeat(text, "返回", board_view.get_global_rect()) - logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) - if run_scope.runner != null: - run_scope.runner.restore_hero_base() - _open_modal_after_playback(_end_overlay) - _refresh_presentations() func _on_battle_end_continue() -> void: - # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石在收束时入存档) - _close_modal(_end_overlay) - _end_overlay.close() - # 三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 - if _bonus.resume_if_pending(): # 三选一停点在途恢复入口(curio_interact_controller) - return - # 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) - if _settlement_stop_pending: - _settlement_stop_pending = false - _enter_level_screen() - level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()))) - level_screen.open_choices(run_scope.pending_candidates, run_scope.runner) - run_scope.pending_candidates = [] - _refresh_presentations() - return - # 会话收束:run_scope 整节点 queue_free()(帧末回收)——新闲时会话即建, - # 局态封印/账本/模态挂起全复位(局态字段清单见 run_scope.gd) - _debug.reset() - _begin_session(false) - debug_overlay.reset_sliders() - _start_overlay.visible = true - _refresh_presentations() + MainAdventureFlow._on_battle_end_continue(self) + + func _start_debug_levels() -> void: - # 守卫:调试对局与活跃冒险/结算/批量模拟并存不开放——并发会清写 - # settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸) - if run_scope.is_adventure or run_scope.settlement != null or _tune_sim.is_running(): - return - debug_overlay.reset_sliders() - _close_modal(debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) - _start_overlay.visible = false - log_view.clear() - log_view.append_bb(Narrator.debug_open_line(_seed_value(), 1, true)) - _begin_session(false) - if not _debug.start_debug_levels(): - _fatal_screen("战斗启动失败——见输出日志") + MainDebugFlow._start_debug_levels(self) -func _on_debug_battle_created(settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: - # begin_battle 前接手 + 接线(signal.connect 唯一归组合根)。settlement 必须先于 begin_battle - # 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务) - run_scope.settlement = settlement_ref - _wire_battle(settlement_ref) -func _on_debug_battle_ready(settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: - run_scope.settlement = settlement_ref - board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) - _check_board_hints() - _refresh_presentations() +func _on_debug_battle_created(settlement_ref: BattleOrchestrator, _level: int, _carry: int)-> void: + MainDebugFlow._on_debug_battle_created(self, settlement_ref, _level, _carry) + + + + +func _on_debug_battle_ready(settlement_ref: BattleOrchestrator, _level: int, _carry: int)-> void: + MainDebugFlow._on_debug_battle_ready(self, settlement_ref, _level, _carry) + + func _on_inter_next_pressed() -> void: - _inter_overlay.visible = false - if run_scope.is_adventure: - # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) - if run_scope.adventure != null and not run_scope.adventure.continue_to_next_level(): - _fatal_screen("关卡推进失败——见输出日志") - return - if not _debug.next_layer(): - _fatal_screen("战斗启动失败——见输出日志") + MainAdventureFlow._on_inter_next_pressed(self) -# 加成生效值供给(debug_session 注入 Callable——每次从浮窗百分比现算,经 encounter 传参零表写; -# 禁表写路径(双写叠加隐患源头); -# 合成式单点在 UiKit.scaled——与调试浮窗生效值预览共用,本类不持有同式私有实现) -func _debug_hero_override() -> Dictionary: - return {"atk": UiKit.scaled(int(db.hero.atk), debug_overlay.atk_pct()), - "max_hp": UiKit.scaled(int(db.hero.max_hp), debug_overlay.hp_pct())} - - -# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; -# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透) -func _identity_lookup(c: Vector2i): - if run_scope.settlement == null or run_scope.settlement.minesweeper.current_board() == null: - return null - if String(run_scope.settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: - return null - return run_scope.settlement.query_reward_identity(c) - - -# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动。 -# 变异表直读本根 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession -# 构造传参),免 settlement 内部链式穿透) -func _variant_mask_lookup(c: Vector2i) -> bool: - if run_scope.settlement == null: - return false - var b = run_scope.settlement.minesweeper.current_board() - if b == null: - return false - for vid in b.variant_ids(c): - var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid)) - if String(vrow.get("effect", "")) == "hide_number": - return true - return false - - -# 地形变异 角标解析(伤害型变异未开格视觉标记;读取方按格子变异数据行动,规则本体零特判。 -# 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络。变异表直读本根 db) -func _variant_mark_lookup(c: Vector2i) -> String: - if run_scope.settlement == null: - return "" - var b = run_scope.settlement.minesweeper.current_board() - if b == null: - return "" - for vid in b.variant_ids(c): - var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid)) - if String(vrow.get("effect", "")) == "flat_damage": - return "flat_damage" - return "" +# 加成生效值供给 / 奖励徽章身份解析 / 地形变异两种解析 —— 见 board_resolvers.gd(有态绑定族判离) func _show_board_fx_for_node(node_type: String = "") -> void: @@ -818,13 +467,13 @@ func _on_battle_finished(payload: Dictionary) -> void: "level_end": _end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), + "钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner), int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) "standalone_end": if result == "victory": _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), + "钥石 · 累计 %d" % run_scope.runner.keystone, HudPresenter.bonus_summary_text(run_scope.runner), int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), @@ -839,27 +488,9 @@ func _on_battle_finished(payload: Dictionary) -> void: # runner 缺位守卫(续玩停点恢复分支:adventure_runner # resume_adventure 的 bonus_pending 分支在 level_started 之前同步 curio_choice_presented.emit # 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照 -# 此刻必在档,语义=续玩恢复面的合计即存档合计;与 _bonus_summary_text 同款守卫式) +# 此刻必在档,语义=续玩恢复面的合计即存档合计;承接体见 HudPresenter.gp_summary_text) func _gp_summary_text(payload: Dictionary) -> String: - if run_scope.runner == null: - var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() - return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(bp.get("gp", 0))] - return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)] - - -# 结算三行账·本关加成行装配(呈现只读投影——runner.bonus 统一加算区同源;零加成=行隐藏) -func _bonus_summary_text() -> String: - if run_scope.runner == null: - return "" - var b: Dictionary[Variant, Variant] = run_scope.runner.run_ledger.bonus # 加算区住 RunLedger - var parts: Array[String] = [] - if int(b.get("atk", 0)) != 0: - parts.append("攻 %+d" % int(b.get("atk", 0))) - if int(b.get("max_hp", 0)) != 0: - parts.append("上限 %+d" % int(b.get("max_hp", 0))) - if int(b.get("armor_class", 0)) != 0: - parts.append("AC %+d" % int(b.get("armor_class", 0))) - return "本关加成 " + " · ".join(parts) + return HudPresenter.gp_summary_text(payload, run_scope.runner, save_manager) # ==================== core↔view 接线(战斗信号统一走这里)==================== @@ -978,46 +609,19 @@ func _show_hint(text: String, hint_id: String) -> void: # ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ---- func _on_node_continue() -> void: - _close_modal(node_overlay) - if run_scope.runner != null: - # 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 - run_scope.runner.continue_after_node() + MainAdventureFlow._on_node_continue(self) -func _on_choices_presented(candidates: Array) -> void: - # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(战斗胜利/兑付确认统一到达此处) - if run_scope.runner == null: - return - log_view.append_bb(Narrator.route_line(candidates, run_scope.runner)) - # 战斗胜利总结结算(battle.md OQ14):战斗胜利停点先弹 BattleEndOverlay - # 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点 - # 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点 - # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 - if run_scope.is_adventure and run_scope.runner.current_battle() == null \ - and String(run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): - _settlement_stop_pending = true - run_scope.pending_candidates = candidates.duplicate() - _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点", - board_view.get_global_rect(), - "战利品 GP +%d · 合计 %d" % [run_scope.last_gain_amount, int(run_scope.runner.gp)]) - _open_modal_after_playback(_end_overlay) - _refresh_presentations() - return - _enter_level_screen() - level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()))) - level_screen.open_choices(candidates, run_scope.runner) - _refresh_presentations() -func _on_route_chosen(node_id: int) -> void: - # 视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈 - # 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈 - # 打开低层时自动隐藏本屏、关闭自动恢复) - if not level_screen.visible: - _close_modal(level_screen) - _exit_level_screen() - if run_scope.runner != null and not run_scope.runner.pick_next_node(node_id): - push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) +func _on_choices_presented(candidates: Array)-> void: + MainAdventureFlow._on_choices_presented(self, candidates) + + + + +func _on_route_chosen(node_id: int)-> void: + MainAdventureFlow._on_route_chosen(self, node_id) # ---- 独立屏屏级互斥(单一出口:进屏隐战斗屏 / 出屏显战斗屏)---- @@ -1025,229 +629,108 @@ func _on_route_chosen(node_id: int) -> void: # 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。 func _enter_level_screen() -> void: - run_scope.level_screen_active = true - _battle_root.visible = false - _open_modal(level_screen) - _sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) + MainAdventureFlow._enter_level_screen(self) + + func _exit_level_screen() -> void: - run_scope.level_screen_active = false - _battle_root.visible = true - if level_screen.visible: - _close_modal(level_screen) - _sync_status_entry() # 同上 + MainAdventureFlow._exit_level_screen(self) + + func _on_abandon_requested() -> void: - # 放弃入口(冒险结构.md 已放开「MVP 无界面入口」后置条款; - # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, - # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 - if not run_scope.is_adventure or run_scope.adventure == null: - return - if not run_scope.adventure.abandon(): - logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) - board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") + MainAdventureFlow._on_abandon_requested(self) -# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)---- - # ---- 最小设置面(入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)---- func _open_settings() -> void: - _start_overlay.visible = false - _open_modal(settings_overlay) # 模态栈单口 + MainOverlayFlow._open_settings(self) + + func _on_settings_closed() -> void: - _close_modal(settings_overlay) # 模态栈单口 - _start_overlay.visible = true + MainOverlayFlow._on_settings_closed(self) -# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- - -# 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装—— -# 快照仅服务单次开层生命周期(open_status 重建各行),跨开层零持有;零写调用。 -# 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息; -# ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退 -# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链) func _status_data() -> Dictionary: - return { - "balance": meta.keystone_balance(), - "hero_hp": int(run_scope.runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命) - "max_hp": run_scope.runner.effective_max_hp(), - "atk": run_scope.runner.effective_atk(), - "ac": run_scope.runner.effective_ac(), - "gp": int(run_scope.runner.gp), - "curios": _status_curios(), - "feats": HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()), - } + return MainOverlayFlow._status_data(self) -# 秘宝行映射(inventory_projection 现役投影逐行取 name/used 两键——受理序沿库存投影; -# 禁第二份投影:件名换算已在 inventory_projection 收口,此处只做键裁剪) -func _status_curios() -> Array: - var out: Array[Variant] = [] - for row in (_treasure_ui.inventory_projection() as Array): - var r: Dictionary[Variant, Variant] = row - out.append({"name": String(r.get("name", "")), "used": bool(r.get("used", false))}) - return out func _open_status_panel() -> void: - _open_modal(status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) - status_panel.open_status(_status_data()) + MainOverlayFlow._open_status_panel(self) + + func _on_status_closed() -> void: - _close_modal(status_panel) # 模态栈单口 + MainOverlayFlow._on_status_closed(self) # ---- 生涯页·只读(入口=开始层生涯钮;开合仿设置面先例:开始层互斥; # 数据快照随开随建零写通路,页内零跳转)---- func _open_career() -> void: - _start_overlay.visible = false - _open_modal(career_overlay) # 模态栈单口 - career_overlay.open_career(_career_data()) + MainOverlayFlow._open_career(self) + + func _on_career_closed() -> void: - _close_modal(career_overlay) # 模态栈单口 - _start_overlay.visible = true + MainOverlayFlow._on_career_closed(self) -# 生涯页快照投影(只读同源——每次开层实时读 SaveManager 计数访问器 + MetaProgression 访问器 -# 现值拼装(状态面板 _status_data 同构);快照仅服务单次开层生命周期,跨开层零持有;零写调用。 -# 生涯页只统计终身计数——不统计天赋树与账号成长态, -# 故 forge_levels_total/workshop/tree 三投影不在本投影内) -func _career_data() -> Dictionary: - var clears: Array[Dictionary] = [] - var boss_first := 0 - for i in db.level_count(): - var li: int = i + 1 - clears.append({"display_name": String(db.level_row(li).get("display_name", "")), - "count": save_manager.clear_count(li)}) - if save_manager.boss_first_kill_marked(li): - boss_first += 1 - return { - "total": save_manager.adventure_total(), - "cleared": save_manager.adventure_cleared(), - "clears": clears, - "boss_first_kills": boss_first, - "boss_first_total": db.level_count(), - "boss_kills": save_manager.boss_kill_count(), - "keystone": meta.keystone_balance(), - } - - -# ==================== 选档界面(营地交互重做卡③)==================== - -# 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为 -# 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。 func _open_save_select() -> void: - _start_overlay.visible = false - _open_modal(save_select_overlay) # 模态栈单口 - save_select_overlay.open_save_select(_save_select_data()) + MainOverlayFlow._open_save_select(self) + + func _on_save_select_closed() -> void: - _close_modal(save_select_overlay) # 模态栈单口 - _start_overlay.visible = true + MainOverlayFlow._on_save_select_closed(self) -# 槽数据快照(只读同源——每次开层实时经卡① 窥视 API 逐槽取数拼装;零写调用)。 -# occupied = has_file ∧ readable:坏槽/高版本槽按空槽呈现(Q2 裁甲——进档/新档均经卡① 兜底链接管); -# 摘要仅三项(裁决 4),has_file 只是空/有档操作集二分判定来处(非摘要项)。 -func _save_select_data() -> Dictionary: - var slots: Array[Dictionary] = [] - for i in range(1, save_manager.SLOT_COUNT + 1): - var p := save_manager.peek_slot_summary(i) # 推断 Dictionary(窥视返回合同——裸注解触 R1、参数化赋值运行期严格拒) - slots.append({"slot": i, - "occupied": bool(p.get("has_file", false)) and bool(p.get("readable", false)), - "keystone": int(p.get("keystone", 0)), - "cleared": int(p.get("cleared", 0)), - "total": int(p.get("total", 0))}) - return {"slots": slots} +func _on_save_select_new(slot: int)-> void: + MainOverlayFlow._on_save_select_new(self, slot) -# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘, -# 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。 -func _on_save_select_new(slot: int) -> void: - save_manager.create_slot(slot) - _camp_pres.open_camp() +func _on_save_select_load(slot: int)-> void: + MainOverlayFlow._on_save_select_load(self, slot) -# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。 -func _on_save_select_load(slot: int) -> void: - save_manager.activate_slot(slot) - _camp_pres.open_camp() +func _on_save_select_delete(slot: int)-> void: + MainOverlayFlow._on_save_select_delete(self, slot) -# 删档链(契约 3):第一次点击只出确认窗(消费方编排——open 前设消费方=delete_slot), -# 确认才删、取消不删;禁长按删/一次点击即删/撤销式软删。 -func _on_save_select_delete(slot: int) -> void: - _confirm_consumer = "delete_slot" - _confirm_consumer_slot = slot - confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") - - -# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰, -# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding) func _on_status_badge_pressed() -> void: - var map_on: bool = level_screen != null and level_screen.visible - # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; - # 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) - if (map_on and _modal_flow.badge_greyed_map()) or (not map_on and _modal_flow.badge_greyed_battle()): - return - _open_status_panel() + MainOverlayFlow._on_status_badge_pressed(self) -# 入口角标可见性/置灰同步(P1 丁+守卫——单一同步点):战斗屏角标=对局内且地图屏未激活; -# 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。 -# 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。 func _sync_status_entry() -> void: - if status_panel == null or _modals == null or board_zone == null or level_screen == null: - return - var map_on: bool = level_screen.visible - var battle_live: bool = run_scope.level_active or run_scope.settlement != null # 调试两层无 level_active——settlement 判据补位 - board_zone.status_badge.visible = run_scope.level_active and not map_on - level_screen.status_badge.visible = map_on - # 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点) - board_zone.status_badge.disabled = _modal_flow.badge_greyed_battle() # 互斥域谓词 - level_screen.status_badge.disabled = _modal_flow.badge_greyed_map() - # 状态角标判据(同上——重复语句在案): - # 战斗屏=对局内(冒险或调试两层)且地图屏未激活;地图屏=随屏;开始菜单零角标 - board_zone.status_badge.visible = run_scope.level_active and not map_on - level_screen.status_badge.visible = map_on - board_zone.status_badge.disabled = _modal_flow.badge_greyed_battle() # 互斥域谓词 - level_screen.status_badge.disabled = _modal_flow.badge_greyed_map() - # 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset - # 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底) - debug_badge.offset_bottom = -101.0 if map_on else -16.0 + MainOverlayFlow._sync_status_entry(self) # ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)---- func _on_camp_closed() -> void: - _camp_pres.on_camp_closed() + MainOverlayFlow._on_camp_closed(self) + + func _camp_data() -> Dictionary: - return _camp_pres.camp_data() + return MainOverlayFlow._camp_data(self) -func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: - # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 - logger.log_wallet(event, gp, extra) - if String(event) == "gain": - run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain) - if shop_overlay.visible and run_scope.runner != null: - if run_scope.runner.is_shop_pool(): - shop_overlay.refresh_shelf_state(int(run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) - else: - shop_overlay.refresh_state(run_scope.runner.shop_products(), int(run_scope.runner.gp)) - _refresh_presentations() + + +func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void: + MainOverlayFlow._on_wallet_changed(self, event, gp, extra) # ==================== 玩家意图 → 核心写调用(唯一出口)==================== @@ -1371,7 +854,8 @@ func _sync_settlement_visuals() -> void: if settling and not run_scope.was_settling: settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根 run_scope.was_settling = settling - HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, _phase_text()) + HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, + HudPresenter.phase_text(run_scope.settlement, run_scope.runner, run_scope.boss_active, db)) func _is_settling() -> bool: @@ -1388,80 +872,37 @@ func _is_settling() -> bool: return not _row_queue.is_empty() -func _phase_text() -> String: - # 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫;相位取数经 - # presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透) - if run_scope.settlement == null: - return "第 — 轮 · —" - return Narrator.phase_text(int(run_scope.settlement.round_index), String(run_scope.settlement.battle_result), - String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, - run_scope.boss_active, Narrator.boss_name(run_scope.runner, db)) +# 相位行 —— 承接体见 HudPresenter.phase_text(caller 直接改引) # ==================== B 区明细带 / 图例(模态入口)==================== func _on_legend_toggled() -> void: - if legend_overlay.is_open(): - _close_modal(legend_overlay) - else: - _open_modal(legend_overlay) + MainOverlayFlow._on_legend_toggled(self) # ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)==================== func _on_diagnose_pressed() -> void: - # 本局种子现值走 _current_master_seed 只读单点 - var ctx := {"mode": "level" if run_scope.level_active else "debug", "seed": _current_master_seed()} - if run_scope.level_active and run_scope.runner != null: - ctx["runner"] = {"node_index": run_scope.runner.node_index, "level_result": String(run_scope.runner.level_result), - "hero_hp": run_scope.runner.hero_hp, "settlement_is_null": run_scope.runner.current_battle() == null} - if run_scope.settlement != null: - ctx["settlement"] = run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) - var path: String = logger.dump_diagnosis(ctx) - board_zone.toast.show_toast("已转储诊断: " + path, "info") + MainDebugFlow._on_diagnose_pressed(self) # ==================== 调试浮窗(开合/应用/模拟编排由组合根承接)==================== func _show_debug_overlay() -> void: - # 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内: - # 棋盘输入锁/状态角标判据排除浮窗, - # 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制 - if debug_overlay == null: - return - _open_modal(debug_overlay) - _sync_debug_overlay() + MainDebugFlow._show_debug_overlay(self) -# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口) func _on_debug_badge_pressed() -> void: - var map_on: bool = level_screen != null and level_screen.visible - if (map_on and _modal_flow.badge_greyed_map()) or (not map_on and _modal_flow.badge_greyed_battle()): - return - _show_debug_overlay() + MainDebugFlow._on_debug_badge_pressed(self) -# 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合 -# 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律) func _sync_debug_overlay() -> void: - if debug_overlay == null: - return - debug_overlay.set_context_locked(run_scope.settlement != null or run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) - debug_overlay.update_run_data(_debug_run_data()) # 当局数据只读区块喂入(开窗+模态开合信号驱动) + MainDebugFlow._sync_debug_overlay(self) -# 当局数据六项只读投影(零直读内部状态、零写通路):种子=adventure.master_seed 现值 -# (非 _seed_value() 重算);调试两层/非对局无冒险对象=RNG 服务 master_seed 现值(只读出口, -# 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域); -# 缺位=「—」占位。零动效(纯标签重查)。 func _debug_run_data() -> Dictionary: - var r = run_scope.adventure.runner() if run_scope.adventure != null else null - var seed_now: int = _current_master_seed() - var seed_txt: String = str(seed_now) if seed_now >= 0 else "—" - if r == null: - return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} - return {"seed": seed_txt, "level": int(run_scope.adventure.level_index), "node": int(r.node_index), - "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(run_scope.adventure.keystone)} + return MainDebugFlow._debug_run_data(self) # 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值; @@ -1473,20 +914,19 @@ func _current_master_seed() -> int: func _close_debug_overlay() -> void: - _close_modal(debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 + MainDebugFlow._close_debug_overlay(self) -func _on_tune_apply(atk_pct: int, hp_pct: int) -> void: - # 加成无持久态可"落"——各战斗开局经 _debug_hero_override/runner 现算生效(应用=关闭浮窗) - _close_debug_overlay() -func _on_tune_simulate(n: int) -> void: - # 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数。 - # 对局中禁用兜底(长程异步模拟与活跃冒险/结算并存态不开放;探针 emit 绕过 disabled 时由此守住) - if run_scope.settlement != null or run_scope.is_adventure: - return - _tune_sim.run(n) +func _on_tune_apply(atk_pct: int, hp_pct: int)-> void: + MainDebugFlow._on_tune_apply(self, atk_pct, hp_pct) + + + + +func _on_tune_simulate(n: int)-> void: + MainDebugFlow._on_tune_simulate(self, n) # ==================== 工具 ==================== diff --git a/game/presentation/main_adventure_flow.gd b/game/presentation/main_adventure_flow.gd new file mode 100644 index 00000000..e6aa68b9 --- /dev/null +++ b/game/presentation/main_adventure_flow.gd @@ -0,0 +1,213 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 组合根冒险流程承接件:菜单开局/续玩之后的冒险编排接线、节点事件分发、终局面与选路屏开合。 +## 本件是 main.gd 同名函数的逐字搬运(成员访问改 root 前缀)——main 侧留同名薄委托,探针 main.X 路径零改动。 +## 局态仍全局住在 run_scope 节点:本件零成员态、零信号连接新增(连接语句随原段搬运)。 +extends RefCounted + +# 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根;断点注入同口) +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.step_row.connect(root._on_step_row) + root.run_scope.adventure.round_started.connect(root._on_round_started) + root.run_scope.adventure.board_changed.connect(root._on_board_changed) + root.run_scope.adventure.node_entered.connect(root._on_runner_node) + root.run_scope.adventure.choices_presented.connect(root._on_choices_presented) + root.run_scope.adventure.wallet_changed.connect(root._on_wallet_changed) + # 三选一信号族接线 + root.run_scope.adventure.curio_choice_presented.connect(root._bonus.on_presented) + root.run_scope.adventure.curio_resolved.connect(root._bonus.on_resolved) + 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.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.effective_atk/effective_max_hp 与战斗面板同源: + # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 + # 后缀判断认任一加成源——永久强化 atk/max_hp flat 或调试浮窗 pct 或局内加算区 atk/max_hp + #(run_ledger.bonus——属性值提升入账后显示值已含加算区 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 int(root.run_scope.runner.run_ledger.bonus.get("atk", 0)) > 0 \ + or int(root.run_scope.runner.run_ledger.bonus.get("max_hp", 0)) > 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)) + +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.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._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() # 同上 + +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") + + +# ---- 营地覆盖层与钥石账本(写调用唯一归组合根)---- + diff --git a/game/presentation/main_debug_flow.gd b/game/presentation/main_debug_flow.gd new file mode 100644 index 00000000..daac63a0 --- /dev/null +++ b/game/presentation/main_debug_flow.gd @@ -0,0 +1,105 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 组合根调试流程承接件:调试浮窗开合与上下文同步、批量模拟入口、两层调试对局起手、诊断转储。 +## 本件是 main.gd 同名函数的逐字搬运(成员访问改 root 前缀)——main 侧留同名薄委托,探针 main.X 路径零改动。 +## 调试会话状态机归 debug_session.gd;本件只接线与出 UI,零成员态。 +extends RefCounted + +static func _on_diagnose_pressed(root: Variant) -> void: + # 本局种子现值走 _current_master_seed 只读单点 + var ctx := {"mode": "level" if root.run_scope.level_active else "debug", "seed": root._current_master_seed()} + if root.run_scope.level_active and root.run_scope.runner != null: + ctx["runner"] = {"node_index": root.run_scope.runner.node_index, "level_result": String(root.run_scope.runner.level_result), + "hero_hp": root.run_scope.runner.hero_hp, "settlement_is_null": root.run_scope.runner.current_battle() == null} + if root.run_scope.settlement != null: + ctx["settlement"] = root.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) + var path: String = root.logger.dump_diagnosis(ctx) + root.board_zone.toast.show_toast("已转储诊断: " + path, "info") + + + +static func _show_debug_overlay(root: Variant) -> void: + # 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内: + # 棋盘输入锁/状态角标判据排除浮窗, + # 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制 + if root.debug_overlay == null: + return + root._open_modal(root.debug_overlay) + _sync_debug_overlay(root) + + + +# 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _show_debug_overlay 单口) +static func _on_debug_badge_pressed(root: Variant) -> void: + var map_on: bool = root.level_screen != null and root.level_screen.visible + if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()): + return + _show_debug_overlay(root) + + + +# 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合 +# 单口(凡影响开态/对局态的路径必经其一——与 _sync_status_entry 同款纪律) +static func _sync_debug_overlay(root: Variant) -> void: + if root.debug_overlay == null: + return + root.debug_overlay.set_context_locked(root.run_scope.settlement != null or root.run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放) + root.debug_overlay.update_run_data(_debug_run_data(root)) # 当局数据只读区块喂入(开窗+模态开合信号驱动) + + + +# 当局数据六项只读投影(零直读内部状态、零写通路):种子=adventure.master_seed 现值 +# (非 _seed_value() 重算);调试两层/非对局无冒险对象=RNG 服务 master_seed 现值(只读出口, +# 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域); +# 缺位=「—」占位。零动效(纯标签重查)。 +static func _debug_run_data(root: Variant) -> Dictionary: + var r = root.run_scope.adventure.runner() if root.run_scope.adventure != null else null + var seed_now: int = root._current_master_seed() + var seed_txt: String = str(seed_now) if seed_now >= 0 else "—" + if r == null: + return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} + return {"seed": seed_txt, "level": int(root.run_scope.adventure.level_index), "node": int(r.node_index), + "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(root.run_scope.adventure.keystone)} + + + +static func _close_debug_overlay(root: Variant) -> void: + root._close_modal(root.debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 + +static func _on_tune_apply(root: Variant, atk_pct: int, hp_pct: int) -> void: + # 加成无持久态可"落"——各战斗开局经 board_resolvers.debug_hero_override/runner 现算生效(应用=关闭浮窗) + _close_debug_overlay(root) + +static func _on_tune_simulate(root: Variant, n: int) -> void: + # 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数。 + # 对局中禁用兜底(长程异步模拟与活跃冒险/结算并存态不开放;探针 emit 绕过 disabled 时由此守住) + if root.run_scope.settlement != null or root.run_scope.is_adventure: + return + root._tune_sim.run(n) + + + +static func _start_debug_levels(root: Variant) -> void: + # 守卫:调试对局与活跃冒险/结算/批量模拟并存不开放——并发会清写 + # settlement/编排态=状态机破坏;模拟在跑时 reset_sliders 会作废测量读数(d5 置灰之外的信号绕过闸) + if root.run_scope.is_adventure or root.run_scope.settlement != null or root._tune_sim.is_running(): + return + root.debug_overlay.reset_sliders() + root._close_modal(root.debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) + root._start_overlay.visible = false + root.log_view.clear() + root.log_view.append_bb(root.Narrator.debug_open_line(root._seed_value(), 1, true)) + root._begin_session(false) + if not root._debug.start_debug_levels(): + root._fatal_screen("战斗启动失败——见输出日志") + +static func _on_debug_battle_created(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: + # begin_battle 前接手 + 接线(signal.connect 唯一归组合根)。settlement 必须先于 begin_battle + # 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务) + root.run_scope.settlement = settlement_ref + root._wire_battle(settlement_ref) + +static func _on_debug_battle_ready(root: Variant, settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: + root.run_scope.settlement = settlement_ref + root.board_view.rebuild(root.run_scope.settlement.minesweeper.current_board(), root.run_scope.settlement.minesweeper) + root._check_board_hints() + root._refresh_presentations() diff --git a/game/presentation/main_overlay_flow.gd b/game/presentation/main_overlay_flow.gd new file mode 100644 index 00000000..ce9755d4 --- /dev/null +++ b/game/presentation/main_overlay_flow.gd @@ -0,0 +1,155 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 组合根覆盖层流程承接件:设置/状态面板/生涯页/选档界面/图例/钱包事件的开合与数据投喂。 +## 本件是 main.gd 同名函数的逐字搬运(成员访问改 root 前缀)——main 侧留同名薄委托,探针 main.X 路径零改动。 +## 全部开合走组合根模态单口(root._open_modal/_close_modal);本件零成员态。 +extends RefCounted + +static func _open_settings(root: Variant) -> void: + root._start_overlay.visible = false + root._open_modal(root.settings_overlay) # 模态栈单口 + +static func _on_settings_closed(root: Variant) -> void: + root._close_modal(root.settings_overlay) # 模态栈单口 + root._start_overlay.visible = true + + +# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- + + +# 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装—— +# 快照仅服务单次开层生命周期(open_status 重建各行),跨开层零持有;零写调用。 +# 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息; +# ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退 +# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链) +static func _status_data(root: Variant) -> Dictionary: + return HudPresenter.status_data(root.db, root.meta, root.run_scope.runner, root._treasure_ui) + +static func _open_status_panel(root: Variant) -> void: + root._open_modal(root.status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项) + root.status_panel.open_status(_status_data(root)) + +static func _on_status_closed(root: Variant) -> void: + root._close_modal(root.status_panel) # 模态栈单口 + + + +static func _open_career(root: Variant) -> void: + root._start_overlay.visible = false + root._open_modal(root.career_overlay) # 模态栈单口 + root.career_overlay.open_career(HudPresenter.career_data(root.db, root.save_manager, root.meta)) + +static func _on_career_closed(root: Variant) -> void: + root._close_modal(root.career_overlay) # 模态栈单口 + root._start_overlay.visible = true + + +# 生涯页快照投影 —— 承接体见 HudPresenter.career_data(开层调用点直接改引) + + +# ==================== 选档界面(营地交互重做卡③)==================== + + +# 开层(模态栈单口;与开始层互斥=career 同款先例)。「进入存档」钮归卡② mn3——本函数为 +# 该钮的唯一落点(卡③ 批内探针函数直调=中间态,实现报告「已知中间态」登记)。 +static func _open_save_select(root: Variant) -> void: + root._start_overlay.visible = false + root._open_modal(root.save_select_overlay) # 模态栈单口 + root.save_select_overlay.open_save_select(HudPresenter.save_select_data(root.save_manager)) + +static func _on_save_select_closed(root: Variant) -> void: + root._close_modal(root.save_select_overlay) # 模态栈单口 + root._start_overlay.visible = true + + +# 槽数据快照 —— 承接体见 HudPresenter.save_select_data(开层调用点直接改引) + + + +# 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘, +# 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。 +static func _on_save_select_new(root: Variant, slot: int) -> void: + root.save_manager.create_slot(slot) + root._camp_pres.open_camp() + + + +# 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。 +static func _on_save_select_load(root: Variant, slot: int) -> void: + root.save_manager.activate_slot(slot) + root._camp_pres.open_camp() + + + +# 删档链(契约 3):第一次点击只出确认窗(消费方编排——open 前设消费方=delete_slot), +# 确认才删、取消不删;禁长按删/一次点击即删/撤销式软删。 +static func _on_save_select_delete(root: Variant, slot: int) -> void: + root._confirm_consumer = "delete_slot" + root._confirm_consumer_slot = slot + root.confirm_dialog.open("删除存档位 %d?删除后该档不可恢复。" % slot, "删除", "取消") + + + +# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰, +# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding) +static func _on_status_badge_pressed(root: Variant) -> void: + var map_on: bool = root.level_screen != null and root.level_screen.visible + # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; + # 地图屏自身的登记开态不计;判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) + if (map_on and root._modal_flow.badge_greyed_map()) or (not map_on and root._modal_flow.badge_greyed_battle()): + return + _open_status_panel(root) + + + +# 入口角标可见性/置灰同步(P1 丁+守卫——单一同步点):战斗屏角标=对局内且地图屏未激活; +# 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。 +# 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。 +static func _sync_status_entry(root: Variant) -> void: + if root.status_panel == null or root._modals == null or root.board_zone == null or root.level_screen == null: + return + var map_on: bool = root.level_screen.visible + var battle_live: bool = root.run_scope.level_active or root.run_scope.settlement != null # 调试两层无 level_active——settlement 判据补位 + root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on + root.level_screen.status_badge.visible = map_on + # 置灰判据为排除式(调试浮窗开态不计入:对局中浮窗开着时角标仍可点) + root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词 + root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map() + # 状态角标判据(同上——重复语句在案): + # 战斗屏=对局内(冒险或调试两层)且地图屏未激活;地图屏=随屏;开始菜单零角标 + root.board_zone.status_badge.visible = root.run_scope.level_active and not map_on + root.level_screen.status_badge.visible = map_on + root.board_zone.status_badge.disabled = root._modal_flow.badge_greyed_battle() # 互斥域谓词 + root.level_screen.status_badge.disabled = root._modal_flow.badge_greyed_map() + # 调试浮层角标(全局浮层任何 UI 不遮挡——可见性不随场景变,只抬 offset + # 避让:地图屏底栏 85px 常驻,角标抬至底栏上方 16px;其余场景右下角 16px 贴底) + root.debug_badge.offset_bottom = -101.0 if map_on else -16.0 + + + +static func _on_camp_closed(root: Variant) -> void: + root._camp_pres.on_camp_closed() + +static func _camp_data(root: Variant) -> Dictionary: + return HudPresenter.camp_data(root._camp_pres) + +static func _on_wallet_changed(root: Variant, event: String, gp: int, extra: Dictionary) -> void: + # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 + root.logger.log_wallet(event, gp, extra) + if String(event) == "gain": + root.run_scope.last_gain_amount = int(extra.get("amount", 0)) # GP 增量取数(每战恰一笔 gain) + if root.shop_overlay.visible and root.run_scope.runner != null: + if root.run_scope.runner.is_shop_pool(): + root.shop_overlay.refresh_shelf_state(int(root.run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) + else: + root.shop_overlay.refresh_state(root.run_scope.runner.shop_products(), int(root.run_scope.runner.gp)) + root._refresh_presentations() + + + +static func _on_legend_toggled(root: Variant) -> void: + if root.legend_overlay.is_open(): + root._close_modal(root.legend_overlay) + else: + root._open_modal(root.legend_overlay) + + diff --git a/game/presentation/main_ui_build.gd b/game/presentation/main_ui_build.gd new file mode 100644 index 00000000..269be5fa --- /dev/null +++ b/game/presentation/main_ui_build.gd @@ -0,0 +1,238 @@ +## VERTICAL SLICE - NOT FOR PRODUCTION +## 组合根 UI 装配分步承接件:三屏壳实例化、视图句柄接线、覆盖层装配与模态编排注入按步拆开。 +## 本件是 main._build_ui 的逐字分步搬运(成员访问改 root 前缀)——main 侧留同名薄委托,装配序与 z 序零改动。 +## 层序唯一事实源仍是 main.LAYER_CONTRACT + _apply_layer_contract(本件不持有层序逻辑)。 +extends RefCounted + +const UiAssembler := preload("res://presentation/ui_assembler.gd") +const RowPacer := preload("res://presentation/row_pacer.gd") +const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") +const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") +const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") +const BoardResolversScript := preload("res://presentation/board_resolvers.gd") +const DebugSessionScript := preload("res://presentation/debug_session.gd") +const TuneSimController := preload("res://presentation/tune_sim_controller.gd") +const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") +const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") +const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") + + + +static func build(root: Variant) -> void: + _build_screens(root) + _build_battle_wiring(root) + _build_overlays(root) + _build_aux_panels(root) + _build_curio(root) + _build_menu_wiring(root) + _build_debug_and_resolvers(root) + _build_modal_flow(root) + +static func _build_screens(root: Variant) -> void: + # 三屏壳实例化:冒险屏先挂、菜单屏后挂(启动态菜单在顶层=玩家可见—— + # 装配序纪律:菜单先挂会被冒险屏盖死,F5 见战斗界面);最终树序+登记序以 LAYER_CONTRACT + # 为唯一事实源,_apply_layer_contract 在装配尾统一落位。 + # 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差; + # 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、 + # debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。 + root._adventure_screen = AdventureScreenScene.instantiate() + root.add_child(root._adventure_screen) + root._menu_screen = MenuScreenScene.instantiate() + root.add_child(root._menu_screen) + root._start_overlay = root._menu_screen.start_overlay + root._battle_root = root._adventure_screen.battle_root + root.stage = root._adventure_screen.stage + root.log_view = root._adventure_screen.log_view + root.info_panel = root._adventure_screen.info_panel + root.board_zone = root._adventure_screen.board_zone + root.board_view = root._adventure_screen.board_view # 探针兼容别名(= board_zone.board_view) + root.ap_bar = root._adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar) + root.settlement_fx = root._adventure_screen.settlement_fx + root.level_screen = root._adventure_screen.level_screen + +static func _build_battle_wiring(root: Variant) -> void: + # 结算行排播泵(本体在 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) + root._row_pacer = RowPacer.new() + root._row_pacer.log_view = root.log_view + root._row_pacer.stage = root.stage + root._row_pacer.board_zone = root.board_zone + root._row_pacer.settlement_fx = root.settlement_fx + root._row_pacer.board_view = root.board_view + root._row_pacer.refresh_request = root._refresh_presentations + root._row_pacer.pump_ended = root._on_pump_ended # 演播完边沿统一派发(挂起 modal 优先,其次锁期点击重放) + root.add_child(root._row_pacer) + # 秘宝两步交互(本体在 treasure_interact_controller.gd;settlement/runner 随关卡更替, + # 经现值回调注入——仿 identity_resolver 只读回调先例) + root._treasure_ui = TreasureInteractController.new(root.db, root.meta, root.info_panel, root.board_zone.toast, root.board_view, + func() -> Variant: return root.run_scope.settlement, func() -> Variant: return root.run_scope.runner) + + root.board_zone.action_requested.connect(root._on_board_action) + root.board_zone.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(战斗屏 HUD 角标) + root.info_panel.treasure_slot_clicked.connect(root._treasure_ui.on_slot_clicked) # 两步交互第一步 + root.board_zone.stop_pressed.connect(root._on_stop_pressed) + root.board_zone.refresh_confirmed.connect(root._on_refresh_confirmed) + root.board_zone.legend_toggled.connect(root._on_legend_toggled) + root.board_zone.hover_forbidden.connect(root._on_hover_forbidden) + root.board_zone.chord_attempted.connect(root._on_chord_attempted) + + root.logger = PlaytestLogger.new() + root.logger.ap_per_round = int(root.db.action_economy.ap_per_round) + +static func _build_overlays(root: Variant) -> void: + # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 + # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读) < 确认弹窗 < 调试浮窗; + # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) + var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(root, root.db) + root.legend_overlay = overlays.legend_overlay + root.node_overlay = overlays.node_overlay + root._end_overlay = overlays.end_overlay + root.shop_overlay = overlays.shop_overlay + root._camp_screen = CampScreenScene.instantiate() + root.add_child(root._camp_screen) + root.camp_overlay = root._camp_screen.camp_overlay + # 营地屏壳插位(夹商店层与设置层之间)由 LAYER_CONTRACT 营地槽统一落位——禁散落 move_child + root.legend_overlay.closed.connect(root._on_modal_closed.bind(root.legend_overlay)) + root.node_overlay.continue_pressed.connect(root._on_node_continue) + root.level_screen.route_chosen.connect(root._on_route_chosen) + root.level_screen.status_badge.pressed.connect(root._on_status_badge_pressed) # 状态面板入口(地图屏顶栏角标) + root.level_screen.abandon_requested.connect(root._on_abandon_requested) # 放弃入口 + root._end_overlay.continue_pressed.connect(root._on_battle_end_continue) + +static func _build_aux_panels(root: Variant) -> void: + root.settings_overlay = SettingsOverlay.new(root.settings_store) # 最小设置面(音量/全屏/减动效) + root.add_child(root.settings_overlay) + root.settings_overlay.closed.connect(root._on_settings_closed) + # 局内状态面板·只读(登记设置层同档带 dim 遮罩,确认弹窗之前; + # 数据=组合根开层实时读 MetaProgression 访问器喂入,本层零写通路) + root.status_panel = StatusPanel.new() + root.add_child(root.status_panel) + root.status_panel.closed.connect(root._on_status_closed) + # 生涯页·只读全屏页(登记序=营地同档,设置层旁、确认弹窗之前; + # 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数 + # 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮) + root.career_overlay = CareerOverlay.new() + root.add_child(root.career_overlay) + root.career_overlay.closed.connect(root._on_career_closed) + # 选档界面(营地交互重做卡③):登记序=生涯同档(树序同步=addChild 同位);三动作只发意图信号 + #(激活/创建/删除+确认分派归组合根——视图零数据层依赖);入口=「进入存档」钮(卡② mn3 接线)。 + root.save_select_overlay = SaveSelectOverlay.new() + root.add_child(root.save_select_overlay) + root.save_select_overlay.closed.connect(root._on_save_select_closed) + root.save_select_overlay.new_slot_requested.connect(root._on_save_select_new) + root.save_select_overlay.load_slot_requested.connect(root._on_save_select_load) + root.save_select_overlay.delete_slot_requested.connect(root._on_save_select_delete) + +static func _build_curio(root: Variant) -> void: + # 提升三选一覆盖层(z 序 = 节点层之上、终局层之下;确认后同拍推进; + # 插位由 LAYER_CONTRACT curio 槽统一落位——禁散落 move_child) + root.curio_overlay = CurioPickOverlay.new() + root.add_child(root.curio_overlay) + # 提升三选一与替换子交互(本体在 curio_interact_controller.gd;本类只接线与回调注入, + # 模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) + root._bonus = CurioInteractController.new(CurioFlowContext.new(root.db, func() -> Variant: return root.meta, root.curio_overlay, root._end_overlay, root.board_view, + root.board_zone.toast, func() -> Variant: return root.save_manager, func() -> Variant: return root.run_scope.adventure, + func() -> Variant: return root.run_scope.runner, func() -> Variant: return root.run_scope.is_adventure, + root._open_modal, root._open_modal_after_playback, root._close_modal, root._refresh_presentations, + root._fatal_screen, func() -> String: return root._gp_summary_text({"gp_dropped": root.run_scope.last_gain_amount}))) # 构造参数收拢 context + root.curio_overlay.pick_confirmed.connect(root._bonus.on_pick_confirmed) + root.curio_overlay.continue_pressed.connect(root._bonus.on_continue) + root.curio_overlay.refresh_requested.connect(root._bonus.on_refresh) + +static func _build_menu_wiring(root: Variant) -> void: + # 菜单屏接线(句柄经屏壳暴露;三入口化——进入存档/设置/退出游戏,卡② mn3: + # 进入存档钮落点=卡③选档层 _open_save_select,点击链在此闭环) + root._menu_screen.entersavebtn.pressed.connect(root._open_save_select) + # 调试入口=UI 最顶层全局浮层角标(任何 UI 不遮挡——菜单/战斗/地图三枚 + # 屏内角标由全局浮层承载;连接归组合根,树序/登记面见 LAYER_CONTRACT 末位) + root.debug_badge = UiAssembler.build_debug_badge(root) + root.add_child(root.debug_badge) + root.debug_badge.pressed.connect(root._on_debug_badge_pressed) + # 营地/商店投影(本体在 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; + # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) + root._camp_pres = CampShopPresenter.new(CampFlowContext.new(root.db, func() -> Variant: return root.meta, func() -> Variant: return root.save_manager, root.camp_overlay, root.shop_overlay, + root.board_zone.toast, root._start_overlay, root.logger, func() -> Variant: return root.run_scope.runner, + root._open_modal, root._close_modal, root._refresh_presentations)) # 构造参数收拢 context + root.shop_overlay.buy_requested.connect(root._camp_pres.on_shop_buy) + root.shop_overlay.refresh_requested.connect(root._camp_pres.on_shop_refresh) # MC-1 + root.shop_overlay.leave_pressed.connect(root._camp_pres.on_shop_leave) + root.camp_overlay.buy_requested.connect(root._camp_pres.on_camp_buy) + root.camp_overlay.attune_toggled.connect(root._camp_pres.on_camp_attune_toggle) + root.camp_overlay.tree_buy_requested.connect(root._camp_pres.on_camp_tree_buy) + root.camp_overlay.tree_fork_toggled.connect(root._camp_pres.on_camp_fork_toggle) + root.camp_overlay.forge_buy_requested.connect(root._camp_pres.on_camp_forge_buy) # 锻造三意图(写口唯一归 MetaProgression) + root.camp_overlay.forge_unlock_requested.connect(root._camp_pres.on_camp_forge_unlock) + root.camp_overlay.forge_select_requested.connect(root._camp_pres.on_camp_forge_select) + root.camp_overlay.closed.connect(root._camp_pres.on_camp_closed) + root._menu_screen.settingsbtn.pressed.connect(root._open_settings) # 最小设置面入口 + root._menu_screen.quitbtn.pressed.connect(root._quit_game) # 退出游戏提级一级菜单 + +static func _build_debug_and_resolvers(root: Variant) -> void: + var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(root) + root._inter_overlay = inter.overlay + root._inter_label = inter.label + inter.next_btn.pressed.connect(root._on_inter_next_pressed) + + # 确认弹窗(新冒险互斥确认弹窗化):树序落在开始层/过场层之后 = 子级最末 + #(纪律「登记序 = 树序 = z 序」——confirm_dialog.open 禁运行时 move_child 自提抬层; + # 弹窗只在开始菜单域弹出,绘制序恒最上; + # 诊断转储入口在调试浮窗) + root.confirm_dialog = ConfirmDialog.new() + root.add_child(root.confirm_dialog) + # 消费方编排(卡③ 契约 3/sl8——禁双常驻静态接线):open 前设 _confirm_consumer, + # confirmed/cancelled 后按该状态量分派;分派面可容纳后续消费方(卡④ 到位直接接入不返工) + root.confirm_dialog.confirmed.connect(root._on_confirm_accepted) + root.confirm_dialog.cancelled.connect(root._on_confirm_declined) + + # 调试浮窗(主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口; + # 全局可开浮窗:调试会话恒开=启动即开〔_ready〕+两屏角标再开+当局数据只读区块; + # 诊断转储入口在本浮窗,玩家画面零常驻调试件)。 + # 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态); + # 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口) + root.debug_overlay = DebugOverlay.new(root.db, int(root.db.hero.atk), int(root.db.hero.max_hp)) + root.add_child(root.debug_overlay) + root._seed_edit = root.debug_overlay.seed_edit # 种子读取面在浮窗(_seed_value 唯一消费点不变) + root.debug_overlay.closed.connect(root._on_modal_closed.bind(root.debug_overlay)) + root.debug_overlay.apply_requested.connect(root._on_tune_apply) + root.debug_overlay.simulate_requested.connect(root._on_tune_simulate) + root.debug_overlay.debug_levels_requested.connect(root._start_debug_levels) + + # 棋盘解析回调族(有态绑定件——board_resolvers.gd 承接;组合根只接线;局部持有不占类成员位) + var resolvers = BoardResolversScript.new(root.db, root.debug_overlay, func() -> Variant: return root.run_scope.settlement) + # 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效) + root.board_view.set_identity_resolver(resolvers.identity_lookup) + # 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取) + root.board_view.set_variant_mask_resolver(resolvers.variant_mask_lookup) + root.board_view.set_variant_mark_resolver(resolvers.variant_mark_lookup) # flat_damage 未开格角标 + # 调试会话(英雄生效值预览回调绑同一解析件——创建晚于浮窗、随 _build_ui 一次接线) + root._debug = DebugSessionScript.new(root.db, root.get_node(root.AUTOLOAD_PATH_RNG), + func() -> Dictionary: return resolvers.debug_hero_override(), root._seed_value) + root._debug.battle_created.connect(root._on_debug_battle_created) + root._debug.battle_ready.connect(root._on_debug_battle_ready) + +static func _build_modal_flow(root: Variant) -> void: + # 模态登记(缺陷⑤纪律——登记序 = 树序 = z 序):层序唯一事实源=LAYER_CONTRACT, + # _apply_layer_contract 统一落位(关卡画面宿住冒险屏壳、营地屏壳夹商店与设置层之间、 + # 三选一对位终局层之下等特例均在合同数据内表达,禁散落 move_child) + # 模态编排注入(ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable) + root._modal_flow = ModalFlow.new() + root._modal_flow.start_visible_get = func() -> bool: return root._start_overlay.visible + root._modal_flow.inter_visible_get = func() -> bool: return root._inter_overlay.visible + root._modal_flow.settlement_get = func() -> Variant: return root.run_scope.settlement + root._modal_flow.rows_empty_get = func() -> bool: return root._row_pacer.pending_rows().is_empty() + root._modal_flow.flush_rows = root._flush_row_queue + root._modal_flow.on_open_sync = func() -> void: + root._sync_status_entry() # 状态面板入口角标可见性/置灰随开态同步 + root._sync_debug_overlay() # 浮窗当局数据/按钮态随开态同步 + root._modal_flow.on_close_sync = func() -> void: + root._sync_status_entry() # 关闭恢复角标可点态(无残留——同源口径) + root._sync_debug_overlay() # 他层关闭复活浮窗时同步刷新(关后自动恢复语义配套) + root._modal_flow.replay_click = root._replay_locked_click + root._modals = root._modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言) + # 视觉层合同装配(唯一事实源=LAYER_CONTRACT:树序+登记序同源派生——改序只改合同表) + root._apply_layer_contract() + root._tune_sim = TuneSimController.new(root.db, root.debug_overlay, int(root.db.hero.max_hp)) + +static func fatal_screen(root: Variant, msg: String) -> void: + var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT) + l.set_anchors_preset(Control.PRESET_CENTER) + root.add_child(l) diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 65015456..607f3b53 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -510,7 +510,7 @@ static func page_shell(host: Control, bright := true) -> Dictionary: return {"page": page, "base": base, "top_row": top_row, "box": box} -# 属性百分比合成(main._debug_hero_override 与调试浮窗生效值预览共用单点——两处同式实现收口) +# 属性百分比合成(board_resolvers.debug_hero_override 与调试浮窗生效值预览共用单点——两处同式实现收口) static func scaled(base: int, pct: int) -> int: return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))