refactor(迭代27 T4): 波四 ModalFlow 抽取+互斥域 tags 化+双 context——对拍全同

P1-5/P2-7 组合根可测化+模态协议内聚(场景拆分后重估结论=抽取面收敛为模态编排域):
- modal_flow.gd 新纯逻辑类: 依赖仅 ModalStack+查询 Callable(审计处方)——开合编排
  (挂起兜底/排播排空/遮挡记账/开合同步)/棋盘输入锁谓词(三例外注入)/双形制角标谓词;
  V6 modal_flow_test 19 断言直接 new() 进 run_all(26→27 套)——测试抓出并修正
  挂起兜底 divergent return 一处(原 main 语义=先结挂起后继续开本层不短路)
- ModalStack 互斥域随层登记: register(m, tags) + TAG_NON_LOCKING/TAG_SCREEN_STATE
  封闭词表 + has_open_excluding_tags 组查询;原 has_open_excluding/_any 调用点拼装退役
  (main 五处判据迁移);main._modals=flow.stack(ui_struct _list 登记序断言缝保留)
- P1-5 控制器构造收拢 context: curio 16 参→CurioFlowContext/camp 13 参→CampFlowContext
  (字段逐一同名迁移,控制器可独立实例化)

验收: 对拍三变体全同;冒烟 1079/0+run_all 27 套+probe click+render golden 全绿
This commit is contained in:
DKChan committed 2026-10-15 00:31:36 +08:00
1 parent 6fd7d782cc
commit 8b9e55673f
13 files changed
+383 -107

No files matched your search

+38
View File
@@ -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
@@ -0,0 +1 @@
uid://dxj1wy8n1yp6k
+15 -17
View File
@@ -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
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
+45
View File
@@ -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
@@ -0,0 +1 @@
uid://lrba2xy83jwv
+18 -21
View File
@@ -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 分派面——战斗总结确认后开三选一)
+41 -53
View File
@@ -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()
+89
View File
@@ -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])
+1
View File
@@ -0,0 +1 @@
uid://nqnk82quv1lb
+19 -16
View File
@@ -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
+1
View File
@@ -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/互斥域谓词/三例外锁/演播后排窗)
]
@@ -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"))
@@ -0,0 +1 @@
uid://d2pdgrtttf26q