598 lines
31 KiB
GDScript
598 lines
31 KiB
GDScript
# tests/system_test.gd — 系统测试宿主薄壳(SceneTree+构造注入)
|
||
# 系统测试 = 启动真实工程走真实玩法路径的断言面(标准四级测试层之一,闸 01)。
|
||
# 本件为系统测试宿主(标准四级测试层之一,闸 01)。(原过渡期自述作废——,
|
||
# 本批只落宿主薄壳,跑通即绿)。迁移完成后 tools/run_system_test.py 缺省 --script 改指本件。
|
||
# 协议照单元侧惯例:extends SceneTree 获得 quit(code);--script 模式不加载 autoload,
|
||
# 运行形态=--script 直跑+autoload 分派闸(ConfigDB/SaveManager 在座才进主断言面——闸 01 实跑 65 项 0 失败实证在案);摘要行由 tools/run_system_test.py 三闸判定
|
||
# (退出码+摘要失败数+脚本错误扫描)。
|
||
# 运行:python tools/run_system_test.py --script res://tests/system_test.gd
|
||
extends SceneTree
|
||
|
||
const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现(lifecycle 活战斗驱动;smoke 库件迁移期按现路径 preload)
|
||
|
||
var _failures: int = 0
|
||
var _checks: int = 0
|
||
|
||
|
||
func check(cond: bool, label: String) -> void:
|
||
_checks += 1
|
||
if cond:
|
||
print(" [PASS] " + label)
|
||
else:
|
||
_failures += 1
|
||
print(" [FAIL] " + label)
|
||
|
||
|
||
func _initialize() -> void:
|
||
print("=== Dungeon Sweeper 系统测试 ===")
|
||
_split_tail()
|
||
|
||
|
||
# ---- 场景拆分/会话物化异步尾段(autoload 挂载需过 _initialize 后首帧〔run_all.gd 头注时序〕,
|
||
# 挂 main.tscn 的断言在帧后执行;摘要打印与 quit 在本尾段)----
|
||
# (自 game/smoke/smoke_all.gd 剪切迁入——断言 label 逐字原样;G12 两套调用不随迁,
|
||
# 归 source_guard_test.gd,断言迁移批 B G1)
|
||
func _split_tail() -> void:
|
||
await process_frame
|
||
await process_frame
|
||
var autoload_ok: bool = get_root().get_node_or_null("/root/ConfigDB") != null and get_root().get_node_or_null("/root/SaveManager") != null
|
||
if autoload_ok:
|
||
await _smoke_scene_split() # 协程函数必须 await(无 await=发射后不管,首个 await 后即被 quit 截断)
|
||
await _smoke_boot_chain()
|
||
await _smoke_run_lifecycle()
|
||
await _smoke_sim_guards()
|
||
await _smoke_lifecycle_matrix()
|
||
else:
|
||
check(false, "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)")
|
||
print("[system_test] 摘要:%d 项检查,%d 项失败" % [_checks, _failures])
|
||
quit(1 if _failures > 0 else 0)
|
||
|
||
|
||
# ---- 三屏场景化断言(V2——独立实例化+关键节点在位+组合根树序/z 序保序)----
|
||
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
|
||
func _smoke_scene_split() -> void:
|
||
print("--- 迭代27 T1 三屏场景化(V2:屏壳独立实例化+组合根装配保序)---")
|
||
# ① 三屏壳独立实例化(F6 等价——headless 直挂;视图代码构建在屏壳 _ready)
|
||
var menu: Node = load("res://presentation/scenes/menu_screen.tscn").instantiate()
|
||
root.add_child(menu)
|
||
await process_frame
|
||
check(menu.start_overlay != null and menu.entersavebtn != null and menu.settingsbtn != null and menu.quitbtn != null,
|
||
"V2 菜单屏壳独立实例化(start_overlay+操作组三钮在位——主菜单三入口化)〔卡② mn1〕")
|
||
check(menu.start_overlay.visible, "V2 菜单层默认可见(非模态全屏态)")
|
||
check(menu.mouse_filter == Control.MOUSE_FILTER_IGNORE, "V2 屏壳根零鼠标截获(IGNORE——全屏 STOP 壳吃输入病灶回归闸)")
|
||
menu.queue_free()
|
||
var camp: Node = load("res://presentation/scenes/camp_screen.tscn").instantiate()
|
||
root.add_child(camp)
|
||
await process_frame
|
||
check(camp.camp_overlay != null, "V2 营地屏壳独立实例化(camp_overlay 在位)")
|
||
check(not camp.camp_overlay.visible, "V2 营地层默认隐藏(模态纪律)")
|
||
camp.queue_free()
|
||
var adv: Node = load("res://presentation/scenes/adventure_screen.tscn").instantiate()
|
||
root.add_child(adv)
|
||
await process_frame
|
||
check(adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != null and adv.ap_bar != null and adv.level_screen != null,
|
||
"V2 冒险屏壳独立实例化(战斗宿主+四区句柄+关卡地图屏在位)")
|
||
adv.queue_free()
|
||
await process_frame
|
||
# ② 组合根装配(合同单点化——LAYER_CONTRACT 唯一事实源:树序=登记序=z 序同源派生)
|
||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||
root.add_child(main)
|
||
await process_frame
|
||
# V1a 启动态顶层断言:boot 后自树顶向下第一个可见内容层=start_overlay(headless 可判)
|
||
#(内容层遍历——屏壳是容器不当候选:camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历会误命中)
|
||
var top: Node = _topmost_visible_layer(main)
|
||
check(top == main._start_overlay,
|
||
"V1a 启动态顶层可见层=start_overlay(实测顶层=%s——菜单被冒险屏盖住即红)〔装配生命批 V1〕"
|
||
% (top.name if top != null else "null"))
|
||
# V1b 语义关系:start_overlay 在冒险屏内容之上(菜单屏壳在冒险屏壳之上——装配序修复判据)
|
||
check(main._menu_screen.get_index() > main._adventure_screen.get_index(),
|
||
"V1b 菜单屏壳在冒险屏壳之上(start_overlay 盖住战斗内容)〔装配生命批 V1〕")
|
||
# V1c 合同树序链逐对保序(直接子层全链=屏壳/模态/全屏态/非视觉节点;宿住层树序随宿主不参与)
|
||
var chain_detail := ""
|
||
var chain_prev: Node = null
|
||
var chain_ok := true
|
||
for entry: Dictionary in main.LAYER_CONTRACT:
|
||
if entry.has("host"):
|
||
continue
|
||
var node: Node = main._layer_node(String(entry["shell"]) if entry.has("shell") else String(entry["key"]))
|
||
if node == null:
|
||
chain_ok = false
|
||
chain_detail += " 缺失:%s" % String(entry["key"])
|
||
continue
|
||
if chain_prev != null and node.get_index() <= chain_prev.get_index():
|
||
chain_ok = false
|
||
chain_detail += " %s(%d)≤%s(%d)" % [chain_prev.name, chain_prev.get_index(), node.name, node.get_index()]
|
||
chain_prev = node
|
||
check(chain_ok, "V1c 合同树序链逐对保序(运行期树序==LAYER_CONTRACT 序;违例:%s)〔装配生命批 V1〕" % chain_detail)
|
||
# ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明)
|
||
check(main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view,
|
||
"V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)")
|
||
check(main.run_scope != null and not main.run_scope.is_adventure,
|
||
"V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)")
|
||
main.queue_free()
|
||
await process_frame
|
||
|
||
|
||
# 顶层可见内容层(V1a 用——候选=合同模态/全屏态+两屏壳的 boot 内容成员;屏壳本身是容器不当候选:
|
||
# camp 壳 boot 态 vis=true 但成员恒隐,裸树遍历自顶向下会先命中透明壳)
|
||
func _topmost_visible_layer(main: Node) -> Node:
|
||
var candidates: Array[Node] = [main._start_overlay, main._battle_root, main._inter_overlay,
|
||
main.legend_overlay, main.node_overlay, main.level_screen, main.curio_overlay,
|
||
main._end_overlay, main.shop_overlay, main.camp_overlay, main.settings_overlay,
|
||
main.status_panel, main.career_overlay, main.confirm_dialog, main.debug_overlay]
|
||
var best: Node = null
|
||
var best_x := -1
|
||
var best_y := -1
|
||
for c: Node in candidates:
|
||
if c == null or not (c is CanvasItem) or not (c as CanvasItem).visible:
|
||
continue
|
||
var rx: int = c.get_parent().get_index()
|
||
var ry: int = c.get_index()
|
||
if best == null or rx > best_x or (rx == best_x and ry > best_y):
|
||
best = c
|
||
best_x = rx
|
||
best_y = ry
|
||
return best
|
||
|
||
|
||
# ---- boot 链路时序后置条件(启动态六断言;只立行为可判断言,不造假时序)----
|
||
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
|
||
|
||
func _count_visible_buttons(node: Node) -> int:
|
||
var n := 1 if (node is Button and node.visible) else 0
|
||
for c in node.get_children():
|
||
n += _count_visible_buttons(c)
|
||
return n
|
||
|
||
|
||
# ---- boot 链路时序后置条件(启动态六断言;只立行为可判断言,不造假时序)----
|
||
func _smoke_boot_chain() -> void:
|
||
print("--- 迭代28 T2 boot 链路时序后置条件(V2:六断言)---")
|
||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||
root.add_child(main)
|
||
await process_frame
|
||
await process_frame
|
||
# ① ConfigDB 就绪且表已载
|
||
var db = main.get_node("/root/ConfigDB")
|
||
check(db.is_ready and db.combat.hero != null,
|
||
"V2a ConfigDB 就绪且表已载(is_ready+hero 表在座)〔装配生命批 V2〕")
|
||
# ② 设置已应用(引擎态=settings_store 现值——真实设置文件照读,一致性判据与文件内容无关)
|
||
var ss = main.settings_store
|
||
var master := AudioServer.get_bus_index("Master")
|
||
check(AudioServer.is_bus_mute(master) == (ss.volume_linear <= 0.001)
|
||
and is_equal_approx(AudioServer.get_bus_volume_db(master), linear_to_db(maxf(ss.volume_linear, 0.001))),
|
||
"V2b 设置已应用·音量(Master mute/volume_db=settings_store 现值 linear=%f)〔装配生命批 V2〕" % ss.volume_linear)
|
||
var ui_kit = load("res://presentation/views/ui_kit.gd")
|
||
check(ui_kit.motion_scale == (0.5 if ss.reduced_motion else 1.0),
|
||
"V2b 设置已应用·减动效(UiKit.motion_scale=%s=settings_store.reduced_motion=%s)〔装配生命批 V2〕" % [
|
||
str(ui_kit.motion_scale), str(ss.reduced_motion)])
|
||
# ③ 启动态菜单可见
|
||
check(main._start_overlay.visible,
|
||
"V2c 启动态 _start_overlay.visible=true(boot 后玩家见菜单)〔装配生命批 V2〕")
|
||
# ④ 续玩钮可见态==SaveManager 断点在档态
|
||
var sm = main.get_node("/root/SaveManager")
|
||
# ④-1 boot 路径:启动态钉死(卡① sv2)——boot 无激活态、断点在档恒否,续玩钮恒隐
|
||
#(原「按钮态==真实档断点态」关系判据随启动态换代改写:test-conventions §9.5 申报在案,卡② mn8 再承接)
|
||
# ④-1b 启动态三断言(卡① sv2——真实 autoload boot 链)
|
||
check(sm.slots.active_slot() == 0 and not sm.slots.has_write_target(),
|
||
"V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕")
|
||
check(sm.meta_state.keystone_balance() == 0 and not sm.has_breakpoint() and sm.meta_state.career_state().adventure_total == 0,
|
||
"V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕")
|
||
check(not sm.write_to_disk(),
|
||
"V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕")
|
||
# ④-2 三钮承接(卡② mn8——V2d 续玩钮族随钮退役废止:钮可见性联动面由卡① V2-s 启动态断言与
|
||
# sv11 attach 语义段承接;attach 原地重置语义保持——绝不碰真实档 user://save.json)
|
||
for p in ["user://smoke_boot_chain.json", "user://smoke_boot_chain.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
sm.slots.attach_path("user://smoke_boot_chain.json")
|
||
check(main._menu_screen.entersavebtn != null and main._menu_screen.settingsbtn != null
|
||
and main._menu_screen.quitbtn != null,
|
||
"V2d 三钮在位·进入存档/设置/退出游戏(续玩钮族退役后主菜单操作组承接)〔卡② mn1/mn8〕")
|
||
for old_btn in ["lvbtn", "resumebtn", "campbtn", "careerbtn"]:
|
||
check(not (old_btn in main._menu_screen),
|
||
"V2d 四旧钮句柄零命中·%s(彻底撤出——禁隐藏态禁置灰态残留)〔卡② mn2〕" % old_btn)
|
||
check(_count_visible_buttons(main._start_overlay) == 3,
|
||
"V2d 渲染面可见按钮计数==3(旧钮接回即红——渲染面锚,句柄缝盲区闭合)〔卡② mn1/mn2〕")
|
||
# ⑤ 闲时会话(_ready 即建、永非 null、在树)
|
||
check(main.run_scope != null and not main.run_scope.is_adventure and main.run_scope.is_inside_tree(),
|
||
"V2e run_scope 非空且为闲时会话(is_adventure=false+is_inside_tree)〔装配生命批 V2〕")
|
||
# ⑥ 调试浮窗 boot 不开+菜单角标在(菜单优先——boot 不自动开浮窗,
|
||
# 入口=开始屏右下角「调试」角标+对局中两屏角标)
|
||
check(not main.debug_overlay.visible,
|
||
"V2f 调试浮窗 boot 不自动开(菜单优先)〔2026-10-16 用户令〕")
|
||
check(main.debug_badge != null and not main.debug_badge.visible,
|
||
"V2f 调试浮层角标发布态恒隐(2026-10-25 删写死行批)")
|
||
main.queue_free()
|
||
await process_frame
|
||
for p in ["user://smoke_boot_chain.json", "user://smoke_boot_chain.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
|
||
|
||
func _smoke_run_lifecycle() -> void:
|
||
print("--- 迭代27 T1 会话物化(V3:两连局复位+脏状态不残留)---")
|
||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||
root.add_child(main)
|
||
await process_frame
|
||
# 存档隔离(autoload attach 原地重置——绝不碰真实档 user://save.json)
|
||
var sm = main.get_node("/root/SaveManager")
|
||
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
sm.slots.attach_path("user://smoke_run_lifecycle.json")
|
||
sm.meta_state.mark_first_adventure_done()
|
||
# ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ----
|
||
main.debug_overlay.seed_edit.text = "42"
|
||
main._start_adventure_from_menu()
|
||
await process_frame
|
||
check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null,
|
||
"V3 开局建会话(is_adventure+adventure 在座)")
|
||
check(main.run_scope.hints_shown.is_empty(),
|
||
"V3 新会话局态零值")
|
||
# 注入脏状态(典型泄漏源——新会话必须整体吞掉;刷新计数单源化在编排器,不属会话脏面)
|
||
main.run_scope.hints_shown["chord"] = true
|
||
main.run_scope.boss_active = true
|
||
main.run_scope.last_gain_amount = 77
|
||
var finished := {"done": false}
|
||
main.run_scope.adventure.adventure_finished.connect(func(_s: Dictionary) -> void:
|
||
finished.done = true, CONNECT_ONE_SHOT)
|
||
check(main.run_scope.adventure.abandon(), "V3 放弃受理(入口停点天然非战斗期)")
|
||
for i in range(8):
|
||
if finished.done:
|
||
break
|
||
await process_frame
|
||
check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)")
|
||
check(main._end_overlay.is_open(), "V3 终局面板弹起")
|
||
main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡)
|
||
await process_frame
|
||
check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null,
|
||
"V3 会话收束(新闲时会话零冒险/关卡对象)")
|
||
check(main.run_scope.hints_shown.is_empty() and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0,
|
||
"V3 局态封印/账本全复位(queue_free 物化——注入脏状态零残留)")
|
||
check(main._start_overlay.visible, "V3 回开始菜单(终局出口路径)")
|
||
# ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ----
|
||
check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点)")
|
||
main.debug_overlay.seed_edit.text = "43"
|
||
main._start_adventure_from_menu()
|
||
await process_frame
|
||
check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty(),
|
||
"V3 第二连局全新会话(两连局零泄漏)")
|
||
check(not main._start_overlay.visible, "V3 直进入局(菜单隐)")
|
||
main.queue_free()
|
||
await process_frame
|
||
for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
|
||
|
||
# ---- d6/d7 批量模拟互斥守卫(wd6/wd7——白盒置态直调入口,以守卫早退的可见后果为断言面)----
|
||
func _smoke_sim_guards() -> void:
|
||
print("--- 卡① T2:批量模拟互斥两守卫(d6/d7)---")
|
||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||
root.add_child(main)
|
||
await process_frame
|
||
var sm = main.get_node("/root/SaveManager")
|
||
for p in ["user://smoke_sim_guards.json", "user://smoke_sim_guards.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
sm.slots.attach_path("user://smoke_sim_guards.json")
|
||
sm.meta_state.mark_first_adventure_done()
|
||
# wd6:闲时态 + 模拟运行态 → 调试两层入口守卫早退(开始层不收起、无结算对象)
|
||
check(main.run_scope.settlement == null and not main.run_scope.is_adventure,
|
||
"前置:闲时会话(settlement 空 + is_adventure=false)")
|
||
main._start_overlay.visible = true
|
||
main._tune_sim._running = true
|
||
main._start_debug_levels()
|
||
check(main.run_scope.settlement == null and main._start_overlay.visible,
|
||
"模拟运行态下调试两层入口守卫早退(开始层不收起)〔wd6〕")
|
||
main._tune_sim._running = false
|
||
# wd7:真实冒险关间态(is_adventure=true、settlement==null)→ 模拟入口守卫早退
|
||
main.debug_overlay.seed_edit.text = "42"
|
||
main._start_adventure_from_menu()
|
||
await process_frame
|
||
check(main.run_scope.is_adventure and main.run_scope.settlement == null,
|
||
"前置:真实冒险关间态(选路停点,非战斗)")
|
||
main._on_tune_simulate(3)
|
||
check(not main._tune_sim.is_running(),
|
||
"冒险关间态下批量模拟入口守卫早退(d7 补 is_adventure)〔wd7〕")
|
||
main._tune_sim._running = false # 红态清理(守卫缺失时 run 已启动)
|
||
main.run_scope.adventure.abandon()
|
||
for i in range(8):
|
||
await process_frame
|
||
main.queue_free()
|
||
await process_frame
|
||
for p in ["user://smoke_sim_guards.json", "user://smoke_sim_guards.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
|
||
|
||
# 引用面快照(五承载物的会话相关面+恒活面——逐个调用现值回调,与 run_scope 成员并采)
|
||
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
|
||
func _face_snapshot(main: Node) -> Dictionary:
|
||
var tu = main._treasure_ui
|
||
var bo = main._bonus
|
||
var cp = main._camp_pres
|
||
var mf = main._modal_flow
|
||
var rp = main._row_pacer
|
||
return {
|
||
"treasure_settlement": tu._settlement_get.call(),
|
||
"treasure_runner": tu._runner_get.call(),
|
||
"bonus_meta": bo.ctx.getters.meta_get.call(),
|
||
"bonus_save": bo.ctx.getters.save_get.call(),
|
||
"bonus_adventure": bo.ctx.getters.adventure_get.call(),
|
||
"bonus_runner": bo.ctx.getters.runner_get.call(),
|
||
"bonus_mode": bo.ctx.getters.adventure_mode_get.call(),
|
||
"camp_meta": cp._meta_get.call(),
|
||
"camp_save": cp._save_get.call(),
|
||
"camp_runner": cp._runner_get.call(),
|
||
"mf_start": mf.start_visible_get.call(),
|
||
"mf_inter": mf.inter_visible_get.call(),
|
||
"mf_settlement": mf.settlement_get.call(),
|
||
"mf_rows_empty": mf.rows_empty_get.call(),
|
||
"rp_log": rp.log_view, "rp_stage": rp.stage, "rp_board_zone": rp.board_zone,
|
||
"rp_fx": rp.settlement_fx, "rp_board": rp.board_view,
|
||
"rp_refresh": rp.refresh_request, "rp_pump": rp.pump_ended,
|
||
"rs_adventure": main.run_scope.adventure, "rs_runner": main.run_scope.runner,
|
||
"rs_settlement": main.run_scope.settlement, "rs_is_adventure": main.run_scope.is_adventure,
|
||
}
|
||
|
||
|
||
# 阶段面断言:adv/run/set=alive|null|both(会话三面存活态);恒活面每阶段必查;
|
||
# 现值回调与 run_scope 成员必须同源(回调链无悬挂铁证);extra=阶段附加判据。
|
||
func _stage_faces(main: Node, tag: String, adv: String, run: String, set: String, extra: Dictionary = {}) -> void:
|
||
var snap := _face_snapshot(main)
|
||
var bad := ""
|
||
for k in ["bonus_meta", "bonus_save", "camp_meta", "camp_save", "rp_log", "rp_stage", "rp_board_zone", "rp_fx", "rp_board"]:
|
||
if not is_instance_valid(snap[k]):
|
||
bad += " %s=失效" % k
|
||
for k in ["rp_refresh", "rp_pump"]:
|
||
var cb: Variant = snap[k]
|
||
if not (cb is Callable) or not (cb as Callable).is_valid():
|
||
bad += " %s=非有效Callable" % k
|
||
if not main.run_scope.is_inside_tree():
|
||
bad += " run_scope=不在树"
|
||
bad += _face_liveness("adventure", snap["rs_adventure"], adv)
|
||
bad += _face_liveness("runner", snap["rs_runner"], run)
|
||
bad += _face_liveness("settlement", snap["rs_settlement"], set)
|
||
if snap["treasure_settlement"] != snap["rs_settlement"]:
|
||
bad += " treasure._settlement_get≠run_scope.settlement(回调悬挂)"
|
||
if snap["treasure_runner"] != snap["rs_runner"]:
|
||
bad += " treasure._runner_get≠run_scope.runner(回调悬挂)"
|
||
if snap["bonus_adventure"] != snap["rs_adventure"]:
|
||
bad += " bonus._adventure_get≠run_scope.adventure(回调悬挂)"
|
||
if snap["bonus_runner"] != snap["rs_runner"]:
|
||
bad += " bonus._runner_get≠run_scope.runner(回调悬挂)"
|
||
if snap["camp_runner"] != snap["rs_runner"]:
|
||
bad += " camp._runner_get≠run_scope.runner(回调悬挂)"
|
||
if snap["mf_settlement"] != snap["rs_settlement"]:
|
||
bad += " modal_flow.settlement_get≠run_scope.settlement(回调悬挂)"
|
||
if bool(snap["bonus_mode"]) != bool(snap["rs_is_adventure"]):
|
||
bad += " bonus._adventure_mode_get≠run_scope.is_adventure(回调悬挂)"
|
||
for k in extra:
|
||
match String(k):
|
||
"mf_start":
|
||
if bool(snap["mf_start"]) != bool(extra[k]):
|
||
bad += " modal_flow.start_visible_get 应=%s" % str(extra[k])
|
||
"mf_inter":
|
||
if bool(snap["mf_inter"]) != bool(extra[k]):
|
||
bad += " modal_flow.inter_visible_get 应=%s" % str(extra[k])
|
||
"mf_rows_empty":
|
||
if bool(snap["mf_rows_empty"]) != bool(extra[k]):
|
||
bad += " modal_flow.rows_empty_get 应=%s" % str(extra[k])
|
||
"level_vis":
|
||
if main.level_screen.visible != bool(extra[k]):
|
||
bad += " level_screen.visible 应=%s" % str(extra[k])
|
||
"end_open":
|
||
if main._end_overlay.is_open() != bool(extra[k]):
|
||
bad += " _end_overlay.is_open 应=%s" % str(extra[k])
|
||
"start_vis":
|
||
if main._start_overlay.visible != bool(extra[k]):
|
||
bad += " _start_overlay.visible 应=%s" % str(extra[k])
|
||
check(bad.is_empty(), "%s(引用面违例:%s)〔装配生命批 V3〕" % [tag, bad])
|
||
|
||
|
||
# 单面存活态判据:want=null=必须缺位/alive=必须有效非空/both=两可但非空必有效
|
||
func _face_liveness(name: String, v, want: String) -> String:
|
||
if want == "null":
|
||
return "" if v == null else " %s 应缺位(实测非空)" % name
|
||
if want == "alive" and v == null:
|
||
return " %s 应在座(实测 null)" % name
|
||
if v != null and not is_instance_valid(v):
|
||
return " %s 悬挂(失效对象)" % name
|
||
return ""
|
||
|
||
|
||
# ---- 生命周期存活性矩阵(九阶段×五引用面;驱动复用 V3 debug seed 开局路径+
|
||
# _auto_play_round 活战斗模式+ui_struct resume 夹具模式,不另起点击链)----
|
||
# 阶段映射说明:计划书十阶段中「开局停点」与「地图选路」在本现行流程同为关卡画面停点态
|
||
#(level_started→choices_presented 同步到达,无可观测中间态)——合并为 V3b 一处断言。
|
||
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
|
||
func _smoke_lifecycle_matrix() -> void:
|
||
print("--- 迭代28 T3 生命周期存活性矩阵(V3:九阶段×五引用面)---")
|
||
var main: Node = load("res://presentation/main.tscn").instantiate()
|
||
root.add_child(main)
|
||
await process_frame
|
||
await process_frame
|
||
var sm = main.get_node("/root/SaveManager")
|
||
for p in ["user://smoke_lifecycle.json", "user://smoke_lifecycle.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
sm.slots.attach_path("user://smoke_lifecycle.json")
|
||
main.debug_overlay.seed_edit.text = "42"
|
||
# 阶段①闲时
|
||
await _stage_faces(main, "V3a 闲时", "null", "null", "null",
|
||
{"mf_start": true, "mf_inter": false, "mf_rows_empty": true, "start_vis": true})
|
||
# 阶段②开局停点(地图选路停点态)
|
||
main._start_adventure_from_menu()
|
||
for i in range(6):
|
||
await process_frame
|
||
await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null",
|
||
{"mf_start": false, "level_vis": true})
|
||
# 阶段③地图选路→战斗中(真实选路提交 press_candidate)
|
||
main.level_screen.press_candidate(0)
|
||
for i in range(10):
|
||
await process_frame
|
||
await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false})
|
||
# 阶段④结算后/终局面(AutoPlayer 活战斗驱动到终局——_auto_play_round 同款 phase/round 守卫)
|
||
var finished := {"done": false}
|
||
main.run_scope.adventure.adventure_finished.connect(func(_s: Dictionary) -> void:
|
||
finished.done = true, CONNECT_ONE_SHOT)
|
||
var guard := 0
|
||
while main.run_scope.settlement != null and main.run_scope.settlement.is_battle_live() \
|
||
and not finished.done and guard < 600:
|
||
var st = main.run_scope.settlement
|
||
var ms = st.minesweeper
|
||
var ae = st.action_economy
|
||
var start_round: int = st.round_index
|
||
var round_guard := 400
|
||
while ae.phase == "Active" and st.round_index == start_round and round_guard > 0 \
|
||
and main.run_scope.settlement != null:
|
||
round_guard -= 1
|
||
var plan: Dictionary = AutoPlayer.choose_dig(ms)
|
||
if plan.is_empty():
|
||
break
|
||
if plan.hero_cell != ms.hero_pos():
|
||
ms.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||
if ae.phase != "Active" or st.round_index != start_round:
|
||
break
|
||
ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||
if main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live():
|
||
break
|
||
if ae.phase == "Active" and st.round_index == start_round:
|
||
ms.settle_batch([{"action": "stop"}])
|
||
guard += 1
|
||
check(main.run_scope.settlement == null or not main.run_scope.settlement.is_battle_live(),
|
||
"V3d 活战斗驱动完毕(战斗已收场——%d 轮,finished=%s)〔装配生命批 V3〕" % [guard, str(finished.done)])
|
||
await process_frame
|
||
main._flush_row_queue()
|
||
for i in range(12):
|
||
if main._end_overlay.is_open():
|
||
break
|
||
await process_frame
|
||
# 分支甲:战斗胜利停点(结算后)→确认进选路→放弃→终局面
|
||
if not finished.done:
|
||
await _stage_faces(main, "V3d 结算后(胜利停点)", "alive", "alive", "both", {"mf_start": false, "end_open": true})
|
||
main._on_battle_end_continue()
|
||
for i in range(8):
|
||
await process_frame
|
||
await _stage_faces(main, "V3d2 选路页(停点后)", "alive", "alive", "both", {"mf_start": false, "level_vis": true})
|
||
check(main.run_scope.adventure.abandon(), "V3d2 放弃受理(选路停点天然非战斗期)〔装配生命批 V3〕")
|
||
for i in range(12):
|
||
if finished.done:
|
||
break
|
||
await process_frame
|
||
main._flush_row_queue()
|
||
for i in range(8):
|
||
if main._end_overlay.is_open():
|
||
break
|
||
await process_frame
|
||
# 阶段⑤终局面(败北/放弃皆达)
|
||
check(finished.done and main._end_overlay.is_open(),
|
||
"V3e 终局面到达(adventure_finished+终局面板开)〔装配生命批 V3〕")
|
||
await _stage_faces(main, "V3e 终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
|
||
# 阶段⑥回菜单收束(旧局消亡+新会话即驻+无真空窗)
|
||
var old_scope = main.run_scope
|
||
main._on_battle_end_continue()
|
||
check(main.run_scope != old_scope and main.run_scope.is_inside_tree(),
|
||
"V3f 收束换会话无真空窗(新闲时会话即刻在树)〔装配生命批 V3〕")
|
||
await process_frame
|
||
check(not is_instance_valid(old_scope),
|
||
"V3f 收束后旧局经 queue_free 消亡(is_instance_valid=false)〔装配生命批 V3〕")
|
||
await _stage_faces(main, "V3f 回菜单收束", "null", "null", "null",
|
||
{"mf_start": true, "start_vis": true, "end_open": false})
|
||
# 阶段⑦各模态开合后(菜单域四模态真实路径开合——会话面零扰动)
|
||
for modal_name in ["settings", "career", "camp", "debug"]:
|
||
if modal_name == "settings":
|
||
main._open_settings()
|
||
elif modal_name == "career":
|
||
main._open_career()
|
||
elif modal_name == "camp":
|
||
# 营地经进档开层(卡② mn9 新路径——campbtn 已撤);槽模板注入隔离命名空间
|
||
#(防碰真实槽 user://save_slot_N.json——真实档红线),营地闭合后恢复生命周期 attach 写盘目标
|
||
#(卡① sv3 无激活拒写——后续阶段断点写盘依赖)
|
||
sm.slots.attach_slot_template("user://smoke_lifecycle_slot_%d.json")
|
||
main._open_save_select()
|
||
for i in range(2):
|
||
await process_frame
|
||
var ssel_new: Button = main.save_select_overlay._new_btns.get(1)
|
||
if ssel_new == null:
|
||
check(false, "V3g 营地入口缺失(选档层新档钮驱动中断)〔装配生命批 V3〕")
|
||
continue
|
||
ssel_new.pressed.emit() # 创建=激活=暂行开营地(卡③ Q3 乙落点)
|
||
else:
|
||
main._show_debug_overlay()
|
||
for i in range(4):
|
||
await process_frame
|
||
var open_extra: Dictionary = {"mf_start": false}
|
||
if modal_name == "debug":
|
||
open_extra = {"mf_start": true} # 调试浮窗=浮于菜单之上的 Dialog(不隐藏开始层)
|
||
await _stage_faces(main, "V3g 模态开·%s" % modal_name, "null", "null", "null", open_extra)
|
||
if modal_name == "settings":
|
||
main._on_settings_closed()
|
||
elif modal_name == "career":
|
||
main._on_career_closed()
|
||
elif modal_name == "camp":
|
||
main._on_camp_closed()
|
||
else:
|
||
main._close_debug_overlay()
|
||
for i in range(4):
|
||
await process_frame
|
||
await _stage_faces(main, "V3g 模态合·%s" % modal_name, "null", "null", "null",
|
||
{"mf_start": true, "start_vis": true})
|
||
if modal_name == "camp":
|
||
main._on_save_select_closed() # 关闭被栈还原的选档层(动线序中间态——卡④ 换代点)
|
||
for i in range(2):
|
||
await process_frame
|
||
sm.slots.attach_path("user://smoke_lifecycle.json") # 恢复生命周期写盘目标
|
||
for suf in ["", ".tmp"]:
|
||
var slot_p: String = "user://smoke_lifecycle_slot_1.json" + suf
|
||
if FileAccess.file_exists(slot_p):
|
||
DirAccess.remove_absolute(slot_p)
|
||
# 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留)
|
||
var first_adv = main.run_scope.adventure
|
||
main.debug_overlay.seed_edit.text = "43"
|
||
main._start_adventure_from_menu()
|
||
for i in range(6):
|
||
await process_frame
|
||
check(main.run_scope.adventure != null and main.run_scope.adventure != first_adv,
|
||
"V3h 两连局第二会话在座(开局停点——新冒险会话非空)〔装配生命批 V3〕")
|
||
await _stage_faces(main, "V3h 两连局·开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true})
|
||
check(main.run_scope.adventure.abandon(), "V3h 放弃受理(第二局)〔装配生命批 V3〕")
|
||
for i in range(12):
|
||
if main._end_overlay.is_open():
|
||
break
|
||
await process_frame
|
||
main._flush_row_queue()
|
||
for i in range(8):
|
||
if main._end_overlay.is_open():
|
||
break
|
||
await process_frame
|
||
await _stage_faces(main, "V3h 两连局·终局面", "alive", "alive", "null", {"mf_start": false, "end_open": true})
|
||
main._on_battle_end_continue()
|
||
await process_frame
|
||
await _stage_faces(main, "V3h 两连局·收束", "null", "null", "null", {"mf_start": true, "start_vis": true})
|
||
# 阶段⑨续玩断点(fresh 隔离档+最小合法断点夹具——ui_struct resume 夹具同构;换会话无真空窗)
|
||
for p in ["user://smoke_lifecycle_resume.json", "user://smoke_lifecycle_resume.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p)
|
||
sm.slots.attach_path("user://smoke_lifecycle_resume.json")
|
||
sm.set_breakpoint({
|
||
"master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 0,
|
||
"level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [],
|
||
"keystone_drops": [], "bonus_taken": {},
|
||
"bonus_pending": {"candidates": [{"kind": "heal", "tier": "common"}], "payload": {"gp_dropped": 0}},
|
||
})
|
||
var idle_scope = main.run_scope
|
||
main._resume_from_menu()
|
||
check(main.run_scope != idle_scope and main.run_scope.is_inside_tree(),
|
||
"V3i 续玩换会话无真空窗(新冒险会话即刻在树)〔装配生命批 V3〕")
|
||
for i in range(10):
|
||
await process_frame
|
||
await _stage_faces(main, "V3i 续玩断点", "alive", "both", "both", {"mf_start": false})
|
||
main.queue_free()
|
||
await process_frame
|
||
for p in ["user://smoke_lifecycle.json", "user://smoke_lifecycle.json.tmp",
|
||
"user://smoke_lifecycle_resume.json", "user://smoke_lifecycle_resume.json.tmp"]:
|
||
if FileAccess.file_exists(p):
|
||
DirAccess.remove_absolute(p) |