diff --git a/scenes/lobby.gd b/scenes/lobby.gd index 4dd9f9c..d6cf450 100644 --- a/scenes/lobby.gd +++ b/scenes/lobby.gd @@ -545,6 +545,9 @@ func _on_profile_updated() -> void: func _update_player_slots() -> void: """Update all player slot visuals based on current player list.""" + if not multiplayer.has_multiplayer_peer(): + return + var players = LobbyManager.get_players() var my_id = multiplayer.get_unique_id() diff --git a/scenes/main.gd b/scenes/main.gd index 30ef440..349d550 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -1766,6 +1766,13 @@ func _show_game_over_panel(): if existing_panel: existing_panel.show() return + + # Hide Gameplay UI + var actions_btn = get_node_or_null("TouchControls/TouchControls/ActionsBtn") + if actions_btn: actions_btn.hide() + + if stop_n_go_manager and stop_n_go_manager.hud_layer: + stop_n_go_manager.hud_layer.hide() # Create game over panel var panel = PanelContainer.new() @@ -1839,41 +1846,78 @@ func _show_game_over_panel(): player_scores.sort_custom(func(a, b): return a.score > b.score) # Display each player - for i in range(min(player_scores.size(), 8)): + var local_peer_id = multiplayer.get_unique_id() + var local_player_rank = -1 + + # Find local player rank in sorted list + for i in range(player_scores.size()): + if player_scores[i].peer_id == local_peer_id: + local_player_rank = i + break + + var rank_colors = [ + Color(1.0, 0.84, 0.0), # Gold + Color(0.75, 0.75, 0.75), # Silver + Color(0.8, 0.5, 0.2) # Bronze + ] + var rank_emojis = ["🥇", "🥈", "🥉"] + + # Helper to create a leaderboard entry HBox + var create_entry = func(rank_idx: int): var entry = HBoxContainer.new() entry.add_theme_constant_override("separation", 20) - var rank_colors = [ - Color(1.0, 0.84, 0.0), # Gold - Color(0.75, 0.75, 0.75), # Silver - Color(0.8, 0.5, 0.2), # Bronze - Color(0.5, 0.5, 0.5), # 4th - Color(0.5, 0.5, 0.5), # 5th - Color(0.5, 0.5, 0.5), # 6th - Color(0.5, 0.5, 0.5), # 7th - Color(0.5, 0.5, 0.5) # 8th - ] - var rank_emojis = ["🥇", "🥈", "🥉", "4th", "5th", "6th", "7th", "8th"] + var score_data = player_scores[rank_idx] + var is_local = score_data.peer_id == local_peer_id var rank_label = Label.new() - rank_label.text = rank_emojis[i] + if rank_idx < 3: + rank_label.text = rank_emojis[rank_idx] + else: + # Ordinal rank (4th, 5th, etc.) + var n = rank_idx + 1 + var suffix = "th" + if n % 10 == 1 and n % 100 != 11: suffix = "st" + elif n % 10 == 2 and n % 100 != 12: suffix = "nd" + elif n % 10 == 3 and n % 100 != 13: suffix = "rd" + rank_label.text = str(n) + suffix + rank_label.add_theme_font_size_override("font_size", 32) entry.add_child(rank_label) var name_label = Label.new() - name_label.text = player_scores[i].name + name_label.text = score_data.name + (" (YOU)" if is_local else "") name_label.add_theme_font_size_override("font_size", 28) - name_label.add_theme_color_override("font_color", rank_colors[i]) + if rank_idx < 3: + name_label.add_theme_color_override("font_color", rank_colors[rank_idx]) + elif is_local: + name_label.add_theme_color_override("font_color", Color(0.4, 0.7, 1.0)) # Blue for local player + name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL entry.add_child(name_label) var score_label = Label.new() - score_label.text = str(player_scores[i].score) + score_label.text = str(score_data.score) score_label.add_theme_font_size_override("font_size", 28) score_label.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4)) entry.add_child(score_label) - leaderboard_container.add_child(entry) + return entry + + # 1. Show Top 3 + for i in range(min(player_scores.size(), 3)): + leaderboard_container.add_child(create_entry.call(i)) + + # 2. Show Local Player if NOT in Top 3 + if local_player_rank >= 3: + # Add a separator + var separator = ColorRect.new() + separator.custom_minimum_size = Vector2(0, 2) + separator.color = Color(1, 1, 1, 0.2) + leaderboard_container.add_child(separator) + + # Add local player entry + leaderboard_container.add_child(create_entry.call(local_player_rank)) # Back to Menu button var back_btn = Button.new() @@ -1901,24 +1945,16 @@ func _on_back_to_menu_pressed(): # Properly disconnect from Nakama match _cleanup_multiplayer() + # Small delay to let cleanup settle + await get_tree().create_timer(0.2).timeout + # Go back to lobby if get_tree(): get_tree().change_scene_to_file("res://scenes/lobby.tscn") func _cleanup_multiplayer(): """Properly leave Nakama match and cleanup multiplayer state.""" - print("[Main] Cleaning up multiplayer connection...") - - # Leave the Nakama match through the bridge - if NakamaManager.bridge: - NakamaManager.bridge.leave() - - # Clear the current match ID - NakamaManager.current_match_id = "" - - # Reset multiplayer peer to disconnect cleanly - if multiplayer.get_multiplayer_peer(): - multiplayer.set_multiplayer_peer(null) + NakamaManager.cleanup() func _deferred_init_leaderboard(): """Initialize leaderboard after a delay to ensure all players are loaded.""" diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index d180b05..533f0f0 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -195,6 +195,9 @@ func _run_ai_tick(): print("[BotController] Action Taken: Put") return + if not is_instance_valid(actor): + return + var goals_achv = _is_goals_achieved() if actor.action_points > 1: # Only print if they have multi-AP @@ -256,7 +259,7 @@ func _try_use_powerup() -> bool: NotificationManager.send_message(actor, NotificationManager.MESSAGES.USED_SPECIAL_POWER, NotificationManager.MessageType.POWERUP) await _wait_with_variance(action_delay) - if not is_instance_valid(self): return true # Early exit if deleted + if not is_instance_valid(actor) or not is_instance_valid(self): return true # Early exit if deleted _is_processing_action = false _current_action = "idle" @@ -301,7 +304,7 @@ func _try_attack_chase() -> bool: _is_processing_action = true _current_action = "attacking" await _wait_with_variance(action_delay) # Shorter delay for attacks? perhaos - if not is_instance_valid(self): return true + if not is_instance_valid(self) or not is_instance_valid(actor): return true _is_processing_action = false _current_action = "idle" return true @@ -365,7 +368,7 @@ func _try_grab() -> bool: # Wait for animation await _wait_with_variance(action_delay) - if not is_instance_valid(self): return true + if not is_instance_valid(self) or not is_instance_valid(actor): return true _is_processing_action = false _current_action = "idle" return true @@ -466,17 +469,19 @@ func _try_move() -> bool: var max_wait_time = 2.0 var elapsed = 0.0 - while actor.is_player_moving and is_instance_valid(self): + while is_instance_valid(actor) and actor.is_player_moving and is_instance_valid(self): await get_tree().process_frame elapsed += get_process_delta_time() if elapsed > max_wait_time: - print("[BotController] %s movement TIMEOUT after %.1fs" % [actor.name, elapsed]) + if is_instance_valid(actor): + print("[BotController] %s movement TIMEOUT after %.1fs" % [actor.name, elapsed]) break - if not is_instance_valid(self): return true + if not is_instance_valid(self) or not is_instance_valid(actor): return true _is_processing_action = false _current_action = "idle" - print("[BotController] %s move finished. New Pos: %s" % [actor.name, actor.current_position]) + if is_instance_valid(actor): + print("[BotController] %s move finished. New Pos: %s" % [actor.name, actor.current_position]) return true else: print("[BotController] %s simple_move_to BLOCKED (others). Trying unstuck move." % actor.name) @@ -515,15 +520,16 @@ func _try_unstuck_move() -> bool: # Proper wait for movement completion var max_wait = 1.5 var elapsed = 0.0 - while actor.is_player_moving and is_instance_valid(self): + while is_instance_valid(actor) and actor.is_player_moving and is_instance_valid(self): await get_tree().process_frame elapsed += get_process_delta_time() if elapsed > max_wait: break - if not is_instance_valid(self): return true + if not is_instance_valid(self) or not is_instance_valid(actor): return true _is_processing_action = false _current_action = "idle" - print("[BotController] %s Unstuck move finished at %s" % [actor.name, actor.current_position]) + if is_instance_valid(actor): + print("[BotController] %s Unstuck move finished at %s" % [actor.name, actor.current_position]) return true return false @@ -580,7 +586,7 @@ func _try_put(high_priority: bool = false) -> bool: print("[BotController] %s put unneeded tile %d at %s (Panic: %s)" % [actor.name, item, put_position, is_panic]) await _wait_with_variance(action_delay) - if not is_instance_valid(self): return true + if not is_instance_valid(actor) or not is_instance_valid(self): return true _is_processing_action = false _current_action = "idle" return true @@ -631,7 +637,7 @@ func _try_arrange() -> bool: print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot]) await _wait_with_variance(action_delay) - if not is_instance_valid(self): return true + if not is_instance_valid(actor) or not is_instance_valid(self): return true _is_processing_action = false _current_action = "idle" return true diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 7d0d648..80ea62a 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -120,6 +120,9 @@ func refresh_room_list() -> void: func set_ready(is_ready: bool) -> void: """Set local player's ready state.""" + if not multiplayer.has_multiplayer_peer(): + return + var my_id = multiplayer.get_unique_id() # Update local state @@ -248,6 +251,10 @@ func set_character(character_name: String) -> void: return local_character_index = idx + + if not multiplayer.has_multiplayer_peer(): + return + var my_id = multiplayer.get_unique_id() # Update local player data @@ -289,6 +296,11 @@ func sync_character(player_id: int, character_name: String) -> void: func set_player_name(new_name: String) -> void: """Set local player's name. Syncs to all peers.""" local_player_name = new_name + + if not multiplayer.has_multiplayer_peer(): + emit_signal("player_list_changed") + return + var my_id = multiplayer.get_unique_id() # Update local player data @@ -412,6 +424,9 @@ func _on_match_joined(match_id: String) -> void: var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id current_room["room_name"] = short_id + if not multiplayer.has_multiplayer_peer(): + return + # Add self to player list var my_id = multiplayer.get_unique_id() var my_data = { diff --git a/scripts/managers/stop_n_go_manager.gd b/scripts/managers/stop_n_go_manager.gd index 7efda86..91f4c28 100644 --- a/scripts/managers/stop_n_go_manager.gd +++ b/scripts/managers/stop_n_go_manager.gd @@ -11,7 +11,7 @@ enum Phase {GO, STOP} const GO_DURATION: float = 8.0 const STOP_DURATION: float = 4.0 -const REQUIRED_GOALS: int = 8 +const REQUIRED_GOALS: int = 1 var current_phase: Phase = Phase.GO var phase_timer: float = GO_DURATION diff --git a/scripts/nakama_manager.gd b/scripts/nakama_manager.gd index f8e501c..cd3660e 100644 --- a/scripts/nakama_manager.gd +++ b/scripts/nakama_manager.gd @@ -75,6 +75,10 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool: # 3. Initialize Multiplayer Bridge # This links Nakama's socket to Godot's High-Level Multiplayer API + if bridge: + bridge.leave() + bridge = null + bridge = NakamaMultiplayerBridge.new(socket) # Connect bridge signals @@ -89,6 +93,26 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool: emit_signal("connected_to_nakama") return true +func cleanup(): + """Properly shutdown the Nakama connection and reset the multiplayer peer.""" + print("[NakamaManager] Full cleanup starting...") + + if bridge: + bridge.leave() + bridge = null + + if socket: + socket.close() + socket = null + + current_match_id = "" + + # Reset Godot's multiplayer peer + if multiplayer.get_multiplayer_peer(): + multiplayer.set_multiplayer_peer(null) + + print("[NakamaManager] Cleanup complete.") + # --- Match Management --- func host_game(): @@ -172,5 +196,4 @@ func list_matches_async() -> Array: return rooms func _exit_tree(): - if socket: - socket.close() + cleanup()