feat: Implement core game managers, player movement logic, and initial UI scenes.

This commit is contained in:
2025-12-27 05:45:57 +08:00
parent 6870016ba6
commit c5e9d073fa
23 changed files with 1456 additions and 97 deletions
+29
View File
@@ -5,6 +5,7 @@ extends Control
@onready var player_name_input = $MainMenuPanel/VBoxContainer/InputSection/PlayerNameInput
@onready var create_room_btn = $MainMenuPanel/VBoxContainer/ButtonSection/CreateRoomBtn
@onready var browse_rooms_btn = $MainMenuPanel/VBoxContainer/ButtonSection/BrowseRoomsBtn
@onready var main_menu_profile_btn = $MainMenuPanel/VBoxContainer/ButtonSection/ProfileBtn
# UI References - Room List
@onready var room_list_panel = $RoomListPanel
@@ -13,6 +14,7 @@ extends Control
@onready var refresh_btn = $RoomListPanel/VBoxContainer/ButtonContainer/RefreshBtn
@onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn
@onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn
@onready var room_list_profile_btn = $RoomListPanel/VBoxContainer/ButtonContainer/ProfileBtn
# UI References - Lobby Panel
@onready var lobby_panel = $LobbyPanel
@@ -28,6 +30,8 @@ extends Control
@onready var copy_id_btn = $LobbyPanel/TopBar/MatchIdContainer/CopyIdBtn
@onready var duration_option = $LobbyPanel/TopBar/SettingsSection/DurationOption
@onready var duration_text_label = $LobbyPanel/TopBar/SettingsSection/DurationTextLabel
@onready var random_spawn_check = $LobbyPanel/TopBar/SettingsSection/RandomSpawnCheck
@onready var random_spawn_label = $LobbyPanel/TopBar/SettingsSection/RandomSpawnLabel
# UI References - Player Slots
@onready var players_container = $LobbyPanel/PlayersContainer
@@ -77,17 +81,22 @@ func _ready():
# Connect button signals - Main Menu
create_room_btn.pressed.connect(_on_create_room_pressed)
browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed)
if main_menu_profile_btn:
main_menu_profile_btn.pressed.connect(_on_profile_btn_pressed)
# Connect button signals - Room List
refresh_btn.pressed.connect(_on_refresh_pressed)
join_btn.pressed.connect(_on_join_pressed)
back_btn.pressed.connect(_on_back_pressed)
if room_list_profile_btn:
room_list_profile_btn.pressed.connect(_on_profile_btn_pressed)
# Connect button signals - Lobby
profile_btn.pressed.connect(_on_profile_btn_pressed)
logout_btn.pressed.connect(_on_logout_pressed)
copy_id_btn.pressed.connect(_on_copy_id_pressed)
duration_option.item_selected.connect(_on_duration_selected)
random_spawn_check.toggled.connect(_on_random_spawn_toggled)
area_left_btn.pressed.connect(func(): LobbyManager.cycle_area(-1))
area_right_btn.pressed.connect(func(): LobbyManager.cycle_area(1))
leave_btn.pressed.connect(_on_leave_pressed)
@@ -104,6 +113,7 @@ func _ready():
LobbyManager.all_players_ready.connect(_on_all_players_ready)
LobbyManager.game_starting.connect(_on_game_starting)
LobbyManager.match_duration_changed.connect(_on_match_duration_changed)
LobbyManager.randomize_spawn_changed.connect(_on_randomize_spawn_changed)
LobbyManager.character_changed.connect(_on_character_changed)
LobbyManager.area_changed.connect(_on_area_changed)
LobbyManager.player_list_changed.connect(_update_player_slots)
@@ -236,6 +246,15 @@ func _on_duration_selected(index: int) -> void:
if index >= 0 and index < durations.size():
LobbyManager.set_match_duration(durations[index])
func _on_random_spawn_toggled(enabled: bool) -> void:
if not LobbyManager.is_host:
return
LobbyManager.set_randomize_spawn(enabled)
func _update_random_spawn_label(enabled: bool) -> void:
if random_spawn_label:
random_spawn_label.text = "Random ✓" if enabled else "Random ✗"
func _on_profile_btn_pressed() -> void:
if not profile_panel_instance:
var profile_panel_scene := load("res://scenes/ui/profile_panel.tscn")
@@ -289,6 +308,12 @@ func _on_room_joined(room_data: Dictionary) -> void:
if not is_host:
_update_duration_text_label(LobbyManager.get_match_duration())
# Random spawn: host sees checkbox, clients see label
random_spawn_check.visible = is_host
random_spawn_label.visible = not is_host
random_spawn_check.button_pressed = LobbyManager.get_randomize_spawn()
_update_random_spawn_label(LobbyManager.get_randomize_spawn())
# Area selector: only host can interact
area_left_btn.disabled = not is_host
area_right_btn.disabled = not is_host
@@ -329,6 +354,10 @@ func _on_match_duration_changed(duration_seconds: int) -> void:
if not LobbyManager.is_host:
_update_duration_text_label(duration_seconds)
func _on_randomize_spawn_changed(enabled: bool) -> void:
if not LobbyManager.is_host:
_update_random_spawn_label(enabled)
func _on_character_changed(_player_id: int, _character_name: String) -> void:
_update_player_slots()
+28
View File
@@ -94,6 +94,12 @@ layout_mode = 2
theme_override_font_sizes/font_size = 16
text = "BROWSE ROOMS"
[node name="ProfileBtn" type="Button" parent="MainMenuPanel/VBoxContainer/ButtonSection"]
custom_minimum_size = Vector2(0, 36)
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "PROFILE"
[node name="RoomListPanel" type="PanelContainer" parent="."]
visible = false
layout_mode = 1
@@ -165,6 +171,11 @@ custom_minimum_size = Vector2(110, 44)
layout_mode = 2
text = "BACK"
[node name="ProfileBtn" type="Button" parent="RoomListPanel/VBoxContainer/ButtonContainer"]
custom_minimum_size = Vector2(80, 44)
layout_mode = 2
text = "PROFILE"
[node name="LobbyPanel" type="Control" parent="."]
visible = false
layout_mode = 1
@@ -268,6 +279,23 @@ theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
theme_override_font_sizes/font_size = 11
text = "3 min"
[node name="SpawnSpacer" type="Control" parent="LobbyPanel/TopBar/SettingsSection"]
custom_minimum_size = Vector2(15, 0)
layout_mode = 2
[node name="RandomSpawnCheck" type="CheckButton" parent="LobbyPanel/TopBar/SettingsSection"]
layout_mode = 2
theme_override_font_sizes/font_size = 11
button_pressed = true
text = "Random Spawn"
[node name="RandomSpawnLabel" type="Label" parent="LobbyPanel/TopBar/SettingsSection"]
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
theme_override_font_sizes/font_size = 11
text = "Random ✓"
[node name="HostBanner" type="PanelContainer" parent="LobbyPanel"]
layout_mode = 1
anchors_preset = 5
+199 -32
View File
@@ -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()
+209 -39
View File
@@ -76,39 +76,6 @@ current = true
fov = 35.5
size = 23.0
[node name="NetworkPanel" type="Panel" parent="."]
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -185.0
offset_top = 25.0
offset_right = 185.0
offset_bottom = 78.0
grow_horizontal = 2
theme_override_styles/panel = ExtResource("5_dvx6y")
[node name="NetworkInfo" type="HBoxContainer" parent="NetworkPanel"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 50
alignment = 1
[node name="NetworkSideDisplay" type="Label" parent="NetworkPanel/NetworkInfo"]
layout_mode = 2
text = "Network Side"
horizontal_alignment = 1
vertical_alignment = 1
[node name="UniquePeerID" type="Label" parent="NetworkPanel/NetworkInfo"]
layout_mode = 2
text = "Unique Peer ID"
horizontal_alignment = 1
vertical_alignment = 1
[node name="PlayerboardPanel" type="PanelContainer" parent="."]
anchors_preset = 4
anchor_top = 0.5
@@ -9375,9 +9342,9 @@ anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -200.0
offset_top = -87.0
offset_top = -52.0
offset_right = 200.0
offset_bottom = -53.0
offset_bottom = -18.0
grow_horizontal = 2
grow_vertical = 0
theme_override_styles/panel = ExtResource("5_dvx6y")
@@ -9397,11 +9364,13 @@ theme_override_constants/separation = 4
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -294.0
offset_left = -211.0
offset_top = 15.0
offset_right = -18.0
offset_bottom = 215.0
offset_bottom = 166.0
grow_horizontal = 0
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="MarginContainer" type="MarginContainer" parent="LeaderboardPanel"]
layout_mode = 2
@@ -9518,8 +9487,8 @@ horizontal_alignment = 2
[node name="GoalsTimer" type="PanelContainer" parent="."]
offset_left = 20.0
offset_top = 20.0
offset_right = 120.0
offset_bottom = 90.0
offset_right = 97.0
offset_bottom = 97.0
[node name="VBox" type="VBoxContainer" parent="GoalsTimer"]
layout_mode = 2
@@ -9537,6 +9506,207 @@ theme_override_font_sizes/font_size = 12
text = "seconds"
horizontal_alignment = 1
[node name="GlobalMatchTimer" type="PanelContainer" parent="."]
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -80.0
offset_top = 24.0
offset_right = 80.0
offset_bottom = 74.0
grow_horizontal = 2
[node name="VBox" type="VBoxContainer" parent="GlobalMatchTimer"]
layout_mode = 2
alignment = 1
[node name="TimerLabel" type="Label" parent="GlobalMatchTimer/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 28
text = "3:00"
horizontal_alignment = 1
[node name="PauseMenu" type="CanvasLayer" parent="."]
process_mode = 3
layer = 10
visible = false
[node name="Background" type="ColorRect" parent="PauseMenu"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.7)
[node name="Panel" type="PanelContainer" parent="PauseMenu"]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -150.0
offset_top = -150.0
offset_right = 150.0
offset_bottom = 150.0
grow_horizontal = 2
grow_vertical = 2
[node name="NetworkPanel" type="Panel" parent="PauseMenu/Panel"]
layout_mode = 2
theme_override_styles/panel = ExtResource("5_dvx6y")
[node name="NetworkInfo" type="HBoxContainer" parent="PauseMenu/Panel/NetworkPanel"]
layout_mode = 1
anchors_preset = 5
anchor_left = 0.5
anchor_right = 0.5
offset_left = -135.0
offset_right = 135.0
offset_bottom = 23.0
grow_horizontal = 2
theme_override_constants/separation = 50
alignment = 1
[node name="NetworkSideDisplay" type="Label" parent="PauseMenu/Panel/NetworkPanel/NetworkInfo"]
layout_mode = 2
text = "Network Side"
horizontal_alignment = 1
vertical_alignment = 1
[node name="UniquePeerID" type="Label" parent="PauseMenu/Panel/NetworkPanel/NetworkInfo"]
layout_mode = 2
text = "Unique Peer ID"
horizontal_alignment = 1
vertical_alignment = 1
[node name="VBox" type="VBoxContainer" parent="PauseMenu/Panel"]
layout_mode = 2
theme_override_constants/separation = 15
alignment = 1
[node name="Title" type="Label" parent="PauseMenu/Panel/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "PAUSED"
horizontal_alignment = 1
[node name="Spacer" type="Control" parent="PauseMenu/Panel/VBox"]
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
[node name="ResumeBtn" type="Button" parent="PauseMenu/Panel/VBox"]
custom_minimum_size = Vector2(200, 45)
layout_mode = 2
text = "Resume"
[node name="SettingsBtn" type="Button" parent="PauseMenu/Panel/VBox"]
custom_minimum_size = Vector2(200, 45)
layout_mode = 2
text = "Settings"
[node name="QuitBtn" type="Button" parent="PauseMenu/Panel/VBox"]
custom_minimum_size = Vector2(200, 45)
layout_mode = 2
text = "Quit Match"
[node name="SettingsPanel" type="CanvasLayer" parent="."]
process_mode = 3
layer = 11
visible = false
[node name="Background" type="ColorRect" parent="SettingsPanel"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.8)
[node name="Panel" type="PanelContainer" parent="SettingsPanel"]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -200.0
offset_top = -200.0
offset_right = 200.0
offset_bottom = 200.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBox" type="VBoxContainer" parent="SettingsPanel/Panel"]
layout_mode = 2
theme_override_constants/separation = 12
alignment = 1
[node name="Title" type="Label" parent="SettingsPanel/Panel/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 22
text = "Settings"
horizontal_alignment = 1
[node name="Spacer" type="Control" parent="SettingsPanel/Panel/VBox"]
custom_minimum_size = Vector2(0, 10)
layout_mode = 2
[node name="TouchHeader" type="Label" parent="SettingsPanel/Panel/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 16
text = "Touch Controls"
[node name="ButtonSizeRow" type="HBoxContainer" parent="SettingsPanel/Panel/VBox"]
layout_mode = 2
[node name="Label" type="Label" parent="SettingsPanel/Panel/VBox/ButtonSizeRow"]
layout_mode = 2
size_flags_horizontal = 3
text = "Button Size"
[node name="ButtonSizeSlider" type="HSlider" parent="SettingsPanel/Panel/VBox/ButtonSizeRow"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
min_value = 50.0
max_value = 120.0
value = 70.0
[node name="OpacityRow" type="HBoxContainer" parent="SettingsPanel/Panel/VBox"]
layout_mode = 2
[node name="Label" type="Label" parent="SettingsPanel/Panel/VBox/OpacityRow"]
layout_mode = 2
size_flags_horizontal = 3
text = "Button Opacity"
[node name="OpacitySlider" type="HSlider" parent="SettingsPanel/Panel/VBox/OpacityRow"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
max_value = 1.0
step = 0.1
value = 0.7
[node name="JoystickToggle" type="CheckButton" parent="SettingsPanel/Panel/VBox"]
layout_mode = 2
button_pressed = true
text = "Enable Virtual Joystick"
[node name="Spacer2" type="Control" parent="SettingsPanel/Panel/VBox"]
custom_minimum_size = Vector2(0, 15)
layout_mode = 2
[node name="BackBtn" type="Button" parent="SettingsPanel/Panel/VBox"]
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
text = "Back"
[connection signal="pressed" from="Menu/Host" to="." method="_on_host_pressed"]
[connection signal="pressed" from="Menu/Join" to="." method="_on_join_pressed"]
[connection signal="text_submitted" from="MessageInput" to="." method="_on_message_input_text_submitted"]
[connection signal="pressed" from="PauseMenu/Panel/VBox/ResumeBtn" to="." method="_on_resume_pressed"]
[connection signal="pressed" from="PauseMenu/Panel/VBox/SettingsBtn" to="." method="_on_settings_pressed"]
[connection signal="pressed" from="PauseMenu/Panel/VBox/QuitBtn" to="." method="_on_quit_match_pressed"]
[connection signal="value_changed" from="SettingsPanel/Panel/VBox/ButtonSizeRow/ButtonSizeSlider" to="." method="_on_button_size_changed"]
[connection signal="value_changed" from="SettingsPanel/Panel/VBox/OpacityRow/OpacitySlider" to="." method="_on_opacity_changed"]
[connection signal="toggled" from="SettingsPanel/Panel/VBox/JoystickToggle" to="." method="_on_joystick_toggled"]
[connection signal="pressed" from="SettingsPanel/Panel/VBox/BackBtn" to="." method="_on_settings_back_pressed"]
+169 -15
View File
@@ -78,6 +78,17 @@ var spawn_point_selected = false
# Action for hilighter
var highlighted_spawn_points = []
var _is_highlighting: bool = false
# Character selection and animation
@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var character_bob: Node3D = $Bob
@onready var character_masbro: Node3D = $Masbro
@onready var character_gatot: Node3D = $Gatot
@onready var character_oldpop: Node3D = $Oldpop
var selected_character: String = "Masbro" # Default character (matches tscn default visibility)
const AVAILABLE_CHARACTERS: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"]
@export var movement_range: int = 1:
@@ -124,6 +135,9 @@ func _ready():
_init_managers()
# Initialize character selection from LobbyManager
_setup_character()
# Early setup for bots
if is_bot == true or is_in_group("Bots"):
# Initialize behavior tree for bots
@@ -162,11 +176,13 @@ func _ready():
enhanced_gridmap.initialize_astar()
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
# Request current spawn positions before highlighting
request_spawn_positions_update()
highlight_available_spawn_points()
# Remove this line as goals are now managed by the host
# Skip manual spawn selection if random spawn is enabled
# Host will assign positions via RPC
if not LobbyManager.get_randomize_spawn():
# Request current spawn positions before highlighting
request_spawn_positions_update()
highlight_available_spawn_points()
# Remove this line as goals are now managed by the host
#append_random_goals()
playerboard.resize(25)
@@ -181,21 +197,26 @@ func _ready():
if enhanced_gridmap:
enhanced_gridmap.initialize_astar()
enhanced_gridmap.set_diagonal_movement(use_diagonal_movement)
current_position = find_valid_starting_position()
update_player_position(current_position)
# Only set position if not using random spawn (host will assign via RPC)
# AND not already assigned
if not LobbyManager.get_randomize_spawn() and not spawn_point_selected:
current_position = find_valid_starting_position()
update_player_position(current_position)
#append_random_goals()
playerboard.resize(25)
playerboard.fill(-1)
# Ensure proper initial positioning
global_position = Vector3(
current_position.x * cell_size.x + cell_size.x * 0.5,
1.0,
current_position.y * cell_size.z + cell_size.z * 0.5
)
if is_multiplayer_authority():
rpc("sync_position", current_position)
# Ensure proper initial positioning (only if NOT using random spawn and not already positioned)
# When random spawn is enabled, the host assigns positions via set_spawn_position RPC
if not LobbyManager.get_randomize_spawn() and not spawn_point_selected:
global_position = Vector3(
current_position.x * cell_size.x + cell_size.x * 0.5,
1.0,
current_position.y * cell_size.z + cell_size.z * 0.5
)
if is_multiplayer_authority():
rpc("sync_position", current_position)
func _init_managers():
movement_manager = load("res://scripts/managers/player_movement_manager.gd").new()
@@ -233,6 +254,125 @@ func _init_managers():
add_child(powerup_manager)
powerup_manager.initialize(self, enhanced_gridmap)
# =============================================================================
# Character Selection
# =============================================================================
func set_character(character_name: String) -> void:
"""Show only the selected character model and hide others. Updates AnimationPlayer root."""
if character_name not in AVAILABLE_CHARACTERS:
push_warning("Invalid character name: %s" % character_name)
return
selected_character = character_name
# Hide all character models
if character_bob: character_bob.visible = false
if character_masbro: character_masbro.visible = false
if character_gatot: character_gatot.visible = false
if character_oldpop: character_oldpop.visible = false
# Show selected character and update AnimationPlayer root
var active_character: Node3D = null
match character_name:
"Bob":
if character_bob:
character_bob.visible = true
active_character = character_bob
"Masbro":
if character_masbro:
character_masbro.visible = true
active_character = character_masbro
"Gatot":
if character_gatot:
character_gatot.visible = true
active_character = character_gatot
"Oldpop":
if character_oldpop:
character_oldpop.visible = true
active_character = character_oldpop
# Update AnimationPlayer's root node to point to active character
if anim_player and active_character:
anim_player.root_node = anim_player.get_path_to(active_character)
# Start with idle animation
play_idle_animation()
@rpc("any_peer", "call_local", "reliable")
func sync_character(character_name: String) -> void:
"""Sync character selection across all clients."""
set_character(character_name)
func _setup_character() -> void:
"""Initialize character based on LobbyManager selection or defaults."""
var character_name = "Masbro" # Default
var player_authority_id = get_multiplayer_authority()
# Look up character from LobbyManager for this player (works for all players)
if LobbyManager:
var players = LobbyManager.get_players()
for player_data in players:
if player_data.get("id") == player_authority_id:
character_name = player_data.get("character", "Masbro")
break
set_character(character_name)
# If this is our local player, also sync to other clients for late joiners
if is_multiplayer_authority():
rpc("sync_character", character_name)
# =============================================================================
# Animation Functions
# =============================================================================
# Animation speed multiplier for fast-paced gameplay
const ANIMATION_SPEED: float = 2.0
func play_walk_animation() -> void:
"""Play walking animation at increased speed."""
if anim_player and anim_player.has_animation("animation-pack/walk_forward"):
anim_player.play("animation-pack/walk_forward", -1, ANIMATION_SPEED)
func play_pickup_animation() -> void:
"""Play pickup/grab tile animation at increased speed."""
if anim_player and anim_player.has_animation("animation-pack/take_tile_1"):
anim_player.play("animation-pack/take_tile_1", -1, ANIMATION_SPEED)
func play_put_animation() -> void:
"""Play put/drop tile animation at increased speed."""
if anim_player and anim_player.has_animation("animation-pack/drop_tile_1"):
anim_player.play("animation-pack/drop_tile_1", -1, ANIMATION_SPEED)
func play_special_animation() -> void:
"""Play special ability animation (backflip) at increased speed."""
if anim_player and anim_player.has_animation("animation-pack/backflip_1"):
anim_player.play("animation-pack/backflip_1", -1, ANIMATION_SPEED * 1.5)
func play_idle_animation() -> void:
"""Play idle animation at normal speed."""
if anim_player and anim_player.has_animation("animation-pack/idle"):
anim_player.play("animation-pack/idle")
# =============================================================================
# Screen Shake
# =============================================================================
@rpc("any_peer", "call_local", "reliable")
func trigger_screen_shake(shake_type: String) -> void:
"""Trigger screen shake effect. Called via RPC when targeted or completing goals."""
var main = get_tree().get_root().get_node_or_null("Main")
if main:
var screen_shake_manager = main.get_node_or_null("ScreenShakeManager")
if screen_shake_manager:
match shake_type:
"targeted":
screen_shake_manager.shake_targeted()
"goal":
screen_shake_manager.shake_goal_complete()
_:
screen_shake_manager.shake_light()
# Add function to check if position is at finish line
func is_at_finish_line() -> bool:
return race_manager.is_at_finish_line()
@@ -1121,6 +1261,20 @@ func sync_position(pos: Vector2i):
current_position.y * cell_size.z + cell_size.z * 0.5
) + cell_offset
@rpc("any_peer", "call_local", "reliable")
func set_spawn_position(pos: Vector2i):
"""Set spawn position - used by random spawn system."""
current_position = pos
spawn_point_selected = true
# Clear any spawn highlights
clear_spawn_highlights()
# Update visual position
global_position = Vector3(
current_position.x * cell_size.x + cell_size.x * 0.5,
cell_size.y,
current_position.y * cell_size.z + cell_size.z * 0.5
) + cell_offset
func highlight_valid_obstacle_cells():
action_manager.highlight_valid_obstacle_cells()
+1
View File
@@ -34,6 +34,7 @@ spacing_glyph = 5
[node name="CharacterBody3D" type="CharacterBody3D"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.5, 0)
collision_layer = 2
script = ExtResource("1_qecr4")
cell_size = Vector3(1, 1, 1)
use_diagonal_movement = true