## VERTICAL SLICE - NOT FOR PRODUCTION ## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。 ## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。 ## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 _open_modal/_close_modal。 ## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。 extends Control # autoload 节点路径单点(降格实现:审计处方「编译期全局名」与本仓 --script # 模式编译器不解析裸 autoload 标识符冲突〔run_all.gd 头注+pacing 实证〕——字符串拼写风险 # 收敛到四常量,类型面由读视图属性承载) const AUTOLOAD_PATH_CFG := "/root/ConfigDB" const AUTOLOAD_PATH_RNG := "/root/RngService" const AUTOLOAD_PATH_SAVE := "/root/SaveManager" const AUTOLOAD_PATH_META := "/root/MetaProgression" 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") # 结算行排播泵 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 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") # ==================== 视觉层合同(唯一事实源——树序=登记序=z 序;改序只改此表) ==================== # kind: shell=屏壳容器(成员随壳不占独立树位;level_screen 例外另占登记位)/modal=模态层(入登记册, # tags 随登记)/state=全屏态(占树位不入登记册)/node=非视觉节点(占树位不入登记册)。 # host=宿住屏壳(树序随宿主、登记序按本合同);shell=模态成员的屏壳(成员与屏壳同槽:屏壳占该树位、 # 成员占该登记位)。落位=_apply_layer_contract(全仓唯一层序落位点,add_child 之后统一排序+登记)。 const LAYER_CONTRACT: Array[Dictionary] = [ {"key": "adventure_screen", "kind": "shell"}, {"key": "menu_screen", "kind": "shell"}, {"key": "row_pacer", "kind": "node"}, {"key": "legend_overlay", "kind": "modal", "tags": []}, {"key": "node_overlay", "kind": "modal", "tags": []}, {"key": "level_screen", "kind": "modal", "tags": [ModalStack.TAG_SCREEN_STATE], "host": "adventure_screen"}, {"key": "curio_overlay", "kind": "modal", "tags": []}, {"key": "end_overlay", "kind": "modal", "tags": []}, {"key": "shop_overlay", "kind": "modal", "tags": []}, {"key": "camp_overlay", "kind": "modal", "tags": [], "shell": "camp_screen"}, {"key": "settings_overlay", "kind": "modal", "tags": []}, {"key": "status_panel", "kind": "modal", "tags": []}, {"key": "career_overlay", "kind": "modal", "tags": []}, {"key": "save_select_overlay", "kind": "modal", "tags": []}, # 选档界面(营地交互重做卡③——登记生涯同档) {"key": "inter_overlay", "kind": "state", "tags": []}, {"key": "confirm_dialog", "kind": "modal", "tags": []}, {"key": "debug_overlay", "kind": "modal", "tags": [ModalStack.TAG_NON_LOCKING]}, {"key": "debug_badge", "kind": "float"}, # 调试入口全局浮层——末位=最顶绘制序; # float 类不注册 ModalStack(open() 遮挡记账只隐藏登记层)→ 任何 UI 打开都不遮它 ] var db var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null) # SaveManager/MetaProgression 升格 autoload——成员名保留为「autoload 读视图」 #(属性 getter 经节点路径解析:--script 模式编译器不解析裸 autoload 标识符〔run_all.gd 头注在案〕, # 节点路径双模式通用;组合根调用点/探针挂载面 main.save_manager 语义零迁移)。 var save_manager: SaveManagerCore: get: return get_node(AUTOLOAD_PATH_SAVE) var meta: MetaProgressionCore: get: return get_node(AUTOLOAD_PATH_META) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) var _debug_entry := false # 调试会话门控(写死=true——恒开:启动即开浮窗+两屏角标点亮;_ready 赋值) var _seed_edit: LineEdit var confirm_dialog: ConfirmDialog # 通用确认弹窗(消费方编排——现行消费方=选档删档确认;新冒险互斥确认已随卡②主菜单撤出,归卡④大门) var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清) var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用) var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(level-clear.md v2 选择面承载;奖励节点共用) var _start_overlay: Control var _end_overlay: BattleEndOverlay var _inter_overlay: Control var _inter_label: Label var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen) var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root) # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null # 视图(views/)——battle.md 三区(节点链进度 # 由关卡画面本体选路覆盖层承载) var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen var log_view: BattleLogView var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区) 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 之上、模态之下) # 三屏壳实例(屏成员经本类句柄暴露;探针挂载假设 main.X 一级路径) var _menu_screen = null # MenuScreen(开始菜单屏壳) var _camp_screen = null # CampScreen(营地屏壳) var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器) # 拆件后的组合根持有面(职责见各类头注;装配见 _build_ui) var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图) var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入) var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发) # 排播队列只读委托面(探针/测试读 main._row_queue——与 RowPacer 队列共享同一 Array 引用) var _row_queue: Array[Variant]: get: return _row_pacer.pending_rows() # 模态 overlay(z 序 = 登记序;pattern 6 层级纪律) var legend_overlay: LegendOverlay var node_overlay: NodeOverlay var shop_overlay: ShopOverlay # 商店覆盖层(商店房节点兑付呈现) var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay) var settings_store # 系统级设置独立存储(音量/全屏/减动效) var settings_overlay: SettingsOverlay # 设置覆盖层(最小设置面) var status_panel: StatusPanel # 局内状态面板·只读(钥石余额+营地三柜台进度查看;登记设置层同档) var career_overlay: CareerOverlay # 生涯页·只读全屏页(跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕) var save_select_overlay: SaveSelectOverlay # 选档界面·3 存档位(新档/进档/删档——营地交互重做卡③;登记生涯同档) var debug_overlay: DebugOverlay # 调试浮窗(种子/数值调整/批量模拟/两层调试入口收一) var debug_badge: Button # 调试浮层入口角标(UI 最顶层浮层——LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡) var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——依赖仅 ModalStack+查询 Callable) var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留) var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd) # 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住 # run_scope.settlement_stop_timer) var _settlement_stop_pending: bool: get: return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false set(v): if run_scope == null: return if v: run_scope.settlement_stop_timer.arm() else: run_scope.settlement_stop_timer.disarm() # 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 #(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, # 排播泵本体在 row_pacer.gd)。 var _deferred_modal: Control: # 演播后开窗挂起只读视图(t17_defer_probe 挂起态断言缝;本体归 _modal_flow) get: return _modal_flow._deferred if _modal_flow != null else null # 演播后排窗(伤害过程演完才切结算窗口):排播队列在播=挂起,演播完边沿 # (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出 # 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。 func _open_modal_after_playback(m: Control) -> void: _modal_flow.open_after_playback(m) # 演播后排窗挂起编排归 ModalFlow # 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放): func _on_pump_ended() -> void: _modal_flow.pump_ended() # 派发编排归 ModalFlow(重放经注入 Callable 回本类) func _replay_locked_click() -> void: if run_scope.locked_click.is_empty() or not _row_queue.is_empty(): return var click: Dictionary[Variant, Variant] = run_scope.locked_click run_scope.locked_click = {} _on_board_action(click["cell"], click["kind"]) # 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责), # 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。 func _begin_session(adventure_session: bool) -> void: if run_scope != null: run_scope.queue_free() run_scope = RunScope.new() run_scope.is_adventure = adventure_session add_child(run_scope) func _ready() -> void: db = get_node(AUTOLOAD_PATH_CFG) if not db.is_ready: var ok: bool = db.load_tables("res://data/tables", false) if not ok: _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return # SaveManager/MetaProgression 升格 autoload:读盘已在各自 _ready 完成 #(autoload 先于主场景就绪——读盘时序等价),此处零动作。 # 系统级设置(独立存储不随存档):启动即恢复音量/全屏/减动效引擎态 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 _debug_entry = true # 调试入口门控写死=true(不读 DS_DEBUG 环境变量;发布前清) debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui) # boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住); # 调试入口=UI 最顶层全局浮层角标(LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡) # ==================== 视图接线(三屏壳 + 模态层装配;构建归屏壳/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) 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) # ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ==================== func _register_modal(m: Control, tags: Array = []) -> void: # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _modal_flow.register(m, tags) # 排除组随层登记(non_locking=调试浮窗/screen_state=关卡地图屏) # 合同层寻址(key→节点;纯查表不含层序语义——层序唯一事实源=LAYER_CONTRACT) func _layer_node(key: String) -> Node: match key: "adventure_screen": return _adventure_screen "menu_screen": return _menu_screen "row_pacer": return _row_pacer "legend_overlay": return legend_overlay "node_overlay": return node_overlay "level_screen": return level_screen "curio_overlay": return curio_overlay "end_overlay": return _end_overlay "shop_overlay": return shop_overlay "camp_screen": return _camp_screen "camp_overlay": return camp_overlay "settings_overlay": return settings_overlay "status_panel": return status_panel "career_overlay": return career_overlay "save_select_overlay": return save_select_overlay "inter_overlay": return _inter_overlay "confirm_dialog": return confirm_dialog "debug_overlay": return debug_overlay "debug_badge": return debug_badge return null # 合同装配(_build_ui 尾调用):按 LAYER_CONTRACT 序统一落位——直接子层 move_child 到合同槽、 # 模态层按合同序登记;宿住层(host)树序随宿主只登记;成员层(shell)与屏壳同槽(屏壳占树位、成员占登记位)。 # 本函数是全仓唯一层序落位点(move_child 单口)——改层序只改 LAYER_CONTRACT。 func _apply_layer_contract() -> void: var slot := 0 for entry: Dictionary in LAYER_CONTRACT: var tags: Array[Variant] = entry.get("tags", []) if entry.has("shell"): var shell: Node = _layer_node(String(entry["shell"])) var member: Node = _layer_node(String(entry["key"])) if shell != null: move_child(shell, slot) slot += 1 if member != null: _register_modal(member as Control, tags) else: push_error("[合同装配] 层缺失: %s" % String(entry["key"])) continue var node: Node = _layer_node(String(entry["key"])) if node == null: push_error("[合同装配] 层缺失: %s" % String(entry["key"])) continue if entry.has("host"): _register_modal(node as Control, tags) continue move_child(node, slot) slot += 1 if String(entry["kind"]) == "modal": _register_modal(node as Control, tags) func _open_modal(m: Control) -> void: _modal_flow.open(m) # 挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow func _close_modal(m: Control) -> void: _modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上) func _on_modal_closed(m: Control) -> void: _close_modal(m) func _input_locked() -> bool: # 模态/非战斗期棋盘输入锁(判据编排归 ModalFlow.locks_input:互斥域随层登记 # [non_locking=调试浮窗]+ 三例外经查询 Callable 注入;本函数留探针缝) return _modal_flow.locks_input() # ==================== 冒险模式入口(唯一试玩入口)==================== func _start_adventure_from_menu() -> void: # 主菜单「开始冒险」钮已随三入口化撤出(卡② mn6——主菜单侧断点互斥确认链同步摘除, # 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针 # 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝 # 由卡① sv3 承接(结算不落盘)。 _start_overlay.visible = false _start_adventure() # 确认弹窗分派(卡③ 契约 3):按 _confirm_consumer 路由——"delete_slot"=卡③ 删档确认 #(确认才删、取消只关窗——裁决 3)。"new_adventure" 消费方已随卡② 主菜单确认链摘除退役 #(mn6/V5——主菜单零确认残留),分派机制保留(卡④ 到位接入第三消费方不返工)。 func _on_confirm_accepted() -> void: confirm_dialog.close() match _confirm_consumer: "delete_slot": save_manager.delete_slot(_confirm_consumer_slot) save_select_overlay.open_save_select(_save_select_data()) # 就地刷新(该槽回空态) _confirm_consumer = "" func _on_confirm_declined() -> void: confirm_dialog.close() # 只关窗(取消无状态变更——裁决 3:取消不删) _confirm_consumer = "" # 退出游戏(一级菜单入口;设置即时落盘后退出) func _quit_game() -> void: if settings_store != null: settings_store.save_settings() get_tree().quit() # 继续上局(resume_adventure 的消费入口):断点重建至最近停点 func _resume_from_menu() -> void: if not save_manager.has_breakpoint(): return var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() _close_modal(debug_overlay) # 开冒险路径显式关浮窗(续玩同口径) _start_overlay.visible = false _begin_session(true) _wire_adventure() run_scope.level_active = true _sync_status_entry() # 续玩恢复点亮状态面板入口角标 level_screen.set_abandon_available(true) log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径) if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, {"treasure_unlocked": _treasure_ui.unlocked_set()})): _fatal_screen("续玩恢复失败——见输出日志") 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) # 断点承载注入(停点快照写盘;调试/模拟不经过此路径) # 冒险开局参数面构造(主菜单开新/续玩两入口共用:同调三键随开局下传。 # 取值口径与营地投影完全同源:装配集=save.attunement_equipped(save_manager.gd 访问器)、Y 刷新池= # meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects); # hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。 # 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照; # 取值=meta.hero_attack_type/hero_armor_type=存档选中路线类型轴,未解锁/异常选中态回退锻造配置默认值)。 # 注:runner 侧 resume_adventure 受理面收 attunement/attune_refresh_y/tree_rules 三键 # (传参/受理两侧对齐)。 static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary: var params: Dictionary[Variant, Variant] = { "hero_override": meta_ref.hero_override() if meta_ref != null else {}, "attack_type": meta_ref.hero_attack_type() if meta_ref != null else "", "armor_type": meta_ref.hero_armor_type() if meta_ref != null else "", "attunement": save_manager_ref.attunement_equipped() if save_manager_ref != null else [], "attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0, "tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [], } params.merge(extra, true) return params # 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接; # 信号连接只在组合根) func _start_adventure() -> void: _close_modal(debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) _begin_session(true) _wire_adventure() run_scope.level_active = true _sync_status_entry() # 进对局点亮状态面板入口角标 level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期) log_view.clear() var seed_val := _seed_value() logger.start_session(seed_val) run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42) if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, {"treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})): _fatal_screen("冒险启动失败——见输出日志") 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_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() # ---- 冒险模式终局面(三选一选择面 / 冒险结算屏)---- # 冒险结算(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() func _start_debug_levels() -> void: # 守卫:调试对局与活跃冒险/结算并存不开放——并发会清写 # settlement/编排态=状态机破坏;对局中开窗是只读浮窗,不是开局前置 if run_scope.is_adventure or run_scope.settlement != null: 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("战斗启动失败——见输出日志") 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_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("战斗启动失败——见输出日志") # 加成生效值供给(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: 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 "" 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 单通道 var decision: Dictionary[Variant, Variant] = _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), "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), 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(), int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) _open_modal_after_playback(_end_overlay) _refresh_presentations() # 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;纯呈现读数): # 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账 # 连接先于组合根 handler——此读数即战后入账完毕的总量)。 # runner 缺位守卫(续玩停点恢复分支:adventure_runner # resume_adventure 的 bonus_pending 分支在 level_started 之前同步 curio_choice_presented.emit # 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照 # 此刻必在档,语义=续玩恢复面的合计即存档合计;与 _bonus_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) # ==================== core↔view 接线(战斗信号统一走这里)==================== func _wire_battle(settlement_ref: BattleOrchestrator) -> 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: # 清图奖励可感知反馈(挖完全图须有奖励感——clear_blast 兑付对敌伤害 # 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带) if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective": board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info") # 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放 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) # 秘宝呈现同步:每战首轮装配链 # 显式重算〔🔴 契约复原语义:下一战装配链重算即含「未装配→写 0」复原〕。 # 挂点时序依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。 # settlement 空防御照既有形态。未装配=参数 0→写 0=缺省零扰动基线逐位不变。) if ri == 1: _sync_treasure_glow() # 巢穴预告呈现(装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示) if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0: board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info") # 开战横幅——整图地形变异预告:首轮快照 board_variants # 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。 if ri == 1 and run_scope.settlement != null: var bvs: Array[Variant] = run_scope.settlement.presentation_snapshot().get("board_variants", []) if not bvs.is_empty(): var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs) if not vlines.is_empty(): settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines)) _refresh_presentations() func _on_board_changed() -> void: # 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步 if run_scope.settlement == null and run_scope.level_active and run_scope.runner != null: run_scope.settlement = run_scope.runner.current_battle() if run_scope.settlement == null: return board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) _check_board_hints() _refresh_presentations() # 秘宝视野呈现桥:阔视野带 light_radius_bonus # 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter # 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2: # 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=缺省基线。 func _sync_treasure_glow() -> void: if run_scope.settlement == null or board_view == null: return board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus")) # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- func _check_board_hints() -> void: var board = run_scope.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 run_scope.hints_shown.has("no_adjacent"): _show_hint("需先走近(点击已开格移动,免费)", "no_adjacent") func _on_chord_attempted(_cell: Vector2i) -> void: if not run_scope.hints_shown.has("chord"): _show_hint("本作无双击快开——封印后轮末自动判定标中;点数字格可移动小人", "chord") func _show_hint(text: String, hint_id: String) -> void: run_scope.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 run_scope.runner != null: # 确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 run_scope.runner.continue_after_node() 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 _enter_level_screen() -> void: run_scope.level_screen_active = true _battle_root.visible = false _open_modal(level_screen) _sync_status_entry() # 屏级互斥切换同步(_open_modal 单口不识屏级状态) 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() # 同上 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") # ---- 营地覆盖层与钥石账本(写调用唯一归组合根)---- # ---- 最小设置面(入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)---- func _open_settings() -> void: _start_overlay.visible = false _open_modal(settings_overlay) # 模态栈单口 func _on_settings_closed() -> void: _close_modal(settings_overlay) # 模态栈单口 _start_overlay.visible = true # ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)---- # 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装—— # 快照仅服务单次开层生命周期(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()), } # 秘宝行映射(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()) func _on_status_closed() -> void: _close_modal(status_panel) # 模态栈单口 # ---- 生涯页·只读(入口=开始层生涯钮;开合仿设置面先例:开始层互斥; # 数据快照随开随建零写通路,页内零跳转)---- func _open_career() -> void: _start_overlay.visible = false _open_modal(career_overlay) # 模态栈单口 career_overlay.open_career(_career_data()) func _on_career_closed() -> void: _close_modal(career_overlay) # 模态栈单口 _start_overlay.visible = true # 生涯页快照投影(只读同源——每次开层实时读 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()) func _on_save_select_closed() -> void: _close_modal(save_select_overlay) # 模态栈单口 _start_overlay.visible = true # 槽数据快照(只读同源——每次开层实时经卡① 窥视 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} # 空槽新档链(Q3 裁乙暂行落点=现役 CampOverlay;卡④ 重立卡后改挂场景图):卡① 创建=激活=白板落盘, # 落点开层。动线序(禁先关后开):先落点开层(开始层保持隐藏),选档层被模态栈遮挡隐藏、不关闭。 func _on_save_select_new(slot: int) -> void: save_manager.create_slot(slot) _camp_pres.open_camp() # 有档槽进档链(契约 8 进档即激活):卡① 激活+读盘(兜底/拒读链按槽成立)→暂行落点 CampOverlay。 func _on_save_select_load(slot: int) -> void: save_manager.activate_slot(slot) _camp_pres.open_camp() # 删档链(契约 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() # 入口角标可见性/置灰同步(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 # ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)---- func _on_camp_closed() -> void: _camp_pres.on_camp_closed() func _camp_data() -> Dictionary: return _camp_pres.camp_data() 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_board_action(cell: Vector2i, kind: String) -> void: # 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口) if _input_locked(): return # 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖): # 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。 # 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前 # _flush_row_queue 自行排空)。 # **点击不丢弃**(v3.1——「卡死」体感根因):锁期点击缓存最后一次,演播完边沿 # (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。 if not _row_queue.is_empty(): run_scope.locked_click = {"cell": cell, "kind": kind} return # 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格; # 只劫持挖掘点击(move/seal 语义不冲突——选中期玩家仍可移动/封印) var treasure_sel: int = _treasure_ui.active_selection() if treasure_sel != 0 and kind == "dig": _treasure_ui.attempt_use(treasure_sel, cell) return var outcomes: Array[Variant] = run_scope.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 run_scope.runner != null and run_scope.runner.treasure_inventory != null: _treasure_ui.refresh_slots() # 授予发生在 settle_batch 结算段内(同步走完,行尾重查) _handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {}) # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(重入隐患守卫) if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared(): board_zone.fx.play_clear_burst() if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null: # 挖掘演出:挥铲三帧 + 角色弹跳——触发=格子点开那一刻 #(settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态; # 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位) var hero_cell: Vector2i = run_scope.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[Variant] = run_scope.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 run_scope.level_active and run_scope.runner != null: ok = run_scope.runner.player_request_refresh() elif run_scope.settlement != null: ok = run_scope.settlement.player_request_refresh() logger.log_action("refresh", Vector2i(-1, -1), ok) if ok: logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()}) board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold") else: # 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断) var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.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_snapshot() + 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 状态机(挖掘/结算/窗口/终局)==================== # 本轮已受理刷新次数只读面(刷新双账收口: # 单一事实源=战斗编排器 _refresh_used_this_round,经 presentation_snapshot 成文面读取) func _refresh_used_snapshot() -> int: if run_scope == null or run_scope.settlement == null: return 0 return int(run_scope.settlement.presentation_snapshot().get("refresh_used", 0)) func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_snapshot()) HudPresenter.refresh(stage, info_panel, ap_bar, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining) _treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗) _sync_settlement_visuals() func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() 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()) func _is_settling() -> bool: # 结算视觉期跟随排播演播到放完。核心层同步走完(轮末结算后逻辑秒回 Active), # 判据若只跟逻辑态,演播期棋盘会提前恢复可挖观感=「没意识到在结算演出」感知缺口根源; # 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹 # 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外: # overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。 # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透)。 if run_scope.settlement == null or not run_scope.settlement.is_battle_live(): return not _row_queue.is_empty() if String(run_scope.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 链式穿透) 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)) # ==================== B 区明细带 / 图例(模态入口)==================== func _on_legend_toggled() -> void: if legend_overlay.is_open(): _close_modal(legend_overlay) else: _open_modal(legend_overlay) # ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)==================== 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") # ==================== 调试浮窗(开合/应用/模拟编排由组合根承接)==================== func _show_debug_overlay() -> void: # 门禁:对局中可开;本函数仍是唯一开窗入口,门禁逻辑收函数内: # 棋盘输入锁/状态角标判据排除浮窗, # 遮挡语义=ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制 if debug_overlay == null: return _open_modal(debug_overlay) _sync_debug_overlay() # 调试浮窗角标点击(两屏同款处理器——与状态角标同形制守卫;仍收口 _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() # 浮窗上下文同步(当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合 # 单口(凡影响开态/对局态的路径必经其一——与 _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()) # 当局数据只读区块喂入(开窗+模态开合信号驱动) # 当局数据六项只读投影(零直读内部状态、零写通路):种子=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)} # 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值; # 调试两层=RNG 服务 master_seed。禁 _seed_value() 重算冒充本局种子。 func _current_master_seed() -> int: if run_scope.adventure != null: return int(run_scope.adventure.master_seed) return int(get_node(AUTOLOAD_PATH_RNG).master_seed) func _close_debug_overlay() -> void: _close_modal(debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复 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: return _tune_sim.run(n) # ==================== 工具 ==================== func _seed_value() -> int: # 种子三分判定单点(全工程唯一种子框读取处,禁第二处读框): # ①显式合法整数全场景优先(含首局——测试/调试接口显式指定;第五路 DebugSession Callable # 经本函数=调试对局与玩家同口径); # ②首局固定 42(存档现役布尔 is_first_adventure_done:新档显式 false=教学局恒 42〔教学 # 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机); # ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service # 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费) if _seed_edit.text.is_valid_int(): return int(_seed_edit.text) if save_manager.is_first_adventure_done(): return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免) return 42