diff --git a/game/presentation/camp_flow_context.gd b/game/presentation/camp_flow_context.gd new file mode 100644 index 00000000..7d0b6d41 --- /dev/null +++ b/game/presentation/camp_flow_context.gd @@ -0,0 +1,38 @@ +class_name CampFlowContext +extends RefCounted +# 营地/商店投影的构造上下文(迭代 27 T4——审计 P1-5:控制器构造参数收拢为 context 对象, +# 原 13 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 +# 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 + +var db +var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) +var save_get: Callable # () -> SaveManagerCore +var camp_overlay: CampOverlay +var shop_overlay: ShopOverlay +var toast: ToastStack +var start_overlay: Control +var logger # PlaytestLogger(试玩日志验收取数面) +var runner_get: Callable # () -> LevelRunner +var open_modal: Callable +var close_modal: Callable +var refresh_presentations: Callable +var refresh_resume_button: Callable + + +func _init(db_ref, meta_get_cb: Callable, save_get_cb: Callable, camp_overlay_ref: CampOverlay, + shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control, + logger_ref, runner_get_cb: Callable, open_modal_cb: Callable, close_modal_cb: Callable, + refresh_presentations_cb: Callable, refresh_resume_button_cb: Callable) -> void: + db = db_ref + meta_get = meta_get_cb + save_get = save_get_cb + camp_overlay = camp_overlay_ref + shop_overlay = shop_overlay_ref + toast = toast_ref + start_overlay = start_overlay_ref + logger = logger_ref + runner_get = runner_get_cb + open_modal = open_modal_cb + close_modal = close_modal_cb + refresh_presentations = refresh_presentations_cb + refresh_resume_button = refresh_resume_button_cb diff --git a/game/presentation/camp_flow_context.gd.uid b/game/presentation/camp_flow_context.gd.uid new file mode 100644 index 00000000..2ab42493 --- /dev/null +++ b/game/presentation/camp_flow_context.gd.uid @@ -0,0 +1 @@ +uid://dxj1wy8n1yp6k diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index 945a6dbf..e9548e4f 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -22,23 +22,21 @@ var _refresh_presentations: Callable var _refresh_resume_button: Callable -func _init(db_ref, meta_get: Callable, save_get: Callable, camp_overlay_ref: CampOverlay, - shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control, - logger_ref, runner_get: Callable, open_modal: Callable, close_modal: Callable, - refresh_presentations: Callable, refresh_resume_button: Callable) -> void: - db = db_ref - _meta_get = meta_get - _save_get = save_get - camp_overlay = camp_overlay_ref - shop_overlay = shop_overlay_ref - toast = toast_ref - start_overlay = start_overlay_ref - logger = logger_ref - _runner_get = runner_get - _open_modal = open_modal - _close_modal = close_modal - _refresh_presentations = refresh_presentations - _refresh_resume_button = refresh_resume_button +func _init(ctx: CampFlowContext) -> void: + # 迭代 27 T4——构造参数收拢 context 对象(P1-5:13 参长签名收一处命名面;字段逐一同名迁移) + db = ctx.db + _meta_get = ctx.meta_get + _save_get = ctx.save_get + camp_overlay = ctx.camp_overlay + shop_overlay = ctx.shop_overlay + toast = ctx.toast + start_overlay = ctx.start_overlay + logger = ctx.logger + _runner_get = ctx.runner_get + _open_modal = ctx.open_modal + _close_modal = ctx.close_modal + _refresh_presentations = ctx.refresh_presentations + _refresh_resume_button = ctx.refresh_resume_button # 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, diff --git a/game/presentation/curio_flow_context.gd b/game/presentation/curio_flow_context.gd new file mode 100644 index 00000000..8a0dfee0 --- /dev/null +++ b/game/presentation/curio_flow_context.gd @@ -0,0 +1,45 @@ +class_name CurioFlowContext +extends RefCounted +# 三选一/替换子交互的构造上下文(迭代 27 T4——审计 P1-5:控制器构造参数收拢为 context 对象, +# 原 16 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 +# 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 + +var db +var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) +var curio_overlay: CurioPickOverlay +var end_overlay: BattleEndOverlay +var board_view: BoardView +var toast: ToastStack +var save_get: Callable # () -> SaveManagerCore +var adventure_get: Callable # () -> AdventureRunner(会话现值) +var runner_get: Callable # () -> LevelRunner +var adventure_mode_get: Callable # () -> bool +var open_modal: Callable # (Control) -> void +var open_modal_after_playback: Callable +var close_modal: Callable +var refresh_presentations: Callable +var fatal_screen: Callable +var gp_summary: Callable # (Dictionary) -> String + + +func _init(db_ref, meta_get_cb: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay, + board_view_ref: BoardView, toast_ref: ToastStack, save_get_cb: Callable, + adventure_get_cb: Callable, runner_get_cb: Callable, adventure_mode_get_cb: Callable, + open_modal_cb: Callable, open_modal_after_playback_cb: Callable, close_modal_cb: Callable, + refresh_presentations_cb: Callable, fatal_screen_cb: Callable, gp_summary_cb: Callable) -> void: + db = db_ref + meta_get = meta_get_cb + curio_overlay = curio_overlay_ref + end_overlay = end_overlay_ref + board_view = board_view_ref + toast = toast_ref + save_get = save_get_cb + adventure_get = adventure_get_cb + runner_get = runner_get_cb + adventure_mode_get = adventure_mode_get_cb + open_modal = open_modal_cb + open_modal_after_playback = open_modal_after_playback_cb + close_modal = close_modal_cb + refresh_presentations = refresh_presentations_cb + fatal_screen = fatal_screen_cb + gp_summary = gp_summary_cb diff --git a/game/presentation/curio_flow_context.gd.uid b/game/presentation/curio_flow_context.gd.uid new file mode 100644 index 00000000..5b1a0669 --- /dev/null +++ b/game/presentation/curio_flow_context.gd.uid @@ -0,0 +1 @@ +uid://lrba2xy83jwv diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index 85a13c76..58796e24 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -31,27 +31,24 @@ var _pending_curio := {} # 三选一停点载荷暂存(迭代 1 var _curio_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路) -func _init(db_ref, meta_get: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay, - board_view_ref: BoardView, toast_ref: ToastStack, save_get: Callable, - adventure_get: Callable, runner_get: Callable, adventure_mode_get: Callable, - open_modal: Callable, open_modal_after_playback: Callable, close_modal: Callable, - refresh_presentations: Callable, fatal_screen: Callable, gp_summary: Callable) -> void: - db = db_ref - _meta_get = meta_get - curio_overlay = curio_overlay_ref - end_overlay = end_overlay_ref - board_view = board_view_ref - toast = toast_ref - _save_get = save_get - _adventure_get = adventure_get - _runner_get = runner_get - _adventure_mode_get = adventure_mode_get - _open_modal = open_modal - _open_modal_after_playback = open_modal_after_playback - _close_modal = close_modal - _refresh_presentations = refresh_presentations - _fatal_screen = fatal_screen - _gp_summary = gp_summary +func _init(ctx: CurioFlowContext) -> void: + # 迭代 27 T4——构造参数收拢 context 对象(P1-5:16 参长签名收一处命名面;字段逐一同名迁移) + db = ctx.db + _meta_get = ctx.meta_get + curio_overlay = ctx.curio_overlay + end_overlay = ctx.end_overlay + board_view = ctx.board_view + toast = ctx.toast + _save_get = ctx.save_get + _adventure_get = ctx.adventure_get + _runner_get = ctx.runner_get + _adventure_mode_get = ctx.adventure_mode_get + _open_modal = ctx.open_modal + _open_modal_after_playback = ctx.open_modal_after_playback + _close_modal = ctx.close_modal + _refresh_presentations = ctx.refresh_presentations + _fatal_screen = ctx.fatal_screen + _gp_summary = ctx.gp_summary # 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 3d60f8ba..a393cf8e 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -119,7 +119,8 @@ var status_panel: StatusPanel # 局内状态面板·只读(迭代 17 var career_overlay: CareerOverlay # 生涯页·只读全屏页(迭代 18 卡④——跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕) var debug_overlay: DebugOverlay # 调试浮窗(迭代 14.5 调试整合批——种子/数值调整/批量模拟/两层调试入口收一) -var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体) +var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——迭代 27 T4 P1-5/P2-7 抽取;依赖仅 ModalStack+查询 Callable) +var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留) var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd) # 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住 @@ -138,30 +139,21 @@ var _settlement_stop_pending: bool: # 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 #(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, # 排播泵本体已迁 row_pacer.gd——T3 结构段)。 -var _deferred_modal: Control = null # 演播后开窗挂起(T17——胜利/Boss 总结/终局面等排播放完再开) +var _deferred_modal: Control: # 演播后开窗挂起只读视图(T17——t17_defer_probe 挂起态断言缝;本体归 _modal_flow) + get: + return _modal_flow._deferred if _modal_flow != null else null # 演播后排窗(T17 用户令「伤害过程演完才切结算窗口」):排播队列在播=挂起,演播完边沿 # (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出 # 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。 func _open_modal_after_playback(m: Control) -> void: - if _row_pacer.pending_rows().is_empty(): - _open_modal(m) - return - _deferred_modal = m - m.visible = false # 挂起含视觉(open_* 已置 visible=true——不压掉则窗在排播期照会上屏,延迟形同虚设) + _modal_flow.open_after_playback(m) # 迭代 27 T4——演播后排窗挂起编排归 ModalFlow(T17 语义逐字随迁) -# 演播完边沿派发(row_pacer.pump_ended 注入口——原直连锁期点击重放,现统一派发): -# 先开挂起 modal(胜利窗接管呈现),后锁期点击重放——modal 开启后 _input_locked 吞掉 -# 重放点击(战斗已收场,重放无意义)。 +# 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放): func _on_pump_ended() -> void: - if _deferred_modal != null: - var m: Control = _deferred_modal - _deferred_modal = null - _open_modal(m) - return - _replay_locked_click() + _modal_flow.pump_ended() # 迭代 27 T4——派发编排归 ModalFlow(重放经注入 Callable 回本类) func _replay_locked_click() -> void: @@ -304,11 +296,11 @@ func _build_ui() -> void: move_child(curio_overlay, _end_overlay.get_index()) # 提升三选一与替换子交互(T4/D4 拆件批——迁 curio_interact_controller.gd;本类只接线与回调注入, # ADR-0001——模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) - _bonus = CurioInteractController.new(db, func() -> Variant: return meta, curio_overlay, _end_overlay, board_view, + _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})) + _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": run_scope.last_gain_amount}))) # 迭代 27 T4——构造参数收拢 context(P1-5) 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) @@ -321,9 +313,9 @@ func _build_ui() -> void: # 接线见两屏装配段;开始层不再有调试入口,DS_DEBUG=1 会话=启动即开浮窗) # 营地/商店投影(T4/D4 拆件批——迁 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) - _camp_pres = CampShopPresenter.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay, + _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, _refresh_resume_button) + _open_modal, _close_modal, _refresh_presentations, _refresh_resume_button)) # 迭代 27 T4——构造参数收拢 context(P1-5) 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) @@ -370,9 +362,24 @@ func _build_ui() -> void: # 模态登记(缺陷⑤纪律——登记序 = 树序:关卡画面住冒险屏壳但登记位仍夹在节点层与三选一层之间; # 状态面板设置层同档、确认弹窗之前——P2 甲;生涯页同档随状态面板之后(迭代 18 卡④ P3 甲)) - _modals = ModalStack.new() - for m: Control in [legend_overlay, node_overlay, level_screen, curio_overlay, _end_overlay, shop_overlay, camp_overlay, settings_overlay, status_panel, career_overlay, confirm_dialog, debug_overlay]: - _register_modal(m) + # 模态编排注入(迭代 27 T4——ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable,P1-5 抽取) + _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() # 迭代 17 卡③——状态面板入口角标可见性/置灰随开态同步 + _sync_debug_overlay() # 迭代 18 卡⑦——浮窗当局数据/按钮态随开态同步 + _modal_flow.on_close_sync = func() -> void: + _sync_status_entry() # 迭代 17 卡③——关闭恢复角标可点态(无残留——R3 同源口径) + _sync_debug_overlay() # 迭代 18 卡⑦——他层关闭复活浮窗时同步刷新(P1 甲关后自动恢复语义配套) + _modal_flow.replay_click = _replay_locked_click + _modals = _modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言) + # 排除组随层登记(迭代 27 T4 P2-7——non_locking=调试浮窗〔迭代 18 卡⑦ T2 防线〕/screen_state=关卡地图屏) + for entry: Array in [[legend_overlay, []], [node_overlay, []], [level_screen, [ModalStack.TAG_SCREEN_STATE]], [curio_overlay, []], [_end_overlay, []], [shop_overlay, []], [camp_overlay, []], [settings_overlay, []], [status_panel, []], [career_overlay, []], [confirm_dialog, []], [debug_overlay, [ModalStack.TAG_NON_LOCKING]]]: + _register_modal(entry[0], entry[1]) _tune_sim = TuneSimController.new(db, debug_overlay, int(db.hero.max_hp)) @@ -384,45 +391,28 @@ func _fatal_screen(msg: String) -> void: # ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ==================== -func _register_modal(m: Control) -> void: +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) - _modals.register(m) + _modal_flow.register(m, tags) # 排除组随层登记(迭代 27 T4 P2-7——non_locking=调试浮窗/screen_state=关卡地图屏) func _open_modal(m: Control) -> void: - # 挂起兜底(T17 演播后排窗):其他路径(如回看)在播期排空队列会使演播完边沿不再触发—— - # 此处先结挂起 modal 防永不兑现(m==挂起对象时跳过=本调用自身)。 - if _deferred_modal != null and m != _deferred_modal: - var dm: Control = _deferred_modal - _deferred_modal = null - _open_modal(dm) - # 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈 - _flush_row_queue() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行) - _modals.open(m) - _sync_status_entry() # 迭代 17 卡③——状态面板入口角标可见性/置灰随开态同步 - _sync_debug_overlay() # 迭代 18 卡⑦——浮窗当局数据/按钮态随开态同步(T3 刷新=模态开合信号驱动) - + _modal_flow.open(m) # 迭代 27 T4——挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow(P1-5 抽取) func _close_modal(m: Control) -> void: - _modals.close(m) - _sync_status_entry() # 迭代 17 卡③——关闭恢复角标可点态(无残留——R3 同源口径) - _sync_debug_overlay() # 迭代 18 卡⑦——他层关闭复活浮窗时同步刷新(P1 甲关后自动恢复语义配套) - + _modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上) func _on_modal_closed(m: Control) -> void: _close_modal(m) func _input_locked() -> bool: - # 模态/非战斗期棋盘输入锁(T3 收口批——开态查询归 modal_stack.has_open(),登记册单一来源, - # 不再手工罗列各层;例外三件:开始层/过场层(非模态全屏态,不在登记册)与战斗缺位(棋盘输入无载体)) - # 迭代 18 卡⑦ T2 防护(审计🔴):判据改排除式——调试浮窗对局中可开(P2 甲),其开态不计入锁 - #(否则 DS_DEBUG 默认开×他层关闭复活浮窗×has_open 组合=棋盘永久锁死——S1+ 停机点防线) - return _modals.has_open_excluding(debug_overlay) or _start_overlay.visible or _inter_overlay.visible or run_scope.settlement == null - + # 模态/非战斗期棋盘输入锁(迭代 27 T4——判据编排归 ModalFlow.locks_input:互斥域随层登记 + # [non_locking=调试浮窗,迭代 18 卡⑦ T2 防线]+ 三例外经查询 Callable 注入;本函数留探针缝) + return _modal_flow.locks_input() # ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)==================== @@ -1085,8 +1075,7 @@ func _on_status_badge_pressed() -> void: var map_on: bool = level_screen != null and level_screen.visible # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; # 地图屏自身的登记开态不计;迭代 18 卡⑦ T2——判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) - if _modals == null or (map_on and _modals.has_open_excluding_any([level_screen, debug_overlay])) \ - or (not map_on and _modals.has_open_excluding(debug_overlay)): + if (map_on and _modal_flow.badge_greyed_map()) or (not map_on and _modal_flow.badge_greyed_battle()): return _open_status_panel() @@ -1102,8 +1091,8 @@ func _sync_status_entry() -> void: board_zone.status_badge.visible = run_scope.level_active and not map_on level_screen.status_badge.visible = map_on # 置灰判据改排除式(迭代 18 卡⑦ T2——调试浮窗开态不计入:对局中浮窗开着时角标仍可点) - board_zone.status_badge.disabled = _modals.has_open_excluding(debug_overlay) - level_screen.status_badge.disabled = _modals.has_open_excluding_any([level_screen, debug_overlay]) + board_zone.status_badge.disabled = _modal_flow.badge_greyed_battle() # 迭代 27 T4——互斥域谓词 + level_screen.status_badge.disabled = _modal_flow.badge_greyed_map() # 调试浮窗角标(迭代 18 卡⑦ P2 甲——迁宿主两屏,DS_DEBUG 门控;17卡③状态角标同形制同守卫): # 战斗屏=对局内(冒险或调试两层)且地图屏未激活;地图屏=随屏;开始菜单零角标(DS_DEBUG=1 启动即开) board_zone.debug_badge.visible = _debug_entry and not map_on and battle_live @@ -1321,8 +1310,7 @@ func _show_debug_overlay() -> void: # 调试浮窗角标点击(两屏同款处理器——17卡③状态角标同形制守卫;C6:仍收口 _show_debug_overlay 单口) func _on_debug_badge_pressed() -> void: var map_on: bool = level_screen != null and level_screen.visible - if _modals == null or (map_on and _modals.has_open_excluding_any([level_screen, debug_overlay])) \ - or (not map_on and _modals.has_open_excluding(debug_overlay)): + if (map_on and _modal_flow.badge_greyed_map()) or (not map_on and _modal_flow.badge_greyed_battle()): return _show_debug_overlay() diff --git a/game/presentation/modal_flow.gd b/game/presentation/modal_flow.gd new file mode 100644 index 00000000..9dadf4dc --- /dev/null +++ b/game/presentation/modal_flow.gd @@ -0,0 +1,89 @@ +class_name ModalFlow +extends RefCounted +# 模态编排纯逻辑类(迭代 27 T4——审计 P1-5/P2-7:组合根可测化抽取 + 模态互斥域内聚)。 +# 依赖仅 ModalStack + 查询 Callable(审计处方原话)——零节点构建、零信号连接、零核心触碰 +#(ADR-0001 同构:连接归组合根,本类只收注入)。可直接 new() 单测(V6)。 +# 职责三件: +# ① 登记与开合编排(遮挡记账单口 / T17 演播后排窗挂起——原 main._open_modal 系逻辑随迁) +# ② 棋盘输入锁谓词(互斥域随层登记——原 main._input_locked 排除式拼装消亡) +# ③ 角标置灰谓词(状态/调试两屏同款守卫判据) +# 排除语义随层登记(ModalStack.register tags):debug_overlay=["non_locking"]、 +# level_screen=["screen_state"],其余层零标签——原调用点排除式拼装面消亡(P2-7 修法)。 + +const ModalStackScript := preload("res://presentation/modal_stack.gd") # 非全局类(main 同款 preload 引用) + +var stack = ModalStackScript.new() + +# ---- 组合根注入面(查询 Callable 组——依赖仅 ModalStack + 查询 Callable 纪律载体) ---- +var start_visible_get: Callable # () -> bool——开始层可见(非模态全屏态例外一) +var inter_visible_get: Callable # () -> bool——过场层可见(例外二) +var settlement_get: Callable # () -> Variant——当前战斗编排器(null=战斗缺位例外三) +var rows_empty_get: Callable # () -> bool——排播队列空(演播后排窗判据) +var flush_rows: Callable # 开层前排空排播(卡⑦——明细带恒完整) +var on_open_sync: Callable # 开侧同步钩子(状态角标+浮窗当局数据) +var on_close_sync: Callable # 关侧同步钩子 +var replay_click: Callable # 演播完边沿无挂起时的锁期点击重放(main._replay_locked_click) + +var _deferred: Control = null # 演播后开窗挂起(T17——排播放完再开) + + +func register(m: Control, tags: Array = []) -> void: + stack.register(m, tags) + + +func open(m: Control) -> void: + # 挂起兜底(T17 演播后排窗):其他路径(如回看)在播期排空队列会使演播完边沿不再触发—— + # 此处先结挂起 modal 防永不兑现(m==挂起对象时跳过=本调用自身)。 + if _deferred != null and m != _deferred: + var dm: Control = _deferred + _deferred = null + open(dm) # 原语义:先结挂起后继续开本层(不 return——两拍都走) + flush_rows.call() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行) + stack.open(m) + on_open_sync.call() + + +func close(m: Control) -> void: + stack.close(m) + on_close_sync.call() + + +# 演播后排窗(T17 用户令「伤害过程演完才切结算窗口」):排播队列在播=挂起,演播完边沿 +# (pump_ended 派发)再开——旧路径前置 flush 会把击杀轮的伤害/死亡演出同帧倒掉。 +# 队列空=立即开(行为同旧路径)。 +func open_after_playback(m: Control) -> void: + if rows_empty_get.call(): + open(m) + return + _deferred = m + m.visible = false # 挂起含视觉(open_* 已置 visible=true——不压掉则窗在排播期照会上屏,延迟形同虚设) + + +# 演播完边沿派发(row_pacer.pump_ended 注入口):先开挂起 modal(胜利窗接管呈现), +# 后锁期点击重放——modal 开启后输入锁吞掉重放点击(战斗已收场,重放无意义)。 +func pump_ended() -> void: + if _deferred != null: + var m: Control = _deferred + _deferred = null + open(m) + return + replay_click.call() + + +# 棋盘输入锁谓词(原 main._input_locked 随迁):模态互斥域(non_locking 层开态不计——迭代 18 +# 卡⑦ T2 防线)+ 三例外(开始层/过场层非模态全屏态 + 战斗缺位棋盘输入无载体)。 +func locks_input() -> bool: + return stack.has_open_excluding_tags([ModalStackScript.TAG_NON_LOCKING]) \ + or bool(start_visible_get.call()) or bool(inter_visible_get.call()) or settlement_get.call() == null + + +# 角标置灰谓词·战斗屏形态(状态/调试两角标同判据):登记册上 non_locking 层外的任一层开着。 +# (地图屏此态恒关——屏级互斥,与旧排除式 [debug] 逐位同值) +func badge_greyed_battle() -> bool: + return stack.has_open_excluding_tags([ModalStackScript.TAG_NON_LOCKING]) + + +# 角标置灰谓词·地图屏形态(地图屏自身 screen_state 不计 + 调试浮窗 non_locking 不计—— +# 与旧排除式 [level_screen, debug] 逐位同值)。 +func badge_greyed_map() -> bool: + return stack.has_open_excluding_tags([ModalStackScript.TAG_NON_LOCKING, ModalStackScript.TAG_SCREEN_STATE]) diff --git a/game/presentation/modal_flow.gd.uid b/game/presentation/modal_flow.gd.uid new file mode 100644 index 00000000..febc42ba --- /dev/null +++ b/game/presentation/modal_flow.gd.uid @@ -0,0 +1 @@ +uid://nqnk82quv1lb diff --git a/game/presentation/modal_stack.gd b/game/presentation/modal_stack.gd index f78c48cb..ad13a8ce 100644 --- a/game/presentation/modal_stack.gd +++ b/game/presentation/modal_stack.gd @@ -8,10 +8,16 @@ extends RefCounted var _list: Array[Control] = [] # 登记序 = z 遮挡序(低 → 高) var _occluded := {} # modal → 被其隐藏的上方层(关闭恢复) +var _tags := {} # modal → 排除组标签集(迭代 27 T4——互斥域随层登记;P2-7 开态谓词收口) + +# 排除组封闭词表(组合根登记用——新标签=改此处+评审,防调用点拼装回潮) +const TAG_NON_LOCKING := "non_locking" # 开态不计入棋盘输入锁与角标置灰(调试浮窗——迭代 18 卡⑦ T2 防线:DS_DEBUG 默认开×遮挡复活×has_open 组合=棋盘永久锁死) +const TAG_SCREEN_STATE := "screen_state" # 常驻屏态(关卡地图屏——自身开态不计入本屏角标判据) -func register(m: Control) -> void: +func register(m: Control, tags: Array = []) -> void: _list.append(m) + _tags[m] = tags func open(m: Control) -> void: @@ -43,21 +49,18 @@ func has_open() -> bool: return false -# 排除式开态查询(迭代 17 卡③ 2026-10-06——状态面板入口守卫判据):skip 层自身的开态不计入 -#(关卡地图屏系登记册模态成员=常态屏态,其上的地图屏角标必须可点;其余任一层开着=角标置灰)。 -# 只读查询零副作用——登记序/开合记账语义零改动。 -func has_open_excluding(skip: Control) -> bool: - return has_open_excluding_any([skip]) - - -# 多排除式开态查询(迭代 18 卡⑦ T2 防护——调试浮窗对局中可开后,棋盘输入锁与状态角标置灰 -# 判据须同时排除调试浮窗自身〔否则 DS_DEBUG 默认开×遮挡复活×has_open 组合=棋盘永久锁死—— -# 审计🔴 S1+ 停机点防线〕;地图屏场景=地图屏+调试浮窗双排除)。语义同 has_open_excluding, -# skip 集参数化;只读查询零副作用,登记序/开合记账语义零改动。 -func has_open_excluding_any(skips: Array) -> bool: +# 排除组开态查询(迭代 27 T4——P2-7 模态协议内聚:原「调用点排除式拼装」has_open_excluding/ +# has_open_excluding_any 退役,排除语义随层登记(register tags),查询按组名集合表达。 +# 只读查询零副作用——登记序/开合记账语义零改动;空标签集=全计(与旧 has_open 同形)。 +func has_open_excluding_tags(exclude_tags: Array) -> bool: for m in _list: - if skips.has(m): - continue if is_instance_valid(m) and m.visible: - return true + var mt: Array = _tags.get(m, []) + var excluded := false + for t in exclude_tags: + if mt.has(t): + excluded = true + break + if not excluded: + return true return false diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 16ad2cef..c773c3f0 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -53,6 +53,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役) {"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴) {"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 迭代 27 T2——结算五段 Segment(V4:直接 new/编排器接线/真实一轮走段序) + {"path": "res://tests/unit/presentation/modal_flow_test.gd", "expect": 0}, # 迭代 27 T4——模态编排纯逻辑(V6:直接 new/互斥域谓词/三例外锁/演播后排窗) ] diff --git a/game/tests/unit/presentation/modal_flow_test.gd b/game/tests/unit/presentation/modal_flow_test.gd new file mode 100644 index 00000000..0c06dfd3 --- /dev/null +++ b/game/tests/unit/presentation/modal_flow_test.gd @@ -0,0 +1,113 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 模态编排纯逻辑类单测(迭代 27 T4——需求卡片 V6 验收条目:ModalFlow 直接 new() 进 run_all; +# 互斥域谓词/三例外输入锁/演播后排窗挂起/演播完边沿派发/遮挡记账——原组合根注释纪律面的 +# 结构化单测缝首次落地)。载体零场景零树——纯 RefCounted + 哑 Control。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/modal_flow_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const ModalFlowScript := preload("res://presentation/modal_flow.gd") # 探针载体禁全局类名裸引用惯例同源 +const ModalStackScript := preload("res://presentation/modal_stack.gd") + + +func _init() -> void: + # 哑层与桩 Callable(纯逻辑类零场景前提的证明面) + var start := Control.new() + var inter := Control.new() + var settlement_holder := {"s": null} # lambda 对局部按值捕获——容器装载 + var rows := {"empty": true} + var calls := {"flush": 0, "open_sync": 0, "close_sync": 0, "replay": 0} + var flow = ModalFlowScript.new() + Kit.check_quiet(flow != null and flow.stack != null, "V6 ModalFlow 直接 new()(stack 内建)〔迭代27 T4〕") + flow.start_visible_get = func() -> bool: return start.visible + flow.inter_visible_get = func() -> bool: return inter.visible + flow.settlement_get = func() -> Variant: return settlement_holder["s"] + flow.rows_empty_get = func() -> bool: return rows["empty"] + flow.flush_rows = func() -> void: calls["flush"] += 1 + flow.on_open_sync = func() -> void: calls["open_sync"] += 1 + flow.on_close_sync = func() -> void: calls["close_sync"] += 1 + flow.replay_click = func() -> void: calls["replay"] += 1 + + # V6-1 互斥域登记+遮挡记账 + var a := Control.new() # 普通层 + var dbg := Control.new() # 调试浮窗(non_locking) + var map := Control.new() # 关卡地图屏(screen_state) + flow.register(a) + flow.register(dbg, [ModalStackScript.TAG_NON_LOCKING]) + flow.register(map, [ModalStackScript.TAG_SCREEN_STATE]) + var tag_dump := "tags a=%s dbg=%s map=%s const=%s" % [str(flow.stack._tags.get(a, "MISSING")), str(flow.stack._tags.get(dbg, "MISSING")), str(flow.stack._tags.get(map, "MISSING")), ModalStackScript.TAG_NON_LOCKING] + print("[DBG] ", tag_dump) + flow.open(a) + Kit.check_quiet(a.visible and calls["open_sync"] == 1 and calls["flush"] == 1, "V6 开侧编排(flush→open→sync 单口序)〔迭代27 T4〕") + # 遮挡记账(登记序=z 序:开低层隐其上方可见层、关闭恢复——低层开启期高层照会不可见的呈现保真) + flow.open(map) + Kit.check_quiet(map.visible, "V6 开最高层(上方无可隐)〔迭代27 T4〕") + flow.open(a) + Kit.check_quiet(a.visible and not map.visible, "V6 遮挡记账(开低层隐其上方可见层)〔迭代27 T4〕") + flow.close(a) + Kit.check_quiet(map.visible and calls["close_sync"] == 1, "V6 关闭恢复(关低层显其上方被隐层+关侧同步)〔迭代27 T4〕") + + # V6-2 输入锁谓词:互斥域随层登记(non_locking 不计)+ 三例外 + start.visible = false + inter.visible = false + settlement_holder["s"] = Object.new() + flow.open(dbg) + Kit.check_quiet(not flow.locks_input(), "V6 输入锁:调试浮窗 non_locking 不计(T2 防线)〔迭代27 T4〕") + flow.close(dbg) + flow.open(a) + Kit.check_quiet(flow.locks_input(), "V6 输入锁:普通层开态计锁〔迭代27 T4〕") + flow.close(a) + flow.close(map) # 登记册全关(close(a) 经遮挡恢复 map——须一并关账才是「全关」态) + settlement_holder["s"] = null + Kit.check_quiet(flow.locks_input(), "V6 输入锁例外三:战斗缺位即锁〔迭代27 T4〕") + settlement_holder["s"] = Object.new() + Kit.check_quiet(not flow.locks_input(), "V6 输入锁:战斗在座+登记册全关=不锁〔迭代27 T4〕") + start.visible = true + Kit.check_quiet(flow.locks_input(), "V6 输入锁例外一:开始层可见即锁〔迭代27 T4〕") + start.visible = false + inter.visible = true + Kit.check_quiet(flow.locks_input(), "V6 输入锁例外二:过场层可见即锁〔迭代27 T4〕") + inter.visible = false + Kit.check_quiet(not flow.locks_input(), "V6 输入锁:全例外解除+登记册全关=不锁〔迭代27 T4〕") + + # V6-3 角标谓词(状态/调试两屏同款守卫——与旧排除式逐位同值) + flow.open(dbg) + Kit.check_quiet(not flow.badge_greyed_battle() and not flow.badge_greyed_map(), + "V6 角标谓词:浮窗开态两形制均不置灰〔迭代27 T4〕") + flow.close(dbg) + flow.open(map) + Kit.check_quiet(not flow.badge_greyed_map() and flow.badge_greyed_battle(), + "V6 角标谓词:地图屏 screen_state 地图形制不计/战斗形制计(旧排除式 [debug] 与 [level,debug] 同值面)〔迭代27 T4〕") + flow.close(map) + flow.open(a) + Kit.check_quiet(flow.badge_greyed_battle() and flow.badge_greyed_map(), + "V6 角标谓词:普通层开态两形制均置灰〔迭代27 T4〕") + flow.close(a) + + # V6-4 演播后排窗挂起+边沿派发(T17 语义) + var end_ov := Control.new() + end_ov.visible = false + rows["empty"] = false + var sync_before1: int = calls["open_sync"] + flow.open_after_playback(end_ov) + Kit.check_quiet(flow._deferred == end_ov and not end_ov.visible and calls["open_sync"] == sync_before1, + "V6 演播后排窗:在播挂起含视觉(不照会上屏——零开侧同步)〔迭代27 T4〕") + rows["empty"] = true + var sync_before2: int = calls["open_sync"] + flow.pump_ended() + Kit.check_quiet(flow._deferred == null and end_ov.visible and calls["open_sync"] == sync_before2 + 1, + "V6 演播完边沿:挂起 modal 先开(队列空判定由 rows_empty_get 承载)〔迭代27 T4〕") + flow.close(end_ov) + flow.pump_ended() + Kit.check_quiet(calls["replay"] == 1, "V6 演播完边沿:无挂起→锁期点击重放派发〔迭代27 T4〕") + # 挂起兜底(他路径开新层先结挂起防永不兑现) + rows["empty"] = false + flow.open_after_playback(end_ov) + rows["empty"] = true + var other := Control.new() + var sync_before: int = calls["open_sync"] + flow.open(other) + Kit.check_quiet(end_ov.visible and flow._deferred == null and calls["open_sync"] == sync_before + 2, + "V6 挂起兜底:开新层先结挂起(T17 防永不兑现——挂起层先开+新层后开两拍)〔迭代27 T4〕") + + quit(Kit.finish("modal_flow")) diff --git a/game/tests/unit/presentation/modal_flow_test.gd.uid b/game/tests/unit/presentation/modal_flow_test.gd.uid new file mode 100644 index 00000000..ec53ac25 --- /dev/null +++ b/game/tests/unit/presentation/modal_flow_test.gd.uid @@ -0,0 +1 @@ +uid://d2pdgrtttf26q