# app_flow_lifecycle_test —— §9.3 生命周期单一属主回归。 # AppFlow 跨登录 / 选人 / 游戏持有唯一 AppLifecycle;GameScene 不重复创建。 extends SceneTree const AppFlow = preload("res://app_flow.gd") class FakeClient extends Node: signal phase_changed(phase: String) signal disconnected(reason: String) signal char_list(characters: Array) signal char_name_changed(pid: int, name: String) signal entered_game() signal login_failed(reason: String) signal char_created(slot: int) signal char_create_failed(reason: int) signal char_deleted(slot: int) signal char_delete_failed() var selects := [] var reconnects := 0 func select_character(index: int) -> bool: selects.append(index) return true func reconnect() -> bool: reconnects += 1 return true var _fail := 0 func _ck(ok: bool, message: String) -> void: if not ok: _fail += 1 printerr("FAIL: " + message) func _init() -> void: var client := FakeClient.new() var flow := AppFlow.new() flow.build_game_scene = false get_root().add_child(flow) flow.start("", client) await process_frame var lifecycle_nodes: Array[Node] = [] for child in flow.get_children(): if child.name == "AppLifecycle": lifecycle_nodes.append(child) _ck(lifecycle_nodes.size() == 1, "AppFlow owns exactly one AppLifecycle") _ck(flow.get_node_or_null("AppLifecycle") != null, "AppLifecycle survives login screen construction") _ck(client.get_parent() == flow, "AppFlow keeps the same client beside lifecycle coordinator") var login_snapshot: Dictionary = flow.get_playable_snapshot() _ck(login_snapshot.get("stage", "") == "LOGIN" and not login_snapshot.get("scene_ready", true), "playable snapshot is a read-only LOGIN summary") # A live game keeps its scene during a transient disconnect. ReconnectUI # owns the retry while the next character list silently re-enters the last # selected slot instead of flashing the character-select page. flow._game = Node.new() flow.add_child(flow._game) flow._state = AppFlow.GAME flow._selected_char_slot = 2 client.disconnected.emit("wifi lost") _ck(flow.state() == AppFlow.GAME and flow._reconnecting_game, "game disconnect enters reconnecting state without leaving GAME") _ck(flow._reconnect != null and flow._reconnect.is_showing(), "game disconnect shows reconnect overlay") client.char_list.emit([{"index": 2, "name": "Hero"}]) _ck(client.selects == [2], "reconnect re-enters remembered character slot") client.entered_game.emit() _ck(flow.state() == AppFlow.GAME and not flow._reconnecting_game and not flow._reconnect.is_showing(), "successful reconnect restores GAME and hides overlay") var game_snapshot: Dictionary = flow.get_playable_snapshot() _ck(game_snapshot.get("stage", "") == "GAME" and not game_snapshot.get("reconnecting", true), "playable snapshot reports GAME after reconnect") # Auto-login with an explicit MT_CHAR_SLOT must never fall back to another # character, and the chosen slot must be remembered for a later reconnect. flow._state = AppFlow.LOGIN flow.auto_login = true flow._auto_char_slot = 5 flow._selected_char_slot = -1 client.selects.clear() client.char_list.emit([{"index": 0, "name": "Other"}]) _ck(client.selects.is_empty(), "absent explicit slot does not enter chars[0]") flow._state = AppFlow.LOGIN client.char_list.emit([{"index": 0, "name": "Other"}, {"index": 5, "name": "Fixture"}]) _ck(client.selects == [5] and flow._selected_char_slot == 5, "explicit slot is entered and remembered for reconnect") flow.queue_free() await process_frame if _fail == 0: print("PASS: app_flow_lifecycle_test (single lifecycle owner)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1)