diff --git a/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd b/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd index 05fc5ae..0067e53 100644 --- a/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd +++ b/addons/com.heroiclabs.nakama/api/NakamaRTAPI.gd @@ -644,9 +644,7 @@ class UserPresence extends NakamaAsyncResult: return NakamaSerializer.serialize(self) func _to_string(): - if is_exception(): return get_exception()._to_string() - return "UserPresence" % [ - persistence, session_id, status, username, user_id] + return "UserPresence" static func create(p_ns : GDScript, p_dict : Dictionary) -> UserPresence: return _safe_ret(NakamaSerializer.deserialize(p_ns, "UserPresence", p_dict), UserPresence) as UserPresence diff --git a/addons/com.heroiclabs.nakama/utils/NakamaSerializer.gd b/addons/com.heroiclabs.nakama/utils/NakamaSerializer.gd index cc8f284..cb47a44 100644 --- a/addons/com.heroiclabs.nakama/utils/NakamaSerializer.gd +++ b/addons/com.heroiclabs.nakama/utils/NakamaSerializer.gd @@ -1,7 +1,20 @@ extends RefCounted class_name NakamaSerializer -static func serialize(p_obj : Object) -> Dictionary: +static var _global_depth := 0 + +static func serialize(p_obj : Object, p_depth : int = 0) -> Dictionary: + _global_depth += 1 + if _global_depth > 64: + _global_depth -= 1 + printerr("NakamaSerializer: Global recursion limit reached!") + return {} + + var result = _serialize_impl(p_obj, p_depth) + _global_depth -= 1 + return result + +static func _serialize_impl(p_obj : Object, p_depth : int) -> Dictionary: var out = {} var schema = p_obj.get("_SCHEMA") if schema == null: @@ -18,13 +31,13 @@ static func serialize(p_obj : Object) -> Dictionary: var val_type = typeof(val) match val_type: TYPE_OBJECT: # Simple objects - out[k] = serialize(val) + out[k] = serialize(val, p_depth + 1) TYPE_ARRAY: # Array of objects var arr = [] for e in val: if typeof(e) != TYPE_OBJECT: continue - arr.append(serialize(e)) + arr.append(serialize(e, p_depth + 1)) out[k] = arr TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_STRING_ARRAY: # Array of ints, bools, or strings var arr = [] @@ -41,7 +54,7 @@ static func serialize(p_obj : Object) -> Dictionary: for l in val: if typeof(val[l]) != TYPE_OBJECT: continue - dict[l] = serialize(val[l]) + dict[l] = serialize(val[l], p_depth + 1) else: # Map of simple types for l in val: var e = val[l] diff --git a/scenes/lobby.gd b/scenes/lobby.gd index f1b31a0..2d8ae09 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -166,6 +166,9 @@ func _ready(): NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama) NakamaManager.connection_failed.connect(_on_connection_failed) + # Connect UserProfileManager signals + UserProfileManager.profile_updated.connect(_on_profile_updated) + # Show main menu initially _show_panel("main_menu") @@ -487,6 +490,17 @@ func _on_connection_failed(error_message: String) -> void: connection_status.text = "Connection failed: %s" % error_message _show_panel("main_menu") +func _on_profile_updated() -> void: + """Handle profile updates (name/avatar change).""" + var new_name = UserProfileManager.get_display_name() + + # Update input if visible + if player_name_input: + player_name_input.text = new_name + + # Sync to LobbyManager if we are in a room or just locally + LobbyManager.set_player_name(new_name) + # ============================================================================= # Player Slot Updates # ============================================================================= diff --git a/scenes/lobby.tscn b/scenes/lobby.tscn index 00fd090..f94962c 100644 --- a/scenes/lobby.tscn +++ b/scenes/lobby.tscn @@ -394,8 +394,8 @@ custom_minimum_size = Vector2(90, 28) layout_mode = 2 theme_override_fonts/font = ExtResource("5_pc087") theme_override_font_sizes/font_size = 11 -item_count = 3 selected = 0 +item_count = 3 popup/item_0/text = "Normal" popup/item_0/id = 0 popup/item_1/text = "Scarce" diff --git a/scenes/main.gd b/scenes/main.gd index 895a96f..2124650 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -370,6 +370,9 @@ func _setup_host_game(): if touch_controls: touch_controls.set_player(player_character) + # Set host name + player_character.display_name = LobbyManager.local_player_name + # Spawn client players that joined via lobby (need to add them first) var lobby_players = LobbyManager.get_players() for lobby_player in lobby_players: @@ -429,6 +432,13 @@ func _spawn_lobby_client_sync(peer_id: int): player_character.add_to_group("Players", true) GameStateManager.add_player(peer_id) + # Set name from LobbyManager data if available + var lobby_players = LobbyManager.get_players() + for p_data in lobby_players: + if p_data.get("id") == peer_id: + player_character.display_name = p_data.get("name", "Player") + break + # Tell all clients to create this player rpc("add_newly_connected_player_character", peer_id) @@ -529,43 +539,93 @@ func _start_game(): ui_manager.initialize_leaderboard_with_players(all_players) func _assign_random_spawn_positions(): - """Assign random unique spawn positions to all players.""" - # Fetch all valid walkable positions from the generated map - var valid_spawns = [] + """Assign spawn positions distributed to 4 corners (2 per corner for 8 players).""" var enhanced_gridmap = $EnhancedGridMap - - if enhanced_gridmap: - for x in range(enhanced_gridmap.columns): - for z in range(enhanced_gridmap.rows): - var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z)) - if ground == 0: # Walkable - valid_spawns.append(Vector2i(x, z)) - - # Fallback if map generation failed or is empty - if valid_spawns.size() < 12: - print("Warning: Low spawn count! Adding defaults.") - for i in range(12): - if not Vector2i(0, i) in valid_spawns: - valid_spawns.append(Vector2i(0, i)) + if not enhanced_gridmap: + return - # Shuffle spawn locations - valid_spawns.shuffle() + # Lists for each quadrant + var spawns_TL = [] # Top-Left + var spawns_TR = [] # Top-Right + var spawns_BL = [] # Bottom-Left + var spawns_BR = [] # Bottom-Right + var all_spawns = [] # Fallback + + var mid_x = enhanced_gridmap.columns / 2 + var mid_z = enhanced_gridmap.rows / 2 + + for x in range(enhanced_gridmap.columns): + for z in range(enhanced_gridmap.rows): + var ground = enhanced_gridmap.get_cell_item(Vector3i(x, 0, z)) + if ground == 0: # Walkable + var pos = Vector2i(x, z) + all_spawns.append(pos) + + if x < mid_x and z < mid_z: + spawns_TL.append(pos) + elif x >= mid_x and z < mid_z: + spawns_TR.append(pos) + elif x < mid_x and z >= mid_z: + spawns_BL.append(pos) + else: + spawns_BR.append(pos) + + # Sort lists by distance to corners (closest to corner should be last, to be popped first) + # TL: Close to (0,0) -> Sort descending distance (so closest is at end) + spawns_TL.sort_custom(func(a, b): return a.length_squared() > b.length_squared()) + + # TR: Close to (13, 0) + var tr_corner = Vector2i(enhanced_gridmap.columns - 1, 0) + spawns_TR.sort_custom(func(a, b): return a.distance_squared_to(tr_corner) > b.distance_squared_to(tr_corner)) + + # BL: Close to (0, 13) + var bl_corner = Vector2i(0, enhanced_gridmap.rows - 1) + spawns_BL.sort_custom(func(a, b): return a.distance_squared_to(bl_corner) > b.distance_squared_to(bl_corner)) + + # BR: Close to (13, 13) + var br_corner = Vector2i(enhanced_gridmap.columns - 1, enhanced_gridmap.rows - 1) + spawns_BR.sort_custom(func(a, b): return a.distance_squared_to(br_corner) > b.distance_squared_to(br_corner)) + + # Fallback shuffle + all_spawns.shuffle() # Get all players var all_players = get_tree().get_nodes_in_group("Players") - - # Assign positions var spawn_index = 0 + + # Round-robin assignment to corners: TL, TR, BR, BL, TL, TR, BR, BL... + # Order: TL -> TR -> BR -> BL (Clockwise-ish) + var quadrants = [spawns_TL, spawns_TR, spawns_BR, spawns_BL] + for player in all_players: - if spawn_index >= valid_spawns.size(): - print("Critical: Not enough spawn points for players!") - break - var spawn_pos = valid_spawns[spawn_index] - # Set position and sync to all clients - player.current_position = spawn_pos - player.position = player.grid_to_world(spawn_pos) - player.spawn_point_selected = true - player.rpc("set_spawn_position", spawn_pos) + var assigned_pos = Vector2i(-1, -1) + + # Try to get from the current quadrant + var quadrant_idx = spawn_index % 4 + var quadrant = quadrants[quadrant_idx] + + if quadrant.size() > 0: + assigned_pos = quadrant.pop_back() + else: + # Fallback: Try other quadrants if preferred one is empty + for q in quadrants: + if q.size() > 0: + assigned_pos = q.pop_back() + break + + # Ultimate fallback: Random from anywhere + if assigned_pos == Vector2i(-1, -1) and all_spawns.size() > 0: + assigned_pos = all_spawns.pop_back() + + if assigned_pos != Vector2i(-1, -1): + # Set position and sync to all clients + player.current_position = assigned_pos + player.position = player.grid_to_world(assigned_pos) + player.spawn_point_selected = true + player.rpc("set_spawn_position", assigned_pos) + else: + print("Critical: No spawn point found for player ", player.name) + spawn_index += 1 # ============================================================================= @@ -910,9 +970,11 @@ func request_full_player_sync(requesting_peer_id: int): var player_data = { "peer_id": peer_id, "position": player.current_position, + "name": player.display_name, "goals": player.goals, "playerboard": player.playerboard, - "is_bot": player.is_bot || player.is_in_group("Bots") + "is_bot": player.is_bot || player.is_in_group("Bots"), + "spawn_point_selected": player.spawn_point_selected } rpc_id(requesting_peer_id, "create_specific_player", player_data) await get_tree().create_timer(0.1).timeout @@ -933,6 +995,20 @@ func create_specific_player(data: Dictionary): add_child(player_character) player_character.add_to_group("Players", true) + # Set spawn flag directly so it doesn't hide itself + if data.has("spawn_point_selected") and data["spawn_point_selected"]: + player_character.spawn_point_selected = true + player_character.visible = true + + # Ensure visual position matches logical + var new_pos = player_character.grid_to_world(data["position"]) + player_character.global_position = new_pos + player_character.target_visual_position = new_pos + + # Set display name + if data.has("name"): + player_character.display_name = data["name"] + if data["is_bot"]: player_character.add_to_group("Bots", true) player_character.is_bot = true diff --git a/scenes/player.gd b/scenes/player.gd index 3f689c4..334bd7d 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -13,7 +13,20 @@ var powerup_manager var score: int = 0 # Display name (synced across network) -var display_name: String = "" +var _display_name: String = "" +var display_name: String: + set(value): + _display_name = value + # Update label if it exists + var name_label = get_node_or_null("Name") + if name_label: + name_label.text = _display_name + + # Sync to other peers if we are authority + if is_multiplayer_authority() and is_inside_tree(): + rpc("sync_display_name", _display_name) + get: + return _display_name # Special effect states var is_frozen: bool = false @@ -262,6 +275,11 @@ func _ready(): rpc("sync_position", current_position) else: target_visual_position = global_position + # If random spawn is enabled, do NOT broadcast (0,0). Wait for set_spawn_position. + + # Prevent visual "jump" by hiding until spawn position is confirmed + if LobbyManager.get_randomize_spawn() and not spawn_point_selected: + visible = false func _init_managers(): movement_manager = load("res://scripts/managers/player_movement_manager.gd").new() @@ -588,8 +606,9 @@ func sync_bot_status(is_bot_status: bool): @rpc("any_peer", "call_local", "reliable") func sync_display_name(new_name: String) -> void: """Sync display name across network.""" - display_name = new_name - $Name.text = display_name + _display_name = new_name + if has_node("Name"): + $Name.text = _display_name @rpc("any_peer", "call_local", "reliable") func sync_modulate(color: Color) -> void: @@ -893,6 +912,11 @@ func activate_powerup(effect_id: int): func _process(delta): + # Failsafe: Ensure player is visible if spawn point is selected and random spawn is active + # This handles race conditions where set_spawn_position might be called before _ready finishes hiding + if LobbyManager.get_randomize_spawn() and spawn_point_selected and not visible: + visible = true + if is_multiplayer_authority(): # Visual debugging - show display name with connection status $Name.text = display_name if not display_name.is_empty() else str(name) @@ -906,8 +930,11 @@ func _process(delta): # Client-side visual smoothing # Only interpolate if NOT running a movement tween, OR if the drift is large (teleport/snap) if not is_player_moving: - # Snap to target if very close (prevents micro-jitter) - if global_position.distance_squared_to(target_visual_position) < 0.001: + var dist_sq = global_position.distance_squared_to(target_visual_position) + + if dist_sq > 4.0: # If distance > 2.0 units (teleport/spawn), snap immediately + global_position = target_visual_position + elif dist_sq < 0.001: # Prevent micro-jitter global_position = target_visual_position else: # Interpolate towards the target position received from authority @@ -1157,6 +1184,12 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): is_player_moving = true if path.size() > 0: target_position = Vector2i(path[-1].x, path[-1].y) + + # FORCE SNAP START: Ensure we start animating from our actual grid position + # This prevents "jumps" if the visual node drifted or was set via global_position vs position mismatch + var start_world_pos = grid_to_world(current_position) + if global_position.distance_squared_to(start_world_pos) > 0.001: + global_position = start_world_pos var tween = create_tween() tween.set_trans(Tween.TRANS_LINEAR) @@ -1167,7 +1200,8 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): step_duration = step_duration / movement_manager.speed_multiplier for point in path: - tween.tween_property(self, "position", grid_to_world(Vector2i(point.x, point.y)), step_duration) + # Use global_position for consistency + tween.tween_property(self, "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration) tween.tween_callback(func(): current_position = Vector2i(path[-1].x, path[-1].y) @@ -1739,6 +1773,13 @@ func update_visual_position(): @rpc("any_peer", "call_local") func sync_position(pos: Vector2i): current_position = pos + + # If random spawn is active, receiving a position sync essentially confirms a valid spawn + if LobbyManager.get_randomize_spawn(): + spawn_point_selected = true + if not visible: + visible = true + # Always update the visual position after position sync var new_pos = Vector3( current_position.x * cell_size.x + cell_size.x * 0.5, @@ -1767,6 +1808,9 @@ func set_spawn_position(pos: Vector2i): global_position = new_pos target_visual_position = new_pos + + # Reveal character now that it's in the correct position + visible = true @rpc("any_peer", "call_local", "reliable") diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index c213d0a..4006cdd 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -277,6 +277,37 @@ func sync_character(player_id: int, character_name: String) -> void: 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(): + 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) # =============================================================================