feat: Implement core game managers, player movement logic, and initial UI scenes.
This commit is contained in:
+199
-32
@@ -8,6 +8,8 @@ extends Node3D
|
||||
var ui_manager
|
||||
var obstacle_manager
|
||||
var goals_cycle_manager
|
||||
var screen_shake_manager
|
||||
var touch_controls
|
||||
|
||||
# Minimal local state
|
||||
var _connection_check_timer: float = 0.0
|
||||
@@ -30,7 +32,8 @@ func _ready():
|
||||
ui_manager.setup_leaderboard_ui(self)
|
||||
ui_manager.setup_powerup_bar_ui(self)
|
||||
_setup_obstacle_ui()
|
||||
_setup_global_match_timer_ui()
|
||||
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
||||
# NetworkPanel is visible during gameplay
|
||||
|
||||
# Auto-start game if coming from lobby (already connected to match)
|
||||
if NakamaManager.is_connected_to_nakama() and multiplayer.get_unique_id() != 0:
|
||||
@@ -56,6 +59,18 @@ func _init_managers():
|
||||
add_child(goals_cycle_manager)
|
||||
goals_cycle_manager.initialize(self)
|
||||
|
||||
# Screen shake manager for impact feedback
|
||||
screen_shake_manager = load("res://scripts/managers/screen_shake.gd").new()
|
||||
screen_shake_manager.name = "ScreenShakeManager"
|
||||
add_child(screen_shake_manager)
|
||||
screen_shake_manager.initialize($Camera3D)
|
||||
|
||||
# Touch controls for mobile
|
||||
touch_controls = load("res://scripts/managers/touch_controls.gd").new()
|
||||
touch_controls.name = "TouchControls"
|
||||
add_child(touch_controls)
|
||||
touch_controls.initialize(self)
|
||||
|
||||
# Connect signals for UI updates
|
||||
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
|
||||
goals_cycle_manager.score_updated.connect(_on_score_updated)
|
||||
@@ -103,7 +118,11 @@ func add_message_to_bar(player_name: String, message: String, type: int = Messag
|
||||
icon = "💬 "
|
||||
color = Color(0.9, 0.9, 0.9) # Light gray
|
||||
|
||||
label.text = "%s%s" % [icon, message]
|
||||
# Include player name in message if provided
|
||||
if player_name and player_name != "":
|
||||
label.text = "%s[%s] %s" % [icon, player_name, message]
|
||||
else:
|
||||
label.text = "%s%s" % [icon, message]
|
||||
label.add_theme_color_override("font_color", color)
|
||||
|
||||
# Add shadow for better visibility
|
||||
@@ -248,14 +267,20 @@ func _process(delta):
|
||||
# =============================================================================
|
||||
|
||||
func _on_match_joined(match_id: String):
|
||||
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
|
||||
|
||||
if multiplayer.is_server():
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Server (Match: %s)" % match_id
|
||||
_setup_host_game()
|
||||
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
|
||||
if network_panel:
|
||||
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
|
||||
if multiplayer.is_server():
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Server (Match: %s)" % match_id
|
||||
_setup_host_game()
|
||||
else:
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client"
|
||||
_setup_client_game()
|
||||
else:
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client"
|
||||
_setup_client_game()
|
||||
if multiplayer.is_server():
|
||||
_setup_host_game()
|
||||
else:
|
||||
_setup_client_game()
|
||||
|
||||
# =============================================================================
|
||||
# Game Setup
|
||||
@@ -273,9 +298,24 @@ func _setup_host_game():
|
||||
GameStateManager.add_player(player_id)
|
||||
GameStateManager.local_player_character = player_character
|
||||
ui_manager.set_local_player(player_character)
|
||||
if touch_controls:
|
||||
touch_controls.set_player(player_character)
|
||||
|
||||
# Wait for player to be fully ready (player.gd has 0.1s await in _ready before managers init)
|
||||
await get_tree().create_timer(0.2).timeout
|
||||
# Spawn client players that joined via lobby (need to add them first)
|
||||
var lobby_players = LobbyManager.get_players()
|
||||
for lobby_player in lobby_players:
|
||||
var peer_id = lobby_player.get("id", 0)
|
||||
if peer_id != 1 and peer_id != 0: # Skip host (1) and invalid (0)
|
||||
print("Spawning lobby player: ", peer_id)
|
||||
_spawn_lobby_client_sync(peer_id)
|
||||
|
||||
# IMMEDIATELY assign random spawn positions before any player _ready() completes
|
||||
# Player _ready() has 0.1s await, so we assign before that completes
|
||||
if LobbyManager.get_randomize_spawn():
|
||||
_assign_random_spawn_positions()
|
||||
|
||||
# Wait for players to be fully ready (player.gd has 0.1s await in _ready before managers init)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
|
||||
# Set host goals - get goals directly from GoalManager
|
||||
var host_goals = GoalManager.get_goals_for_player(0)
|
||||
@@ -290,14 +330,17 @@ func _setup_host_game():
|
||||
_update_player_goals_ui(0, host_goals)
|
||||
ui_manager.update_playerboard_ui()
|
||||
|
||||
# Spawn client players that joined via lobby
|
||||
var lobby_players = LobbyManager.get_players()
|
||||
# Set goals for lobby client players
|
||||
var player_index = 1
|
||||
for lobby_player in lobby_players:
|
||||
var peer_id = lobby_player.get("id", 0)
|
||||
if peer_id != 1 and peer_id != 0: # Skip host (1) and invalid (0)
|
||||
print("Spawning lobby player: ", peer_id)
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
_spawn_lobby_client(peer_id)
|
||||
if peer_id != 1 and peer_id != 0:
|
||||
var client_player = get_node_or_null(str(peer_id))
|
||||
if client_player and player_index < GoalManager.preset_goals.size():
|
||||
var client_goals = GoalManager.preset_goals[player_index].duplicate()
|
||||
client_player.goals = client_goals
|
||||
call_deferred("_deferred_set_player_goals", peer_id, client_goals)
|
||||
player_index += 1
|
||||
|
||||
# Add bots (only if no lobby players connected)
|
||||
if GameStateManager.enable_bots and lobby_players.size() <= 1:
|
||||
@@ -306,8 +349,8 @@ func _setup_host_game():
|
||||
|
||||
_start_game()
|
||||
|
||||
func _spawn_lobby_client(peer_id: int):
|
||||
"""Spawn a client player that was in the lobby."""
|
||||
func _spawn_lobby_client_sync(peer_id: int):
|
||||
"""Spawn a client player synchronously (no await)."""
|
||||
if has_node(str(peer_id)):
|
||||
return
|
||||
|
||||
@@ -319,13 +362,7 @@ func _spawn_lobby_client(peer_id: int):
|
||||
# Tell all clients to create this player
|
||||
rpc("add_newly_connected_player_character", peer_id)
|
||||
|
||||
# Wait for player to be ready then assign goals
|
||||
await get_tree().create_timer(0.3).timeout
|
||||
var player_index = GameStateManager.players.find(peer_id)
|
||||
if player_index >= 0 and player_index < GoalManager.preset_goals.size():
|
||||
var player_goals = GoalManager.preset_goals[player_index].duplicate()
|
||||
player_character.goals = player_goals
|
||||
call_deferred("_deferred_set_player_goals", peer_id, player_goals)
|
||||
# Goals will be assigned after players are ready in _setup_host_game
|
||||
|
||||
func _setup_client_game():
|
||||
"""Setup client when transitioning from lobby."""
|
||||
@@ -340,6 +377,8 @@ func _setup_client_game():
|
||||
GameStateManager.add_player(my_id)
|
||||
GameStateManager.local_player_character = player_character
|
||||
ui_manager.set_local_player(player_character)
|
||||
if touch_controls:
|
||||
touch_controls.set_player(player_character)
|
||||
ui_manager.update_button_states()
|
||||
print("Created local player for client: ", my_id)
|
||||
|
||||
@@ -349,19 +388,24 @@ func _setup_client_game():
|
||||
|
||||
func _auto_start_from_lobby():
|
||||
"""Called when main.tscn is loaded from lobby - game is already connected."""
|
||||
$NetworkPanel/NetworkInfo/UniquePeerID.text = str(multiplayer.get_unique_id())
|
||||
|
||||
# Get match ID from LobbyManager
|
||||
var match_id = LobbyManager.current_room.get("match_id", "")
|
||||
var short_id = match_id.substr(0, 8) if match_id.length() > 8 else match_id
|
||||
|
||||
# Update NetworkPanel in PauseMenu (if exists)
|
||||
var network_panel = get_node_or_null("PauseMenu/Panel/NetworkPanel")
|
||||
if network_panel:
|
||||
network_panel.get_node("NetworkInfo/UniquePeerID").text = str(multiplayer.get_unique_id())
|
||||
if multiplayer.is_server():
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Host (Match: %s)" % short_id
|
||||
else:
|
||||
network_panel.get_node("NetworkInfo/NetworkSideDisplay").text = "Client (Match: %s)" % short_id
|
||||
|
||||
if multiplayer.is_server():
|
||||
print("Auto-starting as HOST - Match: ", short_id)
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Host (Match: %s)" % short_id
|
||||
_setup_host_game()
|
||||
else:
|
||||
print("Auto-starting as CLIENT - Match: ", short_id)
|
||||
$NetworkPanel/NetworkInfo/NetworkSideDisplay.text = "Client (Match: %s)" % short_id
|
||||
_setup_client_game()
|
||||
|
||||
func _start_game():
|
||||
@@ -383,6 +427,34 @@ func _start_game():
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
ui_manager.initialize_leaderboard_with_players(all_players)
|
||||
|
||||
func _assign_random_spawn_positions():
|
||||
"""Assign random unique spawn positions to all players."""
|
||||
var spawn_locations = [
|
||||
Vector2i(0, 0), Vector2i(0, 1), Vector2i(0, 2), Vector2i(0, 3),
|
||||
Vector2i(0, 4), Vector2i(0, 5), Vector2i(0, 6), Vector2i(0, 7),
|
||||
Vector2i(0, 8), Vector2i(0, 9), Vector2i(0, 10), Vector2i(0, 11)
|
||||
]
|
||||
|
||||
# Shuffle spawn locations
|
||||
var shuffled_spawns = spawn_locations.duplicate()
|
||||
shuffled_spawns.shuffle()
|
||||
|
||||
# Get all players
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
|
||||
# Assign positions
|
||||
var spawn_index = 0
|
||||
for player in all_players:
|
||||
if spawn_index >= shuffled_spawns.size():
|
||||
break
|
||||
var spawn_pos = shuffled_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)
|
||||
spawn_index += 1
|
||||
|
||||
# =============================================================================
|
||||
# Player Management
|
||||
# =============================================================================
|
||||
@@ -428,6 +500,8 @@ func add_player_character(peer_id: int):
|
||||
if peer_id == multiplayer.get_unique_id():
|
||||
GameStateManager.local_player_character = player_character
|
||||
ui_manager.set_local_player(player_character)
|
||||
if touch_controls:
|
||||
touch_controls.set_player(player_character)
|
||||
ui_manager.update_button_states()
|
||||
ui_manager.update_playerboard_ui()
|
||||
|
||||
@@ -617,6 +691,12 @@ func _update_goals_ui_for_player(player_id: int, goals: Array):
|
||||
|
||||
@rpc("any_peer", "call_local")
|
||||
func sync_playerboard(player_id: int, new_playerboard: Array):
|
||||
# Find the player and update their playerboard
|
||||
var player = get_node_or_null(str(player_id))
|
||||
if player:
|
||||
player.playerboard = new_playerboard.duplicate()
|
||||
|
||||
# Update UI for local player
|
||||
if player_id == multiplayer.get_unique_id() and GameStateManager.local_player_character:
|
||||
ui_manager.update_playerboard_ui()
|
||||
update_all_players_boards()
|
||||
@@ -1009,9 +1089,8 @@ func _show_game_over_panel():
|
||||
back_btn.custom_minimum_size = Vector2(300, 60)
|
||||
back_btn.add_theme_font_size_override("font_size", 20)
|
||||
back_btn.pressed.connect(_on_back_to_menu_pressed)
|
||||
inner_vbox.add_child(back_btn)
|
||||
|
||||
# Center the button
|
||||
# Center the button in a container
|
||||
var btn_container = HBoxContainer.new()
|
||||
btn_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
btn_container.add_child(back_btn)
|
||||
@@ -1026,11 +1105,28 @@ func _on_back_to_menu_pressed():
|
||||
# Clean up game state
|
||||
GameStateManager.end_game()
|
||||
LobbyManager.reset()
|
||||
# Properly disconnect from Nakama match
|
||||
_cleanup_multiplayer()
|
||||
|
||||
# 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)
|
||||
|
||||
func _deferred_init_leaderboard():
|
||||
"""Initialize leaderboard after a delay to ensure all players are loaded."""
|
||||
# Longer delay ensures players are synced
|
||||
@@ -1151,3 +1247,74 @@ func _get_ordinal(n: int) -> String:
|
||||
3: return "3rd"
|
||||
4: return "4th"
|
||||
_: return str(n) + "th"
|
||||
|
||||
# =============================================================================
|
||||
# Pause Menu & Settings
|
||||
# =============================================================================
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
_toggle_pause_menu()
|
||||
|
||||
func _toggle_pause_menu():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
if pause_menu:
|
||||
pause_menu.visible = not pause_menu.visible
|
||||
get_tree().paused = pause_menu.visible
|
||||
|
||||
func _on_resume_pressed():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
if pause_menu:
|
||||
pause_menu.visible = false
|
||||
get_tree().paused = false
|
||||
|
||||
func _on_settings_pressed():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
var settings_panel = get_node_or_null("SettingsPanel")
|
||||
if pause_menu:
|
||||
pause_menu.visible = false
|
||||
if settings_panel:
|
||||
settings_panel.visible = true
|
||||
|
||||
# Sync settings UI with current state
|
||||
var joystick_toggle = settings_panel.get_node_or_null("Panel/VBox/JoystickToggle")
|
||||
if joystick_toggle and touch_controls:
|
||||
joystick_toggle.set_pressed_no_signal(touch_controls.joystick_enabled)
|
||||
|
||||
var size_slider = settings_panel.get_node_or_null("Panel/VBox/ButtonSizeRow/ButtonSizeSlider")
|
||||
if size_slider and touch_controls:
|
||||
size_slider.set_value_no_signal(touch_controls.button_size)
|
||||
|
||||
var opacity_slider = settings_panel.get_node_or_null("Panel/VBox/OpacityRow/OpacitySlider")
|
||||
if opacity_slider and touch_controls:
|
||||
opacity_slider.set_value_no_signal(touch_controls.button_opacity)
|
||||
|
||||
func _on_quit_match_pressed():
|
||||
get_tree().paused = false
|
||||
# Properly disconnect from Nakama match
|
||||
_cleanup_multiplayer()
|
||||
# Return to lobby or main menu
|
||||
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||
|
||||
func _on_settings_back_pressed():
|
||||
var pause_menu = get_node_or_null("PauseMenu")
|
||||
var settings_panel = get_node_or_null("SettingsPanel")
|
||||
if settings_panel:
|
||||
settings_panel.visible = false
|
||||
if pause_menu:
|
||||
pause_menu.visible = true
|
||||
|
||||
func _on_button_size_changed(value: float):
|
||||
if touch_controls:
|
||||
touch_controls.button_size = value
|
||||
touch_controls._save_settings()
|
||||
|
||||
func _on_opacity_changed(value: float):
|
||||
if touch_controls:
|
||||
touch_controls.button_opacity = value
|
||||
touch_controls._save_settings()
|
||||
|
||||
func _on_joystick_toggled(enabled: bool):
|
||||
if touch_controls:
|
||||
touch_controls.set_joystick_enabled(enabled)
|
||||
touch_controls._save_settings()
|
||||
|
||||
Reference in New Issue
Block a user