# VERTICAL SLICE - NOT FOR PRODUCTION # Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证 # 实现 GDD:design/ux/battle.md(战斗屏 A/B/C/D 四区 + 全屏 States + 遥测;E 区随可选类删除下线 2026-09-15)。 # Date: 2026-09-12(battle.md 战斗屏改造批;基础 = 2026-09-11 HUD框架-lite 组合根;code-review 修复批重构) # 2026-09-13(封装审查修复批):核心状态读取收口至 战斗结算/runner 成文只读面(presentation_snapshot / # current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离 # debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。 # 组合根(composition root):唯一的模式流转与 core↔view 接线处。 # 职责边界:① 构建四区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线) # ② 模式管理(正式冒险 AdventureRunner——唯一试玩入口;调试两层编排归 debug_session.gd;单场原语仅探针使用) # ③ 核心写调用只在此处发生(minesweeper.settle_batch / 刷新编排入口 / runner 推进; # E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable # 以 encounter 参数入战斗,db.hero 运行期只读) # ④ 全屏 States 状态机(挖掘/结算/终局)与一次性提示去重。 # 2026-09-15 格面资源批:道具窗口与库存面板随可选类道具删除下线——E 区移除,窗口接线/选择序列删除。 # GP+商店房实施批(2026-09-21):商店覆盖层挂接与信号接线(装配器零信号连接纪律——句柄返回、 # 本类连接)+ runner.wallet_changed 转发试玩日志(wallet 行族)+ A 区顶栏金币余额。 # 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归 # ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归 # debug_session.gd——本类不拼呈现字符串。 # T3 拆件批(2026-10-02,规范 1.1 单一职责):结算行排播泵迁 row_pacer.gd(本类只入队/排空/读队列, # 探针/测试委托面 _row_queue/pace_delay_for/_flush_row_queue 原位保留);魔法道具两步交互迁 # aux_interact_controller.gd(本类只接线与转发意图,无目标使用显式化——哨兵坐标不出控制器); # GP 冒险钱包影子账迁 gp_shadow_ledger.gd(本类留探针同名读写委托面)。模态开合全量走 # _open_modal/_close_modal 单口,输入锁=modal_stack.has_open() 开态查询+三例外;battle 收尾反馈与 # 结算视觉 setter 收敛 hud_presenter 单通道(时序敏感演出触发留本类并注释);settlement 相位读取改经 # presentation_snapshot 成文面、变异表读取直读本根 db(settlement.db 即同一实例)——禁 # settlement.action_economy / settlement.db 链式穿透。 # ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于 # 驱动视图状态;信号连接只在组合根(row_pacer/aux_interact_controller 零连接,只收注入)。 extends Control const UiAssembler := preload("res://presentation/ui_assembler.gd") const Narrator := preload("res://presentation/battle_narrator.gd") const ModalStack := preload("res://presentation/modal_stack.gd") const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批) const AuxInteractController := preload("res://presentation/aux_interact_controller.gd") # 魔法道具两步交互(T3 拆件批) const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批) const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律) var db var settlement = null # 当前战斗编排器(关卡与调试模式统一读它) var runner = null # 关卡推进器(冒险模式每关重取 adventure.runner();单关 UI 直入口已退役——LevelRunner 仅存 headless 测试面) var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接) var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构) var meta = null # 元进度账本服务(元进度实现批——余额/定价/购买/工坊解锁/Boss两档入账/注入计算;零信号纯被动) var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) var _level_active := false var _seed_edit: LineEdit var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮) var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化) var _settlement_stop_pending := false # 卡⑥ 战斗胜利总结结算框挂起态(确认「下一节点」后进选路页) var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷) var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用) var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用) var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路) var _start_overlay: Control var _end_overlay: BattleEndOverlay var _inter_overlay: Control var _inter_label: Label var level_screen: LevelMapScreen # 关卡画面(独立屏——2026-09-28 形态升格批:激活期战斗屏宿主整体隐藏;零自动推进停点语义不变) var _battle_root: Control # 战斗屏宿主(B/C/D 三区容器——独立屏激活期 visible=false 级互斥) var _level_screen_active := false # 独立屏激活状态(覆盖兑付覆盖层在屏域内弹出期间——战斗屏保持隐藏) # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null # 视图(views/)——battle.md 三区(A 区关卡进度条顶栏随 停点前移批 2026-09-23 退役——节点链进度 # 由关卡画面本体选路覆盖层承载;E 区道具面板随可选类删除下线) var stage: BattleStageView # B 区(含 log_view 明细带) var log_view: BattleLogView var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区——2026-09-26 信息归属拍板精简) var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) var board_view: BoardView # 探针兼容别名(= board_zone.board_view) var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下) # T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui) var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _aux = null # 魔法道具两步交互(aux_interact_controller.gd——本类只接线与转发意图) var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂) # GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况) var _adventure_gp_view: int: get: return _gp_ledger.view_total() set(value): _gp_ledger.set_view_total(value) var _adventure_gp_final: int: get: return _gp_ledger.final_total() # 排播队列只读委托面(探针/测试读 main._row_queue——RowPacer 迁移后保留的同名可读面,共享同一 Array 引用) var _row_queue: Array: get: return _row_pacer.pending_rows() # 模态 overlay(z 序 = 登记序;pattern 6 层级纪律) var legend_overlay: LegendOverlay var review_overlay: SettlementReviewOverlay var node_overlay: NodeOverlay var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现) var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现) var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效) var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面) var tune_panel: TunePanel var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体) var _tune_sim = null # 调整板批量模拟控制器(tune_sim_controller.gd) var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent) var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零) var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批) var _locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放,玩家操作零丢失) # 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 #(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, # 排播泵本体已迁 row_pacer.gd——T3 结构段)。 func _replay_locked_click() -> void: if _locked_click.is_empty() or not _row_queue.is_empty(): return var click: Dictionary = _locked_click _locked_click = {} _on_board_action(click["cell"], click["kind"]) func _ready() -> void: db = get_node("/root/ConfigDB") if not db.is_ready: var ok: bool = db.load_tables("res://data/tables", false) if not ok: _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return # 存档读盘(存档序列化批):拒读/损坏均已在 SaveManager 内部兜底,失败不阻断启动 # (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。 save_manager = SaveManagerScript.new() save_manager.load_from_disk() # 系统级设置(迭代 8 卡④——独立存储不随存档):启动即恢复音量/全屏/减动效引擎态 settings_store = SettingsStore.new() settings_store.load_settings() settings_store.apply() # 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点 meta = MetaProgressionScript.new(db, save_manager) _debug = DebugSessionScript.new(db, get_node("/root/RngService"), Callable(self, "_debug_hero_override"), Callable(self, "_seed_value")) _debug.battle_created.connect(_on_debug_battle_created) _debug.battle_ready.connect(_on_debug_battle_ready) _build_ui() _refresh_resume_button() # 迭代 8 卡②——启动读盘后按断点在档态点亮「继续上局」 _start_overlay.visible = true # ==================== 视图接线(battle.md 四区布局 1280×800;节点构建归 UiAssembler) ==================== func _build_ui() -> void: var root := VBoxContainer.new() add_child(root) _battle_root = root # 独立屏互斥面(2026-09-28 形态升格批) var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db) stage = battle_ui.stage log_view = battle_ui.log_view info_panel = battle_ui.info_panel board_zone = battle_ui.board_zone board_view = battle_ui.board_view # 探针兼容别名(= board_zone.board_view) ap_bar = battle_ui.ap_bar # 探针兼容别名(= board_zone.ap_bar) settlement_fx = battle_ui.settlement_fx # 结算行排播泵(T3 拆件批——迁 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 = _replay_locked_click # T17 v3.1——演播完边沿重放锁期点击(合并批接线) add_child(_row_pacer) # 魔法道具两步交互(T3 拆件批——迁 aux_interact_controller.gd;settlement/runner 随关卡更替, # 经现值回调注入——仿 identity_resolver 只读回调先例) _aux = AuxInteractController.new(db, meta, info_panel, board_zone.toast, board_view, func() -> Variant: return settlement, func() -> Variant: return runner) log_view.review_requested.connect(_open_review_from_band) board_zone.action_requested.connect(_on_board_action) info_panel.aux_slot_clicked.connect(_aux.on_slot_clicked) # 迭代 7 T4——两步交互第一步 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) # 迭代 6 T6——flat_damage 未开格角标 logger = PlaytestLogger.new() logger.ap_per_round = int(db.action_economy.ap_per_round) # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 # < 商店 < 营地 < 调整板 < 设置 < 确认弹窗) var overlays: Dictionary = UiAssembler.build_overlays(self, db, int(db.hero.atk), int(db.hero.max_hp)) legend_overlay = overlays.legend_overlay review_overlay = overlays.review_overlay node_overlay = overlays.node_overlay _end_overlay = overlays.end_overlay shop_overlay = overlays.shop_overlay camp_overlay = overlays.camp_overlay tune_panel = overlays.tune_panel level_screen = LevelMapScreen.new() add_child(level_screen) # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——关卡画面须夹在节点层与商店层之间 #(兑付覆盖层在本屏域内弹出:模态栈打开低层时自动隐藏高层本屏、关闭自动恢复——零机制改动复用) move_child(level_screen, node_overlay.get_index() + 1) legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay)) review_overlay.closed.connect(_on_modal_closed.bind(review_overlay)) node_overlay.continue_pressed.connect(_on_node_continue) level_screen.route_chosen.connect(_on_route_chosen) level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119) shop_overlay.buy_requested.connect(_on_shop_buy) shop_overlay.leave_pressed.connect(_on_shop_leave) camp_overlay.buy_requested.connect(_on_camp_buy) camp_overlay.closed.connect(_on_camp_closed) _end_overlay.review_pressed.connect(_on_end_review) _end_overlay.continue_pressed.connect(_on_battle_end_continue) tune_panel.closed.connect(_close_tune_panel) tune_panel.apply_requested.connect(_on_tune_apply) tune_panel.simulate_requested.connect(_on_tune_simulate) settings_overlay = SettingsOverlay.new(settings_store) # 迭代 8 卡④——最小设置面(音量/全屏/减动效) add_child(settings_overlay) settings_overlay.closed.connect(_on_settings_closed) # 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进) bonus_overlay = BonusPickOverlay.new() add_child(bonus_overlay) bonus_overlay.pick_confirmed.connect(_on_bonus_pick_confirmed) bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen) bonus_overlay.continue_pressed.connect(_on_bonus_continue) move_child(bonus_overlay, _end_overlay.get_index()) var start: Dictionary = UiAssembler.build_start_overlay(self, db) _start_overlay = start.overlay _seed_edit = start.seed_edit _resume_btn = start.resumebtn start.lvbtn.pressed.connect(_start_adventure_from_menu) start.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口 start.tunebtn.pressed.connect(_show_tune_panel) start.dbgbtn.pressed.connect(_start_debug_levels) start.dbgbtn.visible = OS.get_environment("DS_DEBUG") == "1" # 迭代 8 卡④——调试入口默认隐藏门控(DS_DEBUG=1 点亮) start.campbtn.pressed.connect(_open_camp) start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 var inter: Dictionary = UiAssembler.build_inter_overlay(self) _inter_overlay = inter.overlay _inter_label = inter.label inter.next_btn.pressed.connect(_on_inter_next_pressed) var diag_btn := UiAssembler.build_diag_button(self) diag_btn.pressed.connect(_on_diagnose_pressed) # 确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化):树序落在开始层/过场层/诊断钮之后 = 子级最末 #(T3 收口批——「登记序 = 树序 = z 序」纪律复位,confirm_dialog.open 的 move_child 运行时自提 # 创可贴随之删除;弹窗只在开始菜单域弹出,绘制序恒最上) confirm_dialog = ConfirmDialog.new() add_child(confirm_dialog) confirm_dialog.confirmed.connect(_on_confirm_new_adventure) confirm_dialog.cancelled.connect(_on_confirm_cancelled) # 模态登记(缺陷⑤纪律——登记序 = 树序:含 move_child 调位后的关卡画面/三选一终位) _modals = ModalStack.new() for m: Control in [legend_overlay, review_overlay, node_overlay, level_screen, bonus_overlay, _end_overlay, shop_overlay, camp_overlay, tune_panel, settings_overlay, confirm_dialog]: _register_modal(m) _tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp)) func _fatal_screen(msg: String) -> void: var l := UiKit.label(msg, 20, true, UiKit.RED_ALERT) l.set_anchors_preset(Control.PRESET_CENTER) add_child(l) # ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ==================== func _register_modal(m: Control) -> void: # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _modals.register(m) func _open_modal(m: Control) -> void: # 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈 _flush_row_queue() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行) _modals.open(m) func _close_modal(m: Control) -> void: _modals.close(m) func _on_modal_closed(m: Control) -> void: _close_modal(m) func _input_locked() -> bool: # 模态/非战斗期棋盘输入锁(T3 收口批——开态查询归 modal_stack.has_open(),登记册单一来源, # 不再手工罗列各层;例外三件:开始层/过场层(非模态全屏态,不在登记册)与战斗缺位(棋盘输入无载体)) return _modals.has_open() or _start_overlay.visible or _inter_overlay.visible or settlement == null # ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)==================== func _start_adventure_from_menu() -> void: # 互斥确认弹窗化(二轮修改批 A——用户目检裁决「弹窗提示」):有断点时开新冒险弹实底确认框, # 「开新冒险」提交(弃旧断点+写盘)/「取消」回菜单 if save_manager != null and save_manager.has_breakpoint(): var bp: Dictionary = save_manager.breakpoint_snapshot() confirm_dialog.open("已有未完冒险(第 %d 关)——开新冒险将覆盖旧断点" % maxi(1, int(bp.get("level_index", 1))), "开新冒险", "取消") return _start_overlay.visible = false _start_adventure() func _on_confirm_new_adventure() -> void: confirm_dialog.close() if save_manager != null: save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) save_manager.write_to_disk() _start_overlay.visible = false _start_adventure() func _on_confirm_cancelled() -> void: confirm_dialog.close() # 回开始菜单(无状态变更) # 退出游戏(二轮修改批 D1——一级菜单入口;设置即时落盘后退出) func _quit_game() -> void: if settings_store != null: settings_store.save_settings() get_tree().quit() # 继续上局按钮可见性 = 断点在档态(迭代 8 卡②——启动读盘后与每次回开始菜单刷新) func _refresh_resume_button() -> void: if _resume_btn == null or save_manager == null: return var has: bool = save_manager.has_breakpoint() _resume_btn.visible = has if has: var bp: Dictionary = save_manager.breakpoint_snapshot() _resume_btn.text = "▶ 继续上局(第 %d 关 · 自最近停点恢复)" % maxi(1, int(bp.get("level_index", 1))) # 继续上局(迭代 8 卡②——卡① resume_adventure 的消费入口):断点重建至最近停点 func _resume_from_menu() -> void: if save_manager == null or not save_manager.has_breakpoint(): return var bp: Dictionary = save_manager.breakpoint_snapshot() _start_overlay.visible = false _wire_adventure() _level_active = true level_screen.set_abandon_available(true) _reset_run_scope() log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) if not adventure.resume_adventure(bp, {"hero_override": meta.hero_override() if meta != null else {}, "aux_unlocked": _aux.unlocked_set()}): _fatal_screen("续玩恢复失败——见输出日志") return # 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根,ADR-0001;断点注入同口) func _wire_adventure() -> void: adventure = AdventureRunner.new(db, get_node("/root/RngService")) _adventure_mode = true adventure.step_row.connect(_on_step_row) adventure.round_started.connect(_on_round_started) adventure.board_changed.connect(_on_board_changed) adventure.node_entered.connect(_on_runner_node) adventure.choices_presented.connect(_on_choices_presented) adventure.wallet_changed.connect(_on_wallet_changed) # 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线 adventure.bonus_choice_presented.connect(_on_bonus_presented) adventure.bonus_resolved.connect(_on_bonus_resolved) adventure.aux_replace_presented.connect(_on_aux_replace_presented) adventure.adventure_finished.connect(_on_adventure_finished) adventure.level_started.connect(_on_adventure_level_started) adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) # 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接; # 信号连接只在组合根——ADR-0001) func _start_adventure() -> void: _wire_adventure() _level_active = true level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板) _reset_run_scope() log_view.clear() var seed_val := _seed_value() logger.start_session(seed_val) adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) # 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签 var first_script: bool = not save_manager.is_first_adventure_done() if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux.unlocked_set()}): _fatal_screen("冒险启动失败——见输出日志") return # 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达; # 停点前移批 2026-09-23:本回调返回后才有 choices_presented 到达——入口选路层随即弹出) func _on_adventure_level_started(level_index: int, display_name: String) -> void: runner = adventure.runner() log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed), int(db.hero.atk), int(db.hero.max_hp), tune_panel.atk_pct() > 0 or tune_panel.hp_pct() > 0)) func _reset_run_scope() -> void: # 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号) _hints_shown = {} _refresh_used_round = 0 _boss_active = false func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 if _adventure_mode and adventure != null: runner = adventure.runner() var t := String(node.get("type", "")) if t == "battle" or t == "boss": settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) logger.attach_battle(settlement) _boss_active = t == "boss" _show_board_fx_for_node(t) log_view.append_bb(Narrator.node_banner(runner, node, t)) elif t == "reward": # 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面, # 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。 _bonus_context = "reward_node" var node_draw: Dictionary = adventure.node_bonus_candidates() var node_cands: Array = node_draw.get("candidates", []) as Array _open_modal(bonus_overlay) bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array)) adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array) 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": # 商店房兑付(GP+商店房实施批):到达即开店——商品快照/余额经 runner 只读投影喂入; # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进 _open_modal(shop_overlay) shop_overlay.open_shop(runner.shop_products(), int(runner.gp)) log_view.append_bb(Narrator.shop_line(int(runner.gp))) logger.log_node(node, info, runner.node_index) _refresh_presentations() # ---- 冒险模式终局面(三选一选择面 / 冒险结算屏——迭代 10 奖励重构批)---- # 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石 # 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约) func _on_bonus_presented(payload: Dictionary) -> void: if int(payload.get("aux_cleared", 0)) > 0: board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈 _pending_bonus = payload.duplicate(true) _bonus_context = "level_clear" if bool(payload.get("replace_pending", false)): # 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽) _show_bonus_pick() return _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续", board_view.get_global_rect(), "%s\n%s" % [_gp_summary_text({"gp_dropped": _last_gain_amount}), Narrator.bonus_keystone_line(payload)]) _open_modal(_end_overlay) _refresh_presentations() # 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态) func _show_bonus_pick() -> void: var payload: Dictionary = _pending_bonus _open_modal(bonus_overlay) bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array)) _refresh_presentations() var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷) var _last_replace_incoming := {} # 同上——incoming 记录 func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array: var out: Array = [] var aux_i := 0 for id in candidates: var cid := String(id) var text := "" if cid == "aux_item": var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else "" aux_i += 1 text = Narrator.bonus_card_text(cid, int(db.adventure.bonus_value(cid)), _aux_card_line(iid)) else: text = Narrator.bonus_card_text(cid, int(db.adventure.bonus_value(cid))) out.append({"id": cid, "text": text}) return out # 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案) func _aux_card_line(iid: String) -> String: if iid == "": return "" var row: Dictionary = db.aux_item_row(iid) if row.is_empty(): return "" var parts: Array[String] = [] for k in (row.get("effect_kinds", []) as Array): if String(k) == "mine_reveal": parts.append("真视") elif String(k) == "reward_preview": parts.append("预读") elif String(k) == "ap_grant": parts.append("AP +%d" % int(row.get("magnitude", 0))) var tgt := String(row.get("target", "none")) if tgt == "area_3x3": parts.append("3×3") elif tgt == "board_wide": parts.append("全图") return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)] func _on_bonus_pick_confirmed(item_id: String) -> void: if adventure == null: return var ok: bool = adventure.apply_node_bonus(item_id) if _bonus_context == "reward_node" else adventure.pick_bonus(item_id) if not ok: push_error("[组合根] 三选一兑付被拒——见输出日志") func _on_aux_replace_presented(items: Array, incoming: Dictionary) -> void: _last_replace_items = items.duplicate(true) _last_replace_incoming = incoming.duplicate(true) if not bonus_overlay.visible: _open_modal(bonus_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律) _open_bonus_replace(items, incoming) func _open_bonus_replace(items: Array, incoming: Dictionary) -> void: var named: Array = [] for it in items: var rec: Dictionary = it.duplicate() rec["display_name"] = String(_item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", "")))) named.append(rec) bonus_overlay.open_replace(named, String(_item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))), named.size(), int(db.adventure.transitional_sync_slots)) func _on_bonus_replace_chosen(discard: int) -> void: if adventure == null or not adventure.resolve_bonus_replace(discard): push_error("[组合根] 同调位替换受理被拒——见输出日志") # 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续) func _on_bonus_resolved(result: Dictionary) -> void: _pending_bonus = {} bonus_overlay.show_result(Narrator.bonus_result_text(result)) _refresh_presentations() # 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进) func _on_bonus_continue() -> void: _close_modal(bonus_overlay) bonus_overlay.close_pick() if _bonus_context == "reward_node": if runner != null: runner.continue_after_node() return if _adventure_mode and adventure != null: if not adventure.continue_to_next_level(): _fatal_screen("关卡推进失败——见输出日志") # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 func _on_adventure_finished(summary: Dictionary) -> void: _level_active = false var result: String = String(summary.get("result", "")) settlement = null _exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 _sync_settlement_visuals() # → 全关 HudPresenter.battle_end_feedback(stage, ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道(T3 收口批) # 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary[String, Variant] = {} if meta != null: 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, _item_name_lookup()) if result == "completed": _end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(), "本局 GP 入账 · 合计 %d" % _adventure_gp_final) # 迭代 6 T7 卡⑩——GP 行只上冒险终局面板(C5) else: _end_overlay.open_defeat(text, "返回", board_view.get_global_rect()) logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) if runner != null: runner.restore_hero_base() _open_modal(_end_overlay) _refresh_presentations() # 工坊 item_id → 显示名投影(keystone_settle_text 文案数据源——装配归本根,拼句归 Narrator) func _item_name_lookup() -> Dictionary: var lookup := {} if meta != null: for row in db.meta_workshop_items(): lookup[String(row.get("item_id", ""))] = String(row.get("display_name", "")) return lookup func _on_battle_end_continue() -> void: # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批) _close_modal(_end_overlay) _end_overlay.close() # 迭代 10——三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 if not _pending_bonus.is_empty(): _show_bonus_pick() return # 卡⑥ 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) if _settlement_stop_pending: _settlement_stop_pending = false _enter_level_screen() level_screen.open_choices(_pending_candidates, runner) _pending_candidates = [] _refresh_presentations() return _debug.reset() if runner != null: runner.restore_hero_base() runner = null settlement = null adventure = null _adventure_mode = false tune_panel.reset_sliders() _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新(收束后已清=隐藏) _refresh_presentations() func _on_end_review() -> void: review_overlay.open(log_view.parsed_text()) _open_modal(review_overlay) # 迭代 6 T7——走模态栈:终局层在册上方的可见遮挡被显式隐藏, # 修复「回看层被终局层全屏 STOP 遮罩压死」(直开绕栈缺陷) # ==================== 调试模式(连续两层——编排归 debug_session.gd; # 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用)==================== func _start_debug_levels() -> void: tune_panel.reset_sliders() _start_overlay.visible = false log_view.clear() log_view.append_bb(Narrator.debug_open_line(_seed_value(), 1, true)) _reset_run_scope() if not _debug.start_debug_levels(): _fatal_screen("战斗启动失败——见输出日志") func _on_debug_battle_created(settlement_ref, _level: int, _carry: int) -> void: # begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。settlement 必须先于 begin_battle # 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务) settlement = settlement_ref _wire_battle(settlement_ref) func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void: settlement = settlement_ref board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper) _check_board_hints() _refresh_presentations() func _on_inter_next_pressed() -> void: _inter_overlay.visible = false if _adventure_mode: # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) if adventure != null and not adventure.continue_to_next_level(): _fatal_screen("关卡推进失败——见输出日志") return if not _debug.next_layer(): _fatal_screen("战斗启动失败——见输出日志") # E4:加成生效值供给(debug_session 注入 Callable——每次从面板百分比现算,经 encounter 传参零表写; # 原表写路径 _apply_hero_override/_scaled_override 已废除,双写叠加 6→18→54 整类隐患随之移除; # 合成式单点在 UiKit.scaled——与 TunePanel 生效值预览共用,本类不再持有同式私有实现) func _debug_hero_override() -> Dictionary: return {"atk": UiKit.scaled(int(db.hero.atk), tune_panel.atk_pct()), "max_hp": UiKit.scaled(int(db.hero.max_hp), tune_panel.hp_pct())} # 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) func _identity_lookup(c: Vector2i): if settlement == null or settlement.minesweeper.current_board() == null: return null if String(settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: return null return settlement.query_reward_identity(c) # 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动。 # 变异表直读本根 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession # 构造传参),免 settlement 内部链式穿透,T3 收口批) func _variant_mask_lookup(c: Vector2i) -> bool: if settlement == null: return false var b = settlement.minesweeper.current_board() if b == null: return false for vid in b.variant_ids(c): var vrow: Dictionary = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) == "hide_number": return true return false # 地形变异 角标解析(迭代 6 T6——伤害型变异未开格视觉标记;读取方按格子变异数据行动,规则本体零特判。 # 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络。变异表直读本根 db——同上款收口) func _variant_mark_lookup(c: Vector2i) -> String: if settlement == null: return "" var b = settlement.minesweeper.current_board() if b == null: return "" for vid in b.variant_ids(c): var vrow: Dictionary = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) == "flat_damage": return "flat_damage" return "" func _show_board_fx_for_node(node_type: String = "") -> void: # Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6) if node_type == "boss": board_zone.fx.play_boss_shadow() func _on_battle_finished(payload: Dictionary) -> void: # 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派) var result: String = String(payload.get("result", "")) _sync_settlement_visuals() # → 全关(battle_over 反馈面) HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道(T3 收口批) var decision: Dictionary = _debug.on_battle_finished(payload) match String(decision.outcome): "next_layer": _inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [ int(decision.layer), int(decision.carry_hp), int(db.hero.max_hp)] _inter_overlay.visible = true _refresh_presentations() return "level_end": _end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", board_view.get_global_rect(), _gp_summary_text(payload)) "standalone_end": if result == "victory": _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload)) else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) _open_modal(_end_overlay) _refresh_presentations() # 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;账本语义零改动,纯呈现读数): # 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账 # 连接先于组合根 handler——此读数即战后入账完毕的总量)。 func _gp_summary_text(payload: Dictionary) -> String: return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)] # ==================== core↔view 接线(战斗信号统一走这里)==================== func _wire_battle(settlement_ref) -> void: settlement_ref.step_row.connect(_on_step_row) settlement_ref.battle_finished.connect(_on_battle_finished) settlement_ref.round_started.connect(_on_round_started) settlement_ref.board_changed.connect(_on_board_changed) # ==================== 结算行排播(泵本体归 row_pacer.gd——本节仅入口与探针/测试委托面)==================== func _on_step_row(row: Dictionary) -> void: # 卡⑦ 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放 if UiKit.motion_scale < 1.0: _row_pacer.consume_now(row) return _row_pacer.enqueue(row) # 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面) func pace_delay_for(prev_seg: String, next_seg: String) -> float: return _row_pacer.pace_delay_for(prev_seg, next_seg) # 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读) func _flush_row_queue() -> void: _row_pacer.flush() func _on_round_started(ri: int) -> void: # 卡⑦:轮首分隔行随排播队列走(落在前轮各行之后——带序正确);轮横幅即轮切换时刻发射 #(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用 # _consume_row 轮首分隔行分支,与原直写逐字等价)。 if UiKit.motion_scale < 1.0: _row_pacer.consume_now({"__round_line": ri}) else: _row_pacer.enqueue({"__round_line": ri}) settlement_fx.play_round_banner(ri) _refresh_used_round = 0 _refresh_presentations() func _on_board_changed() -> void: # 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步 if settlement == null and _level_active and runner != null: settlement = runner.current_battle() if settlement == null: return board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper) _check_board_hints() _refresh_presentations() # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- func _check_board_hints() -> void: var board = settlement.minesweeper.current_board() if board == null: return # 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态) if board.illegal_product: board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn") # 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知 elif board.degraded_product: board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn") func _on_hover_forbidden(_cell: Vector2i) -> void: if not _hints_shown.has("no_adjacent"): _show_hint("需先走近(点击已开格移动,免费)", "no_adjacent") func _on_chord_attempted(_cell: Vector2i) -> void: if not _hints_shown.has("chord"): _show_hint("本作无双击快开——插旗后轮末自动判定标中;点数字格可移动小人", "chord") func _show_hint(text: String, hint_id: String) -> void: _hints_shown[hint_id] = true logger.log_ux("ux_hint_shown", {"hint_id": hint_id}) board_zone.toast.show_toast(text, "info") # ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ---- func _on_node_continue() -> void: _close_modal(node_overlay) if runner != null: # 泛化语义(接口漂移清偿):确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 runner.continue_after_node() func _on_choices_presented(candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(候选按钮退役 2026-09-28——战斗胜利/兑付确认统一到达此处) if runner == null: return log_view.append_bb(Narrator.route_line(candidates, runner)) # 卡⑥ 战斗胜利总结结算(battle.md OQ14 拍板面接线——欠账清偿):战斗胜利停点先弹 BattleEndOverlay # 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点 # 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点 # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 if _adventure_mode and runner.current_battle() == null \ and String(runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): _settlement_stop_pending = true _pending_candidates = candidates.duplicate() _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点", board_view.get_global_rect(), "战利品 GP +%d · 合计 %d" % [_last_gain_amount, int(runner.gp)]) _open_modal(_end_overlay) _refresh_presentations() return _enter_level_screen() level_screen.open_choices(candidates, runner) _refresh_presentations() func _on_route_chosen(node_id: int) -> void: # 形态升格分支(2026-09-28):视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈 # 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈 # 打开低层时自动隐藏本屏、关闭自动恢复) if not level_screen.visible: _close_modal(level_screen) _exit_level_screen() if runner != null and not runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) # ---- 独立屏屏级互斥(形态升格批 2026-09-28——单一出口:进屏隐战斗屏 / 出屏显战斗屏)---- # 兑付覆盖层(节点/商店)在本屏激活域内弹出期间保持隐藏态不变——模态栈打开低层时隐藏本屏、 # 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。 func _enter_level_screen() -> void: _level_screen_active = true _battle_root.visible = false _open_modal(level_screen) func _exit_level_screen() -> void: _level_screen_active = false _battle_root.visible = true if level_screen.visible: _close_modal(level_screen) func _on_abandon_requested() -> void: # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——推翻 冒险结构.md「MVP 无界面入口」后置条款; # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 if not _adventure_mode or adventure == null: return if not adventure.abandon(): logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") # ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)---- # 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后, # 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账) func _open_camp() -> void: if meta == null: return meta.refresh_workshop_unlocks() _start_overlay.visible = false _open_modal(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈) camp_overlay.open_camp(_camp_data()) _refresh_presentations() func _on_camp_closed() -> void: _close_modal(camp_overlay) # 模态栈单口(T3 收口批) _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新 _refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) # ---- 最小设置面(迭代 8 卡④——入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)---- func _open_settings() -> void: _start_overlay.visible = false _open_modal(settings_overlay) # 模态栈单口(T3 收口批) func _on_settings_closed() -> void: _close_modal(settings_overlay) # 模态栈单口(T3 收口批) _start_overlay.visible = true _refresh_resume_button() # 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression) func _camp_data() -> Dictionary: var forge_rows: Array[Dictionary] = [] if meta != null: for attr in MetaTable.FORGE_ATTRS: var row: Dictionary = db.meta_forge_row(attr) forge_rows.append({ "attr": attr, "display_name": String(row.get("display_name", "")), "level": meta.forge_level(attr), "flat": int(row.get("flat_value", 0)), "price": meta.forge_price(attr), }) return { "balance": meta.keystone_balance() if meta != null else 0, "forge": forge_rows, "workshop": meta.workshop_status() if meta != null else [], "recent": save_manager.recent_summaries() if save_manager != null else [], } func _on_camp_buy(attr: String) -> void: if meta == null: return if not meta.buy_forge(attr): # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn") return camp_overlay.refresh_camp(_camp_data()) _refresh_presentations() # ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)---- func _on_shop_buy(product_id: String) -> void: if runner == null: return if not runner.buy_product(product_id): # 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1) logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"}) board_zone.toast.show_toast("金币不足——买不起这件商品", "warn") return shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) _refresh_presentations() func _on_shop_leave() -> void: _close_modal(shop_overlay) if runner != null: # 复用兑付确认纪律(接口漂移清偿):离开 ≠ 推进——确认后经 choices_presented 弹出选路口 runner.continue_after_node() func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 logger.log_wallet(event, gp, extra) _gp_ledger.apply_event(event, gp) # GP 影子账(迭代 6 T7——收束清零前取值,纯呈现零核心触碰=C1;账本归 gp_shadow_ledger.gd) if String(event) == "gain": _last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) if shop_overlay.visible and runner != null: shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) _refresh_presentations() # ==================== 玩家意图 → 核心写调用(唯一出口)==================== func _on_board_action(cell: Vector2i, kind: String) -> void: # 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口) if _input_locked(): return # 结算演播期棋盘冻结(T17 用户明确令 v3——不可点/不可移/不可挖): # 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。 # 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前 # _flush_row_queue 自行排空)。 # **点击不丢弃**(v3.1——用户实机「卡死」根因):锁期点击缓存最后一次,演播完边沿 # (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。 if not _row_queue.is_empty(): _locked_click = {"cell": cell, "kind": kind} return # 魔法道具选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格; # 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗) var aux_sel: int = _aux.active_selection() if aux_sel != 0 and kind == "dig": _aux.attempt_use(aux_sel, cell) return var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}]) var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted) logger.log_action(kind, cell, accepted) if runner != null and runner.aux_inventory != null: _aux.refresh_slots() # 迭代 7 T4——授予发生在 settle_batch 结算段内(同步走完,行尾重查) _handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {}) # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固) if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared(): board_zone.fx.play_clear_burst() if accepted and kind == "dig" and settlement != null and settlement.minesweeper != null: # 挖掘演出(棋盘小人贴图批 2026-09-25):挥铲三帧 + 角色弹跳——触发=格子点开那一刻 #(2026-09-25 用户裁决;settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态; # 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位) var hero_cell: Vector2i = settlement.minesweeper.hero_pos() board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale()) board_view.play_hero_dig_bounce() elif accepted and kind == "move": board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知 board_view.refresh() _refresh_presentations() func _on_stop_pressed() -> void: if _input_locked(): return # stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济) var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted)) if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided": # voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗) board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn") logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"}) board_view.refresh() _refresh_presentations() func _on_refresh_confirmed() -> void: # 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板旗作废,每轮上限读表) if _input_locked(): return var ok: bool = false if _level_active and runner != null: ok = runner.player_request_refresh() elif settlement != null: ok = settlement.player_request_refresh() logger.log_action("refresh", Vector2i(-1, -1), ok) if ok: _refresh_used_round += 1 logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round}) board_zone.toast.show_toast("RAP−%d · 旧板旗作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold") else: # 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断) var rap: int = int(settlement.presentation_snapshot().get("rap", 0)) if settlement != null else 0 var cost: int = int(db.action_economy.rap_refresh_cost) var result := "rejected_rap" if rap < cost else "rejected_cap" logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1}) if result == "rejected_rap": board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn") ap_bar.pulse_rap() else: board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn") board_view.refresh() _refresh_presentations() func _handle_outcome_feedback(outcome: Dictionary) -> void: # 双源拒绝反馈(pattern 1):每次拒绝玩家可感知 + 来源可区分 + 返回值命令式驱动(无拒绝事件) if outcome.is_empty() or bool(outcome.get("accepted", false)): return logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")), "sub_reason": String(outcome.get("sub_reason", ""))}) board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn") if String(outcome.get("reject_reason", "")) == "resource_face": ap_bar.pulse_ap() # ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)==================== func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round) HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining) _sync_settlement_visuals() func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() if settling and not _was_settling: settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带(演出链批)——时序敏感演出触发,例外留在组合根 _was_settling = settling HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, _phase_text()) func _is_settling() -> bool: # T17 修复:结算视觉期跟随排播演播到放完。核心层同步走完(轮末结算后逻辑秒回 Active), # 旧判定让演播期棋盘提前恢复可挖观感=用户「没意识到在结算演出」感知缺口的根源; # 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹 # 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外: # overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。 # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透(T3 收口批)。 if settlement == null or not settlement.is_battle_live(): return not _row_queue.is_empty() if String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: return true return not _row_queue.is_empty() func _phase_text() -> String: # 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫;相位取数经 # presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) 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)) # ==================== B 区明细带 / 图例(模态入口)==================== func _open_review_from_band() -> void: review_overlay.open(log_view.parsed_text()) _open_modal(review_overlay) # 迭代 6 T7 同款——净空态下栈上无可隐藏层,行为与旧直开逐位等价 func _on_legend_toggled() -> void: if legend_overlay.is_open(): _close_modal(legend_overlay) else: _open_modal(legend_overlay) # ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)==================== func _on_diagnose_pressed() -> void: var ctx := {"mode": "level" if _level_active else "debug", "seed": _seed_value()} if _level_active and runner != null: ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result), "hero_hp": runner.hero_hp, "settlement_is_null": runner.current_battle() == null} if settlement != null: ctx["settlement"] = settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12) var path: String = logger.dump_diagnosis(ctx) board_zone.toast.show_toast("已转储诊断: " + path, "info") # ==================== 数值调整板(加成应用/批量模拟编排——组合根承接,视图纯 UI)==================== func _show_tune_panel() -> void: _start_overlay.visible = false _open_modal(tune_panel) # 模态栈单口(T3 收口批) func _close_tune_panel() -> void: _close_modal(tune_panel) # 模态栈单口(T3 收口批) _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新 func _on_tune_apply(atk_pct: int, hp_pct: int) -> void: # E4:加成无持久态可"落"——各战斗开局经 _debug_hero_override/runner 现算生效(面板仅关闭回开始层) _close_tune_panel() func _on_tune_simulate(n: int) -> void: # 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数 _tune_sim.run(n) # ==================== 工具 ==================== func _seed_value() -> int: if _seed_edit.text.is_valid_int(): return int(_seed_edit.text) return 42