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

+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