Files
tekton/scripts/managers/lobby_manager.gd
T
2026-01-28 02:29:43 +08:00

449 lines
14 KiB
GDScript

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 = true # 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] = ["Bob", "Gatot", "Masbro", "Oldpop"]
var available_areas: Array[String] = ["Desert", "Forest", "City", "Factory"]
var selected_area: String = "Desert" # Host-controlled
var local_character_index: int = 0 # Local player's character index
# 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])
@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")
# =============================================================================
# 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)
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)
# 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
enable_cycle_timer = false