diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index b39a522f..3c510c2a 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -88,6 +88,7 @@ func _split_tail() -> void: 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]) @@ -302,6 +303,282 @@ func _smoke_boot_chain() -> void: DirAccess.remove_absolute(p) +# ---- 生命周期存活性矩阵(迭代 28 T3——九阶段×五引用面;驱动复用 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(不隐藏开始层——迭代 18 卡⑦形态) + 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 "" + + # ---- 会话物化断言(迭代 27 T1 V3——两连局复位/无脏状态残留;原手工复位清单消亡的行为级证明)---- func _smoke_run_lifecycle() -> void: print("--- 迭代27 T1 会话物化(V3:两连局复位+脏状态不残留)---")