extends Control # Lobby (task 1.5): roster list split by team, team swap, ready toggle, # leave. Reads/writes MatchNet.roster — this scene owns no state of its # own, it's a view over the autoload. Reached via main_menu.gd's Host/Join # flow (task 1.7) calling change_scene_to_file("res://scenes/lobby.tscn") # after NetworkManager.host()/join() succeeds — this scene must always be # loaded that way (as the real current_scene), not instantiated as a child # of something else: change_scene_to_file() operates on # get_tree().current_scene, and _on_disconnected_from_server()/_leave() # below call it themselves, which hangs if this scene isn't actually the # tree's current_scene when that happens (see multiplayer-next.md §9 # gotcha 27 — found the hard way while building tests/lobby_smoke.gd). @onready var _status_label: Label = %StatusLabel @onready var _team0_list: VBoxContainer = %Team0List @onready var _team1_list: VBoxContainer = %Team1List @onready var _controls_row: HBoxContainer = %ControlsRow @onready var _switch_team_button: Button = %SwitchTeamButton @onready var _ready_button: CheckButton = %ReadyButton @onready var _leave_button: Button = %LeaveButton var _planned_server_shutdown := false func _ready() -> void: MatchNet.welcomed.connect(_on_welcomed) MatchNet.player_joined.connect(_on_roster_changed) MatchNet.player_left.connect(_on_roster_changed) MatchNet.player_state_changed.connect(_on_roster_changed) MatchNet.rejected.connect(_on_rejected) MatchNet.server_shutdown.connect(_on_server_shutdown) NetworkManager.disconnected_from_server.connect(_on_disconnected_from_server) # The server process is never a roster member (§1.1 decision 2) — it # gets a read-only view, no team/ready controls to operate on itself. _controls_row.visible = NetworkManager.is_client _refresh() if not MatchNet.last_server_shutdown_reason.is_empty(): _planned_server_shutdown = true _status_label.text = "Server closed: %s" % MatchNet.last_server_shutdown_reason func _process(_delta: float) -> void: NetworkManager.poll() func _physics_process(_delta: float) -> void: NetworkManager.poll() func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): _leave() func _on_roster_changed(_a = null, _b = null, _c = null) -> void: _refresh() func _on_welcomed() -> void: _refresh() func _on_rejected(reason: String) -> void: _status_label.text = "Connection rejected: %s" % reason func _on_server_shutdown(reason: String) -> void: _planned_server_shutdown = true _status_label.text = "Server closed: %s" % reason func _on_disconnected_from_server() -> void: if _planned_server_shutdown: return get_tree().change_scene_to_file(ScenePaths.MAIN_MENU) func _on_switch_team_pressed() -> void: var my_id := multiplayer.get_unique_id() var info: MatchNet.PlayerInfo = MatchNet.roster.get(my_id) if info == null: return MatchNet.request_set_team((info.team + 1) % MatchNet.TEAM_COUNT) func _on_ready_toggled(pressed: bool) -> void: MatchNet.request_set_ready(pressed) func _on_leave_pressed() -> void: _leave() func _leave() -> void: NetworkManager.shutdown() get_tree().change_scene_to_file(ScenePaths.MAIN_MENU) func _refresh() -> void: if NetworkManager.is_server: _status_label.text = "Hosting — %d player(s) connected" % MatchNet.roster.size() elif NetworkManager.is_client: _status_label.text = "Connected" if not MatchNet.roster.is_empty() else "Connecting..." else: _status_label.text = "Not connected" for child in _team0_list.get_children(): child.queue_free() for child in _team1_list.get_children(): child.queue_free() var my_id := multiplayer.get_unique_id() var infos: Array = MatchNet.roster.values() infos.sort_custom(func(a: MatchNet.PlayerInfo, b: MatchNet.PlayerInfo) -> bool: return a.peer_id < b.peer_id) for info: MatchNet.PlayerInfo in infos: var row := Label.new() var marker := " (you)" if info.peer_id == my_id else "" var ready_mark := "✓" if info.ready else "…" row.text = "%s %s%s" % [ready_mark, info.player_name, marker] var target_list := _team0_list if info.team == 0 else _team1_list target_list.add_child(row) if NetworkManager.is_client: var my_info: MatchNet.PlayerInfo = MatchNet.roster.get(my_id) if my_info != null: _ready_button.set_pressed_no_signal(my_info.ready)