extends Node # LobbyManager - Manages room/lobby state across scenes # Signals signal room_list_updated(rooms: Array) signal room_joined(room_data: Dictionary) signal room_left() signal player_joined(player_data: Dictionary) signal player_left(player_id: int) signal ready_state_changed(player_id: int, is_ready: bool) signal all_players_ready() signal game_starting() signal match_duration_changed(duration_seconds: int) signal randomize_spawn_changed(enabled: bool) signal character_changed(player_id: int, character_name: String) signal area_changed(area_name: String) signal player_list_changed() # Room data structure var current_room: Dictionary = {} var players_in_room: Array = [] # [{id, name, is_ready}] var available_rooms: Array = [] var is_host: bool = false var local_player_name: String = "Player" # Match duration in seconds (configurable in lobby by host) var match_duration: int = 180 # Default 3 minutes # Randomize spawn locations (configurable in lobby by host) var randomize_spawn: bool = false # Default enabled # Timer setting var enable_cycle_timer: bool = false # Default disabled signal enable_cycle_timer_changed(enabled: bool) # Scarcity setting var scarcity_mode: String = "Normal" # Normal, Aggressive, Chaos signal scarcity_mode_changed(mode: String) # Character and area selection var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"] var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"] var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Tekton Doors"] var selected_area: String = "Desert" # Host-controlled var game_mode: String = "Freemode" # Host-controlled var local_character_index: int = 0 # Local player's character index # Signals signal game_mode_changed(mode: String) # Ready to start game check var _all_ready: bool = false func _ready(): # Connect to Nakama signals NakamaManager.match_joined.connect(_on_match_joined) multiplayer.peer_connected.connect(_on_peer_connected) multiplayer.peer_disconnected.connect(_on_peer_disconnected) # ============================================================================= # Room Creation / Joining # ============================================================================= func create_room(room_name: String) -> void: """Host creates a new room with the given name.""" is_host = true current_room = { "room_name": room_name, "host_name": local_player_name, "max_players": GameStateManager.max_players } # Connect to Nakama and create match var success = await NakamaManager.connect_to_nakama_async() if not success: push_error("Failed to connect to Nakama") return NakamaManager.host_game() func join_room(match_id: String) -> void: """Client joins an existing room by match ID.""" is_host = false var success = await NakamaManager.connect_to_nakama_async() if not success: push_error("Failed to connect to Nakama") return NakamaManager.join_game(match_id) func leave_room() -> void: """Leave the current room.""" current_room = {} players_in_room.clear() is_host = false _all_ready = false # Disconnect from Nakama match if NakamaManager.socket: NakamaManager.socket.close() emit_signal("room_left") func refresh_room_list() -> void: """Request updated room list from Nakama.""" if not NakamaManager.is_connected_to_nakama(): var success = await NakamaManager.connect_to_nakama_async() if not success: return var rooms = await NakamaManager.list_matches_async() available_rooms = rooms emit_signal("room_list_updated", rooms) # ============================================================================= # Ready State Management # ============================================================================= func set_ready(is_ready: bool) -> void: """Set local player's ready state.""" var my_id = multiplayer.get_unique_id() # Update local state for player in players_in_room: if player["id"] == my_id: player["is_ready"] = is_ready break # Sync to all peers rpc("sync_ready_state", my_id, is_ready) @rpc("any_peer", "call_local", "reliable") func sync_ready_state(player_id: int, is_ready: bool) -> void: """Sync ready state across all clients.""" for player in players_in_room: if player["id"] == player_id: player["is_ready"] = is_ready break emit_signal("ready_state_changed", player_id, is_ready) _check_all_ready() func _check_all_ready() -> void: """Check if all players are ready.""" if players_in_room.size() < 2: _all_ready = false return for player in players_in_room: if not player["is_ready"]: _all_ready = false return _all_ready = true emit_signal("all_players_ready") func is_all_ready() -> bool: return _all_ready # ============================================================================= # Game Start # ============================================================================= func set_match_duration(duration_seconds: int) -> void: """Host sets match duration. Syncs to all clients.""" match_duration = duration_seconds if is_host: rpc("sync_match_duration", duration_seconds) @rpc("authority", "call_local", "reliable") func sync_match_duration(duration_seconds: int) -> void: """Sync match duration from host to clients.""" match_duration = duration_seconds emit_signal("match_duration_changed", duration_seconds) func get_match_duration() -> int: return match_duration func set_randomize_spawn(enabled: bool) -> void: """Host sets randomize spawn. Syncs to all clients.""" randomize_spawn = enabled if is_host: rpc("sync_randomize_spawn", enabled) @rpc("authority", "call_local", "reliable") func sync_randomize_spawn(enabled: bool) -> void: """Sync randomize spawn from host to clients.""" randomize_spawn = enabled emit_signal("randomize_spawn_changed", enabled) func get_randomize_spawn() -> bool: return randomize_spawn # ============================================================================= # Timer Setting # ============================================================================= func set_enable_cycle_timer(enabled: bool) -> void: """Host sets enable cycle timer. Syncs to all clients.""" enable_cycle_timer = enabled if is_host: rpc("sync_enable_cycle_timer", enabled) @rpc("authority", "call_local", "reliable") func sync_enable_cycle_timer(enabled: bool) -> void: """Sync enable cycle timer from host to clients.""" enable_cycle_timer = enabled emit_signal("enable_cycle_timer_changed", enabled) func get_enable_cycle_timer() -> bool: return enable_cycle_timer # ============================================================================= # Scarcity Mode # ============================================================================= func set_scarcity_mode(mode: String) -> void: """Host sets scarcity mode. Syncs to clients.""" scarcity_mode = mode if is_host: rpc("sync_scarcity_mode", mode) @rpc("authority", "call_local", "reliable") func sync_scarcity_mode(mode: String) -> void: scarcity_mode = mode # Update ScarcityModel immediately ScarcityModel.set_mode(mode) emit_signal("scarcity_mode_changed", mode) func get_scarcity_mode() -> String: return scarcity_mode # ============================================================================= # Character Selection # ============================================================================= func get_local_character() -> String: """Get the local player's current character name.""" return available_characters[local_character_index] func set_character(character_name: String) -> void: """Set local player's character. Syncs to all peers.""" var idx = available_characters.find(character_name) if idx == -1: push_error("Invalid character: " + character_name) return local_character_index = idx var my_id = multiplayer.get_unique_id() # Update local player data for player in players_in_room: if player["id"] == my_id: player["character"] = character_name break # Sync to all peers rpc("sync_character", my_id, character_name) func cycle_character(direction: int) -> void: """Cycle through available characters. direction: -1 for left, +1 for right.""" local_character_index = wrapi(local_character_index + direction, 0, available_characters.size()) set_character(available_characters[local_character_index]) func select_random_character() -> void: """Select a random character (excluding 'Randomized' option itself).""" # Pick from indices 0-3 (Copper, Dabro, Gatot, Pip) var random_idx = randi() % 4 local_character_index = random_idx set_character(available_characters[local_character_index]) @rpc("any_peer", "call_local", "reliable") func sync_character(player_id: int, character_name: String) -> void: """Sync character selection across all clients.""" for player in players_in_room: if player["id"] == player_id: player["character"] = character_name break emit_signal("character_changed", player_id, character_name) emit_signal("player_list_changed") # ============================================================================= # Player Name Management # ============================================================================= func set_player_name(new_name: String) -> void: """Set local player's name. Syncs to all peers.""" local_player_name = new_name var my_id = multiplayer.get_unique_id() # Update local player data for player in players_in_room: if player["id"] == my_id: player["name"] = new_name break # Sync to all peers if connected if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: rpc("sync_player_name", my_id, new_name) emit_signal("player_list_changed") @rpc("any_peer", "call_local", "reliable") func sync_player_name(player_id: int, new_name: String) -> void: """Sync player name across all clients.""" for player in players_in_room: if player["id"] == player_id: player["name"] = new_name break emit_signal("player_list_changed") # ============================================================================= # Area Selection (Host Only) # ============================================================================= func get_selected_area() -> String: return selected_area func get_area_index() -> int: return available_areas.find(selected_area) func set_area(area_name: String) -> void: """Host sets the game area. Syncs to all clients.""" if not is_host: push_warning("Only host can change area") return if area_name not in available_areas: push_error("Invalid area: " + area_name) return selected_area = area_name rpc("sync_area", area_name) func cycle_area(direction: int) -> void: """Host cycles through available areas. direction: -1 for left, +1 for right.""" if not is_host: return var current_idx = available_areas.find(selected_area) var new_idx = wrapi(current_idx + direction, 0, available_areas.size()) set_area(available_areas[new_idx]) @rpc("authority", "call_local", "reliable") func sync_area(area_name: String) -> void: """Sync area selection from host to clients.""" selected_area = area_name emit_signal("area_changed", area_name) # ============================================================================= # Game Mode Selection (Host Only) # ============================================================================= func set_game_mode(mode: String) -> void: """Host sets the game mode. Syncs to all clients.""" if not is_host: push_warning("Only host can change game mode") return if mode not in available_game_modes: push_error("Invalid game mode: " + mode) return game_mode = mode rpc("sync_game_mode", mode) @rpc("authority", "call_local", "reliable") func sync_game_mode(mode: String) -> void: """Sync game mode selection from host to clients.""" game_mode = mode emit_signal("game_mode_changed", mode) func start_game() -> void: """Host triggers game start (transitions all players to main.tscn).""" if not is_host: push_error("Only host can start the game") return if not _all_ready: push_error("Not all players are ready") return # Sync match duration to all clients before starting rpc("sync_match_duration", match_duration) # Sync timer setting rpc("sync_enable_cycle_timer", enable_cycle_timer) # Sync scarcity mode rpc("sync_scarcity_mode", scarcity_mode) # Sync game mode rpc("sync_game_mode", game_mode) # Notify all clients to start rpc("_on_game_starting") @rpc("call_local", "reliable") func _on_game_starting() -> void: """Called on all clients when game is starting.""" emit_signal("game_starting") # Scene change will be handled by lobby.gd after receiving this signal # ============================================================================= # Player Management # ============================================================================= func _on_match_joined(match_id: String) -> void: """Called when successfully joined a Nakama match.""" current_room["match_id"] = match_id # Use first 8 chars of match ID as room name (matches server browser) var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id current_room["room_name"] = short_id # Add self to player list var my_id = multiplayer.get_unique_id() var my_data = { "id": my_id, "name": local_player_name, "is_ready": false, "character": available_characters[local_character_index] } players_in_room.append(my_data) if is_host: # Host is automatically in the room emit_signal("room_joined", current_room) # Client will request room info when peer connection is established @rpc("any_peer", "reliable") func request_room_info(requester_id: int, requester_name: String, requester_character: String) -> void: """Client requests room info from host, sending their name and character.""" if not multiplayer.is_server(): return # Update the player's name and character in the list for player in players_in_room: if player["id"] == requester_id: player["name"] = requester_name player["character"] = requester_character break # Send room data to requester rpc_id(requester_id, "receive_room_info", current_room, players_in_room) # Also sync updated player list to all other clients rpc("sync_player_list", players_in_room) emit_signal("player_list_changed") @rpc("reliable") func receive_room_info(room_data: Dictionary, player_list: Array) -> void: """Client receives room info from host.""" current_room = room_data players_in_room = player_list emit_signal("room_joined", current_room) func _on_peer_connected(peer_id: int) -> void: """Called when new peer connects.""" print("Peer connected: ", peer_id) if multiplayer.is_server(): # Host: add new player and sync list var new_player = { "id": peer_id, "name": "Player %d" % peer_id, "is_ready": false, "character": available_characters[0] } players_in_room.append(new_player) # Sync player list to all clients rpc("sync_player_list", players_in_room) emit_signal("player_joined", new_player) else: # Client: if we connected to the host (peer_id 1), request room info if peer_id == 1 and not is_host: # Wait a frame to ensure connection is stable await get_tree().process_frame # Send our actual name and character to the host rpc_id(1, "request_room_info", multiplayer.get_unique_id(), local_player_name, available_characters[local_character_index]) func _on_peer_disconnected(peer_id: int) -> void: """Called when peer disconnects.""" for i in range(players_in_room.size()): if players_in_room[i]["id"] == peer_id: players_in_room.remove_at(i) break if multiplayer.is_server(): rpc("sync_player_list", players_in_room) emit_signal("player_left", peer_id) _check_all_ready() @rpc("reliable") func sync_player_list(player_list: Array) -> void: """Sync player list from host to all clients.""" players_in_room = player_list func get_players() -> Array: return players_in_room func get_room_name() -> String: return current_room.get("room_name", "Unknown Room") func reset() -> void: """Reset lobby state.""" current_room = {} players_in_room.clear() available_rooms.clear() is_host = false _all_ready = false match_duration = 180 # Reset to default 3 minutes selected_area = "Desert" local_character_index = 0 # Default to "Copper" enable_cycle_timer = false game_mode = "Freemode"