test(迁移): 批 B 波 1 G1 系统启动链尾段六套 49 条迁入 system_test.gd——剪切挂载绿+按条变异 47/49 验红(:279 迁移不变式与 :421 营地护栏记死断言候选照迁禁删);smoke_all.gd 同批删六函数三 helper 瘦壳留 G12;闸01剩余面1146绿+新宿主65绿+闸02四十套件绿
This commit is contained in:
1 parent
4a15fbf282
commit
43a7f5a1dd
3 files changed
+537
-500
No files matched your search
@@ -80,16 +80,8 @@ func _initialize() -> void:
|
||||
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
|
||||
_smoke_type_gate()
|
||||
_smoke_hygiene_gates()
|
||||
if autoload_ok:
|
||||
await _smoke_scene_split() # 协程函数必须 await(无 await=发射后不管,首个 await 后即被 quit 截断)
|
||||
await _smoke_boot_chain()
|
||||
await _smoke_run_lifecycle()
|
||||
await _smoke_lifecycle_matrix()
|
||||
else:
|
||||
check(false, "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)")
|
||||
print("=== 冒烟完成:%d 项检查,%d 项失败 ===" % [_checks, _failures])
|
||||
quit(1 if _failures > 0 else 0)
|
||||
|
||||
@@ -152,498 +144,6 @@ func _smoke_hygiene_gates() -> void:
|
||||
check(outside_calls == 0,
|
||||
"V1d 层序落位单口(_apply_layer_contract 之外 move_child 调用点=%d 应=0)〔装配生命批 V1〕" % outside_calls)
|
||||
|
||||
# ---- 三屏场景化断言(V2——独立实例化+关键节点在位+组合根树序/z 序保序)----
|
||||
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.lvbtn != null and menu.resumebtn != null and menu.campbtn != null and menu.careerbtn != null and menu.settingsbtn != null and menu.quitbtn != null,
|
||||
"V2 菜单屏壳独立实例化(start_overlay+操作组六钮在位)")
|
||||
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 链路时序后置条件(启动态六断言;只立行为可判断言,不造假时序)----
|
||||
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.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 再承接)
|
||||
check(not sm.has_breakpoint() and not main._resume_btn.visible,
|
||||
"V2d 续玩钮 boot 态恒隐(无激活态——启动态不隐式激活;断点在档=%s visible=%s)〔装配生命批 V2·卡① sv2 改写〕" % [
|
||||
str(sm.has_breakpoint()), str(main._resume_btn.visible)])
|
||||
# ④-1b 启动态三断言+旧单档迁移不变式(卡① sv2/sv12——真实 autoload boot 链)
|
||||
check(sm.active_slot() == 0 and not sm.has_write_target(),
|
||||
"V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕")
|
||||
check(sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0,
|
||||
"V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕")
|
||||
check(not sm.write_to_disk(),
|
||||
"V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕")
|
||||
check(not (FileAccess.file_exists("user://save.json") and not FileAccess.file_exists(sm.slot_path(1))),
|
||||
"V2-s4 旧单档迁移不变式(旧档在则槽 1 必在——Q1 甲一次性迁移已发生)〔卡① sv12〕")
|
||||
# ④-2 隔离新档双向(attach 原地重置——绝不碰真实档 user://save.json;刷新单口与 boot 路径同源)
|
||||
for p in ["user://smoke_boot_chain.json", "user://smoke_boot_chain.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_boot_chain.json")
|
||||
main._refresh_resume_button()
|
||||
check(not sm.has_breakpoint() and not main._resume_btn.visible,
|
||||
"V2d 续玩钮·无断点态隐藏(visible=%s has_breakpoint=%s)〔装配生命批 V2〕" % [
|
||||
str(main._resume_btn.visible), str(sm.has_breakpoint())])
|
||||
sm.set_breakpoint({"level_index": 2, "master_seed": 42})
|
||||
main._refresh_resume_button()
|
||||
check(sm.has_breakpoint() and main._resume_btn.visible,
|
||||
"V2d 续玩钮·有断点态可见(visible=%s has_breakpoint=%s)〔装配生命批 V2〕" % [
|
||||
str(main._resume_btn.visible), str(sm.has_breakpoint())])
|
||||
sm.clear_breakpoint()
|
||||
main._refresh_resume_button()
|
||||
check(not main._resume_btn.visible,
|
||||
"V2d 续玩钮·清断点后回隐藏(visible=%s)〔装配生命批 V2〕" % str(main._resume_btn.visible))
|
||||
# ⑤ 闲时会话(_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 main.debug_badge.visible,
|
||||
"V2f 调试浮层角标 boot 可见(全局浮层入口在座)〔2026-10-16 用户令〕")
|
||||
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)
|
||||
|
||||
|
||||
# ---- 生命周期存活性矩阵(九阶段×五引用面;驱动复用 V3 debug seed 开局路径+
|
||||
# _auto_play_round 活战斗模式+ui_struct resume 夹具模式,不另起点击链)----
|
||||
# 阶段映射说明:计划书十阶段中「开局停点」与「地图选路」在本现行流程同为关卡画面停点态
|
||||
#(level_started→choices_presented 同步到达,无可观测中间态)——合并为 V3b 一处断言。
|
||||
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.attach_path("user://smoke_lifecycle.json")
|
||||
main._refresh_resume_button() # 隔离后重同步(boot 时按钮态反映的是真实档——T2 已断 boot 关系,此处按隔离档定基)
|
||||
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, "resume_vis": false})
|
||||
# 阶段②开局停点(地图选路停点态)
|
||||
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)])
|
||||
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":
|
||||
var campbtn: Button = main._menu_screen.campbtn
|
||||
if campbtn == null:
|
||||
check(false, "V3g 营地入口缺失(驱动中断)〔装配生命批 V3〕")
|
||||
continue
|
||||
campbtn.pressed.emit()
|
||||
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})
|
||||
# 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留)
|
||||
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.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}},
|
||||
})
|
||||
main._refresh_resume_button()
|
||||
check(main._resume_btn.visible, "V3i 续玩钮·断点在档可见〔装配生命批 V3〕")
|
||||
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)
|
||||
|
||||
|
||||
# 引用面快照(五承载物的会话相关面+恒活面——逐个调用现值回调,与 run_scope 成员并采)
|
||||
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._meta_get.call(),
|
||||
"bonus_save": bo._save_get.call(),
|
||||
"bonus_adventure": bo._adventure_get.call(),
|
||||
"bonus_runner": bo._runner_get.call(),
|
||||
"bonus_mode": bo._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])
|
||||
"resume_vis":
|
||||
if main._resume_btn.visible != bool(extra[k]):
|
||||
bad += " _resume_btn.visible 应=%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 v != null and not is_instance_valid(v):
|
||||
return " %s 悬挂(失效对象)" % name
|
||||
return ""
|
||||
|
||||
|
||||
# ---- 会话物化断言(V3——两连局复位/无脏状态残留)----
|
||||
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.attach_path("user://smoke_run_lifecycle.json")
|
||||
sm.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)
|
||||
|
||||
|
||||
# ---- Core 集成冒烟(表加载成功才执行)----
|
||||
var _db
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
# 运行:python tools/run_system_test.py --script res://tests/system_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const AutoPlayer := preload("res://smoke/auto_player.gd") # 自动玩家策略单一实现(lifecycle 活战斗驱动;smoke 库件迁移期按现路径 preload)
|
||||
|
||||
var _failures: int = 0
|
||||
var _checks: int = 0
|
||||
|
||||
@@ -23,5 +25,521 @@ func check(cond: bool, label: String) -> void:
|
||||
|
||||
func _initialize() -> void:
|
||||
print("=== Dungeon Sweeper 系统测试(宿主薄壳——断言面待迁移批自 smoke_all.gd 迁入) ===")
|
||||
_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_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.lvbtn != null and menu.resumebtn != null and menu.campbtn != null and menu.careerbtn != null and menu.settingsbtn != null and menu.quitbtn != null,
|
||||
"V2 菜单屏壳独立实例化(start_overlay+操作组六钮在位)")
|
||||
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 _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.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 再承接)
|
||||
check(not sm.has_breakpoint() and not main._resume_btn.visible,
|
||||
"V2d 续玩钮 boot 态恒隐(无激活态——启动态不隐式激活;断点在档=%s visible=%s)〔装配生命批 V2·卡① sv2 改写〕" % [
|
||||
str(sm.has_breakpoint()), str(main._resume_btn.visible)])
|
||||
# ④-1b 启动态三断言+旧单档迁移不变式(卡① sv2/sv12——真实 autoload boot 链)
|
||||
check(sm.active_slot() == 0 and not sm.has_write_target(),
|
||||
"V2-s1 boot 后无激活态(active_slot=0、写盘目标空)〔卡① sv2〕")
|
||||
check(sm.keystone_balance() == 0 and not sm.has_breakpoint() and sm.adventure_total() == 0,
|
||||
"V2-s2 boot 内存=默认合法空态(钥石/断点/总局数全零值)〔卡① sv2〕")
|
||||
check(not sm.write_to_disk(),
|
||||
"V2-s3 boot 无激活写盘拒绝(write_to_disk=false 生产入口)〔卡① sv3〕")
|
||||
check(not (FileAccess.file_exists("user://save.json") and not FileAccess.file_exists(sm.slot_path(1))),
|
||||
"V2-s4 旧单档迁移不变式(旧档在则槽 1 必在——Q1 甲一次性迁移已发生)〔卡① sv12〕")
|
||||
# ④-2 隔离新档双向(attach 原地重置——绝不碰真实档 user://save.json;刷新单口与 boot 路径同源)
|
||||
for p in ["user://smoke_boot_chain.json", "user://smoke_boot_chain.json.tmp"]:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
sm.attach_path("user://smoke_boot_chain.json")
|
||||
main._refresh_resume_button()
|
||||
check(not sm.has_breakpoint() and not main._resume_btn.visible,
|
||||
"V2d 续玩钮·无断点态隐藏(visible=%s has_breakpoint=%s)〔装配生命批 V2〕" % [
|
||||
str(main._resume_btn.visible), str(sm.has_breakpoint())])
|
||||
sm.set_breakpoint({"level_index": 2, "master_seed": 42})
|
||||
main._refresh_resume_button()
|
||||
check(sm.has_breakpoint() and main._resume_btn.visible,
|
||||
"V2d 续玩钮·有断点态可见(visible=%s has_breakpoint=%s)〔装配生命批 V2〕" % [
|
||||
str(main._resume_btn.visible), str(sm.has_breakpoint())])
|
||||
sm.clear_breakpoint()
|
||||
main._refresh_resume_button()
|
||||
check(not main._resume_btn.visible,
|
||||
"V2d 续玩钮·清断点后回隐藏(visible=%s)〔装配生命批 V2〕" % str(main._resume_btn.visible))
|
||||
# ⑤ 闲时会话(_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 main.debug_badge.visible,
|
||||
"V2f 调试浮层角标 boot 可见(全局浮层入口在座)〔2026-10-16 用户令〕")
|
||||
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)
|
||||
|
||||
|
||||
# ---- 会话物化断言(V3——两连局复位/无脏状态残留)----
|
||||
# (自 game/smoke/smoke_all.gd 剪切迁入——label 与 cond 逐字原样,断言迁移批 B G1)
|
||||
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.attach_path("user://smoke_run_lifecycle.json")
|
||||
sm.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)
|
||||
|
||||
|
||||
# 引用面快照(五承载物的会话相关面+恒活面——逐个调用现值回调,与 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._meta_get.call(),
|
||||
"bonus_save": bo._save_get.call(),
|
||||
"bonus_adventure": bo._adventure_get.call(),
|
||||
"bonus_runner": bo._runner_get.call(),
|
||||
"bonus_mode": bo._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])
|
||||
"resume_vis":
|
||||
if main._resume_btn.visible != bool(extra[k]):
|
||||
bad += " _resume_btn.visible 应=%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 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.attach_path("user://smoke_lifecycle.json")
|
||||
main._refresh_resume_button() # 隔离后重同步(boot 时按钮态反映的是真实档——T2 已断 boot 关系,此处按隔离档定基)
|
||||
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, "resume_vis": false})
|
||||
# 阶段②开局停点(地图选路停点态)
|
||||
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)])
|
||||
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":
|
||||
var campbtn: Button = main._menu_screen.campbtn
|
||||
if campbtn == null:
|
||||
check(false, "V3g 营地入口缺失(驱动中断)〔装配生命批 V3〕")
|
||||
continue
|
||||
campbtn.pressed.emit()
|
||||
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})
|
||||
# 阶段⑧两连局(第二局全新会话——旧局对象不复用、脏态零残留)
|
||||
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.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}},
|
||||
})
|
||||
main._refresh_resume_button()
|
||||
check(main._resume_btn.visible, "V3i 续玩钮·断点在档可见〔装配生命批 V3〕")
|
||||
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)
|
||||
@@ -225,3 +225,22 @@
|
||||
8. **_smoke_type_glow**:全量迁单元 presentation,与现役 type_glow_test 同域共存;重复面记「另批去重候选」,**本批禁删任何断言**(验证集不缩小为绝对约束)。
|
||||
|
||||
附(普查新发现落位,波 2 执行):`game/presentation/tune_sim_controller.gd:27` 与 `unit/pacing_queue_test.gd:53` 的 `res://smoke/` preload 指向 autoplay 驱动件——两类件随波 2 入 `game/sim/`,preload 同步改指;`体验验证清单:83` 旧函数名一并清扫;合同必改点修正——PARITY 触面实位 :209(合同写 :206 漂 3 行),`sim-data/analyze.py` 零命中从必改点除名。
|
||||
|
||||
## 九、波 1 施工记录(逐组追加;G1 —— 2026-10-22 波 1 主刀代理)
|
||||
|
||||
**G1 系统·启动链尾段 → game/tests/system_test.gd:六套 49 条全部迁入,原子 commit 一笔。**
|
||||
挂载终态=`_split_tail` 剪切形态(autoload 检查+四协程套依序 await+摘要 quit);G12 两套调用(_smoke_type_gate/_smoke_hygiene_gates)不随迁。helpers `_topmost_visible_layer/_face_snapshot/_face_liveness` 随用套迁入;`AutoPlayer` 按现路径 `res://smoke/auto_player.gd` preload(账本 §五夹具前提)。smoke_all.gd 同批删六函数+三 helper,`_split_tail` 瘦壳只留 G12 两套调用+摘要 quit。
|
||||
|
||||
逐套变异记账(每条独立一轮:改坏→跑新宿主→红→`git checkout` 还原→`git status` 清洁确认):
|
||||
|
||||
- `_smoke_scene_split`(11 条):11/11 验红。变异点——:162 menu_screen.gd careerbtn 置 null(红,连带 V1c);:164 menu_screen.gd 尾加 start_overlay.visible=false(红唯一);:165 mouse_filter 改 STOP(红唯一);:170 camp_screen.gd camp_overlay 置 null(红,null 访问中断后续属预期形态);:171 camp_screen.gd 尾加 visible=true(红,连带 V1a);:176 adventure_screen.gd stage 置 null(红唯一);:187 main.gd:199 启动态 visible 翻 false(红唯一);:191 LAYER_CONTRACT 前两 shell 行对调(红,连带 V1a);:209 LAYER_CONTRACT legend_overlay key 改错名(红唯一);:211 main.gd:228 level_screen 句柄置 null(红;首试 board_view=null 因后续 set_identity_resolver 崩链过早、:211 未达,换点后成);:213 main.gd:197 闲时会话参数翻 true(红唯一)。
|
||||
- `_smoke_boot_chain`(15 条):14/15 验红。变异点——:250 config_db.gd:139 hero 置 null(红,连带 V1c);:255 settings_store.gd 音量 db 偏移 +1.0(红唯一);:259 motion_scale 两支对调(红唯一);:263 main.gd:199 翻 false(红,连带 V1a);:269 _refresh_resume_button has 恒 true(红,连带 :287/:297);:273 save_manager.gd:83 active_slot() 访问器恒返 1(红唯一);:275 keystone_balance() 偏移 +3(红唯一);:277 write_to_disk 无激活拒绝分支返 true(红唯一);:287 `_resume_btn.visible = not has`(红,连带 3 条同面);:292 恒 false(红唯一);:297 clear_breakpoint 改 pass(红唯一);:300 main.gd:197 翻 true(红,连带 :213);:304 _ready 尾插 debug_overlay.visible=true(红,连带 V1a);:306 badge 恒 false(红唯一)。
|
||||
- **:279 死断言候选(唯一一条,照迁禁删)**:「V2-s4 旧单档迁移不变式」——本机磁盘态已固化(save.json 与 save_slot_1.json 均在档=迁移已完成),cond 的可红面(旧档在而槽 1 缺)在生产码变异下不可达;三形态结论:迁移调用行注释=cond 恒真不红;slot_path 派生改坏会触发 _ready 真迁移副作用(碰真实档,红线禁止);迁移停摆+槽派生两处组合=save_manager 内部消费链崩、断言不可达。护栏语义在 fresh 环境有效(旧档存在且未迁时必红),本机环境无法复现该态。
|
||||
- `_smoke_run_lifecycle`(11 条):11/11 验红。变异点——:608/:638 main.gd:607(_start_adventure 内 `_begin_session(true)`)翻 false 两独立轮(各红);:610 run_scope.gd hints_shown 预置(红,连带 :630/:638);:619 adventure_runner.gd:830 abandon 返 false(红唯一);:624 :970 emit 注释(红,连带 :625);:625 battle_end_overlay.gd:165 is_open 恒 false(红唯一;首试注释 main.gd:700 else 体致 Parse Error 不算红,换点);:628 main.gd:727 收束会话翻 true(红唯一);:630 run_scope.gd boss_active 预置 true(红唯一);:632 main.gd:729 翻 false(红唯一);:634 adventure_runner.gd:967 clear_breakpoint 注释(红,连带 :638/:640);:640 main.gd:499 翻 true(红唯一)。
|
||||
- `_stage_faces`(1 条):1/1 验红。变异点——main.gd:383 `start_visible_get` 回调取反(红唯一,tag「V3a 闲时」)。
|
||||
- `_smoke_lifecycle_matrix`(10 条):9/10 验红。变异点——:374 battle_orchestrator.gd:1224 is_battle_live 恒真(驱动跑满 guard 600 红唯一);:388/:453 abandon 返 false 两独立轮(各红);:399 :970 emit 注释(红,连带 4 条);:405 main.gd:727 收束会话注释(红,连带多条);:408 _begin_session 的 queue_free 改 pass(红唯一;注释形态 Parse Error 不算红,换 pass);:421 **死断言候选(照迁禁删)**——menu_screen.gd campbtn 置 null 后 boot 的信号接线先崩(SCRIPT ERROR 中断 _build_ui),:421 护栏在执行序上被遮断不可达;其拦截语义由 boot 崩与前置断言代偿;:450 断点残留形态(:967 clear_breakpoint 注释,第二局走确认框分支不开局→adventure null)验红(首试 wire 断链因 run_lifecycle 先中断不可达,换形态);:479 `_resume_btn.visible` 恒 false(红,连带 :292);:482 main.gd:550 resume 不换会话(红唯一)。
|
||||
- `_split_tail`(1 条):1/1 验红。变异点——project.godot SaveManager autoload 行注释(走 else 分支,红唯一,1 项检查 1 失败)。
|
||||
|
||||
窄复验:新宿主面 65 项检查 0 失败 PASS(运行计数 65>静态 49,因 _stage_faces 每阶段调用各计一次——smoke 原生特性);闸 01 剩余面(smoke_all.gd)1146 项检查 0 失败 PASS;闸 02(run_all.gd)40 套件 all green。标签逐字抽查 6 条与冻结清单全对齐;新宿主 check( 调用计数 49=账本条数。
|
||||
|
||||
施工发现(供后续组参考):① Git Bash sed 的 `a\` 追加与含块结构注释变异易产 Parse Error(不算红),整行 `pass`/python 插入替代;② save_manager.gd `_init` 内对成员的赋值会被成员初始化器时序覆盖(probe 实证 active_slot 恒 0),变异请选访问器面;③ main.gd `_begin_session(true)` 有两处(_resume_from_menu:550、_start_adventure:607),awk 首匹配会错定位,变异前必核行号上下文。
|
||||
Reference in new issue
Block a user