feat: Implement a new lobby system with configurable match duration, game over screen, and core game state management.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
[ ADT's Daily Report - 2025-12-18 ]
|
||||
|
||||
✅ Global Match Timer System - Added configurable match duration that can be set by host in lobby (1, 2, 3, 5, or 10 minutes). The timer displays at the top center of the screen in mm:ss format and syncs across all clients via RPC. Modified lobby_manager.gd, lobby.tscn, lobby.gd, and goals_cycle_manager.gd.
|
||||
|
||||
✅ Game Over Screen - When global timer reaches 0, the match ends and displays a full-screen "TIME'S UP!" overlay with final leaderboard showing 🥇🥈🥉 rankings for all players. Player controls are disabled when match ends.
|
||||
|
||||
✅ Back to Main Menu - Added "BACK TO MAIN MENU" button on the game over screen that properly cleans up game state (GameStateManager.end_game(), LobbyManager.reset()) and transitions back to lobby.tscn.
|
||||
|
||||
✅ Goals Cycle Integration - Extended goals_cycle_manager.gd with global match timer (start_match, match_ended signal). The 30-second goal cycles continue until the global match timer expires, at which point no new cycles start.
|
||||
@@ -0,0 +1,36 @@
|
||||
[ ADT's Daily Report - 2025-12-19 ]
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
✅ **GDScript Parse Errors Fixed** - Resolved type inference errors in `boot_screen.gd` and `admin_panel.gd` by using explicit type annotations instead of inferred typing for variables where the type couldn't be determined (e.g., `await` returns, `Dictionary.get()` comparisons).
|
||||
|
||||
## Lobby UI Redesign
|
||||
|
||||
✅ **Match Duration Sync** - Added client-side read-only display for match duration. Host sees OptionButton to change, clients see Label text that updates via RPC when host changes duration.
|
||||
|
||||
✅ **Character Selection System** - Implemented in `lobby_manager.gd`:
|
||||
- 4 characters available: Bob, Gatot, Masbro, Oldpop
|
||||
- `cycle_character(direction)` with ◀ ▶ buttons
|
||||
- RPC sync via `sync_character()` to broadcast selections to all players
|
||||
- Character expression textures used as preview images
|
||||
|
||||
✅ **Area Selection System** - Host-only area selector:
|
||||
- 4 placeholder areas: Desert, Forest, City, Factory
|
||||
- `cycle_area(direction)` with ◀ ▶ buttons (disabled for clients)
|
||||
- RPC sync via `sync_area()` to show current area to all players
|
||||
|
||||
✅ **New Lobby Layout** (`lobby.tscn` redesign):
|
||||
- Top bar: Profile, Logout, Match ID + Copy, Duration setting
|
||||
- Host banner (yellow "HOST" label)
|
||||
- 4 player slots with: character preview image, ◀ ▶ nav buttons, ready status
|
||||
- Area selector at bottom-left
|
||||
- Bottom bar: Leave, Ready, Match buttons
|
||||
|
||||
✅ **Player Slot Character Navigation** - Fixed client character selector arrows missing by adding CharacterNav (◀ ▶) to all 4 player slots. Local player sees navigation buttons, other players see just character name label.
|
||||
|
||||
## Files Modified
|
||||
- `scripts/ui/boot_screen.gd` - Type annotation fix
|
||||
- `scripts/ui/admin_panel.gd` - Type annotation fixes
|
||||
- `scripts/managers/lobby_manager.gd` - Character/area selection data, signals, RPCs
|
||||
- `scenes/lobby.tscn` - Complete UI redesign with player slots
|
||||
- `scenes/lobby.gd` - New UI logic for character/area selection
|
||||
@@ -1,7 +1,7 @@
|
||||
[gd_resource type="StyleBoxFlat" format=3 uid="uid://dlw1ogamn741n"]
|
||||
|
||||
[resource]
|
||||
bg_color = Color(0, 0, 0, 0.784314)
|
||||
bg_color = Color(0, 0, 0, 0.588235)
|
||||
corner_radius_top_left = 5
|
||||
corner_radius_top_right = 5
|
||||
corner_radius_bottom_right = 5
|
||||
|
||||
+211
-178
@@ -14,22 +14,44 @@ extends Control
|
||||
@onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn
|
||||
@onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn
|
||||
|
||||
# UI References - Lobby
|
||||
# UI References - Lobby Panel
|
||||
@onready var lobby_panel = $LobbyPanel
|
||||
@onready var room_name_header = $LobbyPanel/VBoxContainer/RoomNameHeader
|
||||
@onready var match_id_display = $LobbyPanel/VBoxContainer/MatchIdContainer/MatchIdDisplay
|
||||
@onready var copy_id_btn = $LobbyPanel/VBoxContainer/MatchIdContainer/CopyIdBtn
|
||||
@onready var player_list = $LobbyPanel/VBoxContainer/PlayerList
|
||||
@onready var status_label = $LobbyPanel/VBoxContainer/StatusLabel
|
||||
@onready var ready_btn = $LobbyPanel/VBoxContainer/ButtonContainer/ReadyBtn
|
||||
@onready var start_game_btn = $LobbyPanel/VBoxContainer/ButtonContainer/StartGameBtn
|
||||
@onready var leave_btn = $LobbyPanel/VBoxContainer/ButtonContainer/LeaveBtn
|
||||
@onready var room_name_header = $LobbyPanel/RoomNameHeader
|
||||
@onready var host_banner = $LobbyPanel/HostBanner
|
||||
@onready var host_banner_label = $LobbyPanel/HostBanner/HostBannerLabel
|
||||
|
||||
|
||||
# UI References - Top Bar
|
||||
@onready var profile_btn = $LobbyPanel/TopBar/ProfileSection/ProfileBtn
|
||||
@onready var logout_btn = $LobbyPanel/TopBar/ProfileSection/LogoutBtn
|
||||
@onready var match_id_display = $LobbyPanel/TopBar/MatchIdContainer/MatchIdDisplay
|
||||
@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
|
||||
|
||||
# UI References - Player Slots
|
||||
@onready var players_container = $LobbyPanel/PlayersContainer
|
||||
@onready var player_slots: Array[Control] = []
|
||||
|
||||
# UI References - Area Selector
|
||||
@onready var area_selector = $LobbyPanel/AreaSelector
|
||||
@onready var area_left_btn = $LobbyPanel/AreaSelector/AreaLeftBtn
|
||||
@onready var area_name_label = $LobbyPanel/AreaSelector/AreaName
|
||||
@onready var area_right_btn = $LobbyPanel/AreaSelector/AreaRightBtn
|
||||
|
||||
# UI References - Bottom Bar
|
||||
@onready var leave_btn = $LobbyPanel/BottomBar/LeaveBtn
|
||||
@onready var ready_btn = $LobbyPanel/BottomBar/ReadyBtn
|
||||
@onready var start_game_btn = $LobbyPanel/BottomBar/StartGameBtn
|
||||
@onready var status_label = $LobbyPanel/StatusLabel
|
||||
|
||||
# UI References - Status
|
||||
@onready var connection_status = $StatusBar/ConnectionStatus
|
||||
|
||||
# UI References - User Profile Bar (will be added to scene)
|
||||
var user_profile_bar: Control
|
||||
# Character preview textures
|
||||
var character_textures: Dictionary = {}
|
||||
|
||||
# Profile panel instance
|
||||
var profile_panel_instance: Control
|
||||
var admin_panel_instance: Control
|
||||
|
||||
@@ -39,27 +61,38 @@ var current_match_id: String = ""
|
||||
func _ready():
|
||||
# Check if user is authenticated
|
||||
if not AuthManager.is_logged_in():
|
||||
# Redirect to login screen - must use deferred call during _ready()
|
||||
call_deferred("_go_to_login")
|
||||
return
|
||||
|
||||
# Initialize user profile bar
|
||||
_setup_user_profile_bar()
|
||||
# Load character textures
|
||||
_load_character_textures()
|
||||
|
||||
# Get player slot references
|
||||
_setup_player_slots()
|
||||
|
||||
# Set player name from profile
|
||||
if player_name_input:
|
||||
player_name_input.text = UserProfileManager.get_display_name()
|
||||
|
||||
# Connect button signals
|
||||
# Connect button signals - Main Menu
|
||||
create_room_btn.pressed.connect(_on_create_room_pressed)
|
||||
browse_rooms_btn.pressed.connect(_on_browse_rooms_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)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
ready_btn.toggled.connect(_on_ready_toggled)
|
||||
start_game_btn.pressed.connect(_on_start_game_pressed)
|
||||
leave_btn.pressed.connect(_on_leave_pressed)
|
||||
copy_id_btn.pressed.connect(_on_copy_id_pressed)
|
||||
|
||||
# Connect LobbyManager signals
|
||||
LobbyManager.room_list_updated.connect(_on_room_list_updated)
|
||||
@@ -70,6 +103,10 @@ func _ready():
|
||||
LobbyManager.ready_state_changed.connect(_on_ready_state_changed)
|
||||
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.character_changed.connect(_on_character_changed)
|
||||
LobbyManager.area_changed.connect(_on_area_changed)
|
||||
LobbyManager.player_list_changed.connect(_update_player_slots)
|
||||
|
||||
# Connect NakamaManager signals
|
||||
NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama)
|
||||
@@ -77,144 +114,35 @@ func _ready():
|
||||
|
||||
# Show main menu initially
|
||||
_show_panel("main_menu")
|
||||
_update_profile_bar()
|
||||
|
||||
# =============================================================================
|
||||
# User Profile Bar
|
||||
# Setup
|
||||
# =============================================================================
|
||||
|
||||
func _setup_user_profile_bar() -> void:
|
||||
# Create profile bar dynamically (or get reference if in scene)
|
||||
user_profile_bar = _create_profile_bar()
|
||||
add_child(user_profile_bar)
|
||||
func _load_character_textures() -> void:
|
||||
"""Load character expression textures for preview."""
|
||||
var characters = ["Bob", "Gatot", "Masbro", "Oldpop"]
|
||||
for char_name in characters:
|
||||
var tex_path = "res://assets/characters/%s_%s-expression.png" % [char_name, char_name.to_lower()]
|
||||
if ResourceLoader.exists(tex_path):
|
||||
character_textures[char_name] = load(tex_path)
|
||||
else:
|
||||
print("[Lobby] Character texture not found: ", tex_path)
|
||||
|
||||
func _create_profile_bar() -> Control:
|
||||
var bar := HBoxContainer.new()
|
||||
bar.name = "UserProfileBar"
|
||||
bar.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||
bar.offset_top = 5
|
||||
bar.offset_bottom = 45
|
||||
bar.offset_left = 10
|
||||
bar.offset_right = -10
|
||||
|
||||
# Avatar
|
||||
var avatar := TextureRect.new()
|
||||
avatar.name = "Avatar"
|
||||
avatar.custom_minimum_size = Vector2(35, 35)
|
||||
avatar.expand_mode = TextureRect.EXPAND_FIT_WIDTH
|
||||
avatar.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
bar.add_child(avatar)
|
||||
|
||||
# Display name
|
||||
var name_label := Label.new()
|
||||
name_label.name = "DisplayName"
|
||||
name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
name_label.add_theme_color_override("font_color", Color(0, 0.831, 1))
|
||||
bar.add_child(name_label)
|
||||
|
||||
# Account type badge
|
||||
var badge := Label.new()
|
||||
badge.name = "AccountBadge"
|
||||
badge.add_theme_font_size_override("font_size", 10)
|
||||
badge.add_theme_color_override("font_color", Color(0.5, 0.5, 0.6))
|
||||
bar.add_child(badge)
|
||||
|
||||
# Profile button
|
||||
var profile_btn := Button.new()
|
||||
profile_btn.name = "ProfileBtn"
|
||||
profile_btn.text = "Profile"
|
||||
profile_btn.pressed.connect(_on_profile_btn_pressed)
|
||||
bar.add_child(profile_btn)
|
||||
|
||||
# Admin button (only visible for admins/hosts)
|
||||
var admin_btn := Button.new()
|
||||
admin_btn.name = "AdminBtn"
|
||||
admin_btn.text = "Admin"
|
||||
admin_btn.visible = false # Hidden by default, shown if user is admin
|
||||
admin_btn.pressed.connect(_on_admin_btn_pressed)
|
||||
bar.add_child(admin_btn)
|
||||
|
||||
# Logout button
|
||||
var logout_btn := Button.new()
|
||||
logout_btn.name = "LogoutBtn"
|
||||
logout_btn.text = "Logout"
|
||||
logout_btn.pressed.connect(_on_logout_pressed)
|
||||
bar.add_child(logout_btn)
|
||||
|
||||
return bar
|
||||
|
||||
func _update_profile_bar() -> void:
|
||||
if not user_profile_bar:
|
||||
return
|
||||
|
||||
var name_label := user_profile_bar.get_node_or_null("DisplayName") as Label
|
||||
if name_label:
|
||||
name_label.text = UserProfileManager.get_display_name()
|
||||
|
||||
var badge := user_profile_bar.get_node_or_null("AccountBadge") as Label
|
||||
if badge:
|
||||
badge.text = "[Guest]" if AuthManager.is_guest else "[Registered]"
|
||||
|
||||
var avatar := user_profile_bar.get_node_or_null("Avatar") as TextureRect
|
||||
if avatar:
|
||||
var avatar_url := UserProfileManager.get_avatar_url()
|
||||
if ResourceLoader.exists(avatar_url):
|
||||
avatar.texture = load(avatar_url)
|
||||
|
||||
# Show admin button if user is admin or host
|
||||
var admin_btn := user_profile_bar.get_node_or_null("AdminBtn") as Button
|
||||
if admin_btn:
|
||||
# Check if user is admin (you can define admin check logic here)
|
||||
var is_admin = _check_if_admin()
|
||||
admin_btn.visible = is_admin
|
||||
|
||||
func _on_profile_btn_pressed() -> void:
|
||||
# Show profile panel
|
||||
if not profile_panel_instance:
|
||||
var profile_panel_scene := load("res://scenes/ui/profile_panel.tscn")
|
||||
profile_panel_instance = profile_panel_scene.instantiate()
|
||||
profile_panel_instance.closed.connect(func(): profile_panel_instance.hide())
|
||||
profile_panel_instance.profile_updated.connect(_update_profile_bar)
|
||||
add_child(profile_panel_instance)
|
||||
|
||||
profile_panel_instance.show_panel()
|
||||
# Center the panel
|
||||
profile_panel_instance.position = (get_viewport_rect().size - profile_panel_instance.size) / 2
|
||||
|
||||
func _on_logout_pressed() -> void:
|
||||
AuthManager.logout()
|
||||
_go_to_login()
|
||||
|
||||
func _go_to_login() -> void:
|
||||
if get_tree():
|
||||
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
||||
|
||||
func _on_admin_btn_pressed() -> void:
|
||||
# Show admin panel
|
||||
if not admin_panel_instance:
|
||||
var admin_panel_scene := load("res://scenes/ui/admin_panel.tscn")
|
||||
admin_panel_instance = admin_panel_scene.instantiate()
|
||||
# Connect close signal if available
|
||||
if admin_panel_instance.has_signal("closed"):
|
||||
admin_panel_instance.closed.connect(func(): admin_panel_instance.hide())
|
||||
add_child(admin_panel_instance)
|
||||
|
||||
admin_panel_instance.show()
|
||||
# Center the panel
|
||||
admin_panel_instance.position = (get_viewport_rect().size - admin_panel_instance.size) / 2
|
||||
|
||||
func _check_if_admin() -> bool:
|
||||
# Check if user is admin - can be host or have specific admin role
|
||||
# You can extend this to check Nakama user metadata or roles
|
||||
if LobbyManager.is_host:
|
||||
return true
|
||||
|
||||
# Check if user has admin role in their profile (optional)
|
||||
var user_id = AuthManager.get_user_id() if AuthManager.has_method("get_user_id") else ""
|
||||
# Add your admin user IDs here or check from Nakama metadata
|
||||
var admin_user_ids = ["admin_user_id_1", "admin_user_id_2"] # Configure as needed
|
||||
|
||||
return user_id in admin_user_ids
|
||||
func _setup_player_slots() -> void:
|
||||
"""Get references to all player slot nodes."""
|
||||
player_slots.clear()
|
||||
for i in range(1, 5):
|
||||
var slot = players_container.get_node_or_null("PlayerSlot%d" % i)
|
||||
if slot:
|
||||
player_slots.append(slot)
|
||||
# Connect character navigation buttons for all slots
|
||||
var left_btn = slot.get_node_or_null("CharacterNav%d/CharLeftBtn%d" % [i, i])
|
||||
var right_btn = slot.get_node_or_null("CharacterNav%d/CharRightBtn%d" % [i, i])
|
||||
if left_btn:
|
||||
left_btn.pressed.connect(func(): LobbyManager.cycle_character(-1))
|
||||
if right_btn:
|
||||
right_btn.pressed.connect(func(): LobbyManager.cycle_character(1))
|
||||
|
||||
# =============================================================================
|
||||
# Panel Management
|
||||
@@ -230,13 +158,11 @@ func _show_panel(panel_name: String) -> void:
|
||||
# =============================================================================
|
||||
|
||||
func _on_create_room_pressed() -> void:
|
||||
# Set player name
|
||||
LobbyManager.local_player_name = player_name_input.text.strip_edges()
|
||||
if LobbyManager.local_player_name.is_empty():
|
||||
LobbyManager.local_player_name = "Host"
|
||||
|
||||
connection_status.text = "Creating room..."
|
||||
# Room name auto-generated since Nakama doesn't support custom names
|
||||
LobbyManager.create_room("Room %d" % randi_range(1000, 9999))
|
||||
|
||||
func _on_browse_rooms_pressed() -> void:
|
||||
@@ -254,11 +180,9 @@ func _on_refresh_pressed() -> void:
|
||||
LobbyManager.refresh_room_list()
|
||||
|
||||
func _on_join_pressed() -> void:
|
||||
# First check if there's a match ID entered
|
||||
var match_id = match_id_input.text.strip_edges()
|
||||
|
||||
if match_id.is_empty():
|
||||
# Try to use selected room from list
|
||||
var selected_items = room_list.get_selected_items()
|
||||
if selected_items.size() == 0:
|
||||
connection_status.text = "Please select a room or enter Match ID"
|
||||
@@ -272,7 +196,6 @@ func _on_join_pressed() -> void:
|
||||
connection_status.text = "No room selected"
|
||||
return
|
||||
|
||||
# Set player name
|
||||
LobbyManager.local_player_name = player_name_input.text.strip_edges()
|
||||
if LobbyManager.local_player_name.is_empty():
|
||||
LobbyManager.local_player_name = "Player"
|
||||
@@ -303,7 +226,33 @@ func _on_leave_pressed() -> void:
|
||||
|
||||
func _on_copy_id_pressed() -> void:
|
||||
DisplayServer.clipboard_set(current_match_id)
|
||||
connection_status.text = "Match ID copied to clipboard!"
|
||||
status_label.text = "Match ID copied!"
|
||||
|
||||
func _on_duration_selected(index: int) -> void:
|
||||
if not LobbyManager.is_host:
|
||||
return
|
||||
|
||||
var durations = [60, 120, 180, 300, 600]
|
||||
if index >= 0 and index < durations.size():
|
||||
LobbyManager.set_match_duration(durations[index])
|
||||
|
||||
func _on_profile_btn_pressed() -> void:
|
||||
if not profile_panel_instance:
|
||||
var profile_panel_scene := load("res://scenes/ui/profile_panel.tscn")
|
||||
profile_panel_instance = profile_panel_scene.instantiate()
|
||||
profile_panel_instance.closed.connect(func(): profile_panel_instance.hide())
|
||||
add_child(profile_panel_instance)
|
||||
|
||||
profile_panel_instance.show_panel()
|
||||
profile_panel_instance.position = (get_viewport_rect().size - profile_panel_instance.size) / 2
|
||||
|
||||
func _on_logout_pressed() -> void:
|
||||
AuthManager.logout()
|
||||
_go_to_login()
|
||||
|
||||
func _go_to_login() -> void:
|
||||
if get_tree():
|
||||
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
||||
|
||||
# =============================================================================
|
||||
# LobbyManager Signal Handlers
|
||||
@@ -327,12 +276,25 @@ func _on_room_joined(room_data: Dictionary) -> void:
|
||||
_show_panel("lobby")
|
||||
current_match_id = room_data.get("match_id", "")
|
||||
room_name_header.text = "ROOM: %s" % room_data.get("room_name", "Unknown")
|
||||
match_id_display.text = "Match ID: %s " % current_match_id
|
||||
match_id_display.text = "ID: %s" % _truncate_id(current_match_id)
|
||||
|
||||
# Update start button visibility (host only)
|
||||
start_game_btn.visible = LobbyManager.is_host
|
||||
# Configure host-specific UI
|
||||
var is_host = LobbyManager.is_host
|
||||
host_banner.visible = is_host
|
||||
start_game_btn.visible = is_host
|
||||
|
||||
_update_player_list()
|
||||
# Duration: host sees dropdown, clients see text
|
||||
duration_option.visible = is_host
|
||||
duration_text_label.visible = not is_host
|
||||
if not is_host:
|
||||
_update_duration_text_label(LobbyManager.get_match_duration())
|
||||
|
||||
# Area selector: only host can interact
|
||||
area_left_btn.disabled = not is_host
|
||||
area_right_btn.disabled = not is_host
|
||||
area_name_label.text = LobbyManager.get_selected_area()
|
||||
|
||||
_update_player_slots()
|
||||
connection_status.text = "Connected to room"
|
||||
|
||||
func _on_room_left() -> void:
|
||||
@@ -340,33 +302,38 @@ func _on_room_left() -> void:
|
||||
connection_status.text = "Left room"
|
||||
|
||||
func _on_player_joined(player_data: Dictionary) -> void:
|
||||
_update_player_list()
|
||||
_update_player_slots()
|
||||
status_label.text = "%s joined!" % player_data.get("name", "Player")
|
||||
|
||||
func _on_player_left(_player_id: int) -> void:
|
||||
_update_player_list()
|
||||
_update_player_slots()
|
||||
status_label.text = "A player left"
|
||||
|
||||
func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void:
|
||||
_update_player_list()
|
||||
_update_player_slots()
|
||||
_update_status()
|
||||
|
||||
func _on_all_players_ready() -> void:
|
||||
if LobbyManager.is_host:
|
||||
start_game_btn.disabled = false
|
||||
status_label.text = "All players ready! Host can start."
|
||||
status_label.text = "All ready! Start the match!"
|
||||
else:
|
||||
status_label.text = "All players ready! Waiting for host..."
|
||||
status_label.text = "All ready! Waiting for host..."
|
||||
|
||||
func _on_game_starting() -> void:
|
||||
connection_status.text = "Starting game..."
|
||||
# Small delay for visual feedback
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
get_tree().change_scene_to_file("res://scenes/main.tscn")
|
||||
|
||||
# =============================================================================
|
||||
# NakamaManager Signal Handlers
|
||||
# =============================================================================
|
||||
func _on_match_duration_changed(duration_seconds: int) -> void:
|
||||
if not LobbyManager.is_host:
|
||||
_update_duration_text_label(duration_seconds)
|
||||
|
||||
func _on_character_changed(_player_id: int, _character_name: String) -> void:
|
||||
_update_player_slots()
|
||||
|
||||
func _on_area_changed(area_name: String) -> void:
|
||||
area_name_label.text = area_name
|
||||
|
||||
func _on_connected_to_nakama() -> void:
|
||||
connection_status.text = "Connected to server"
|
||||
@@ -376,18 +343,64 @@ func _on_connection_failed(error_message: String) -> void:
|
||||
_show_panel("main_menu")
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# Player Slot Updates
|
||||
# =============================================================================
|
||||
|
||||
func _update_player_list() -> void:
|
||||
player_list.clear()
|
||||
func _update_player_slots() -> void:
|
||||
"""Update all player slot visuals based on current player list."""
|
||||
var players = LobbyManager.get_players()
|
||||
for player in players:
|
||||
var player_name = player.get("name", "Unknown")
|
||||
var is_ready = player.get("is_ready", false)
|
||||
var ready_icon = " ✓" if is_ready else " ✗"
|
||||
var host_tag = " (Host)" if player.get("id") == 1 else ""
|
||||
player_list.add_item("%s%s%s" % [player_name, host_tag, ready_icon])
|
||||
var my_id = multiplayer.get_unique_id()
|
||||
|
||||
for i in range(player_slots.size()):
|
||||
var slot = player_slots[i]
|
||||
var slot_num = i + 1
|
||||
|
||||
if i < players.size():
|
||||
var player = players[i]
|
||||
slot.visible = true
|
||||
|
||||
# Update player name
|
||||
var name_label = slot.get_node_or_null("PlayerName%d" % slot_num)
|
||||
if name_label:
|
||||
var display_name = player.get("name", "Player %d" % slot_num)
|
||||
if player.get("id") == 1:
|
||||
display_name += " (Host)"
|
||||
name_label.text = display_name
|
||||
|
||||
# Update character preview
|
||||
var char_preview = slot.get_node_or_null("CharacterPreview%d" % slot_num)
|
||||
var char_name = player.get("character", "Bob")
|
||||
if char_preview and character_textures.has(char_name):
|
||||
char_preview.texture = character_textures[char_name]
|
||||
|
||||
# Check if this is the local player
|
||||
var is_local_player = player.get("id") == my_id
|
||||
|
||||
# Update character name in nav (inside CharacterNav)
|
||||
var char_name_in_nav = slot.get_node_or_null("CharacterNav%d/CharacterName%d" % [slot_num, slot_num])
|
||||
if char_name_in_nav:
|
||||
char_name_in_nav.text = char_name
|
||||
|
||||
# Update character name label (outside nav, for non-local players)
|
||||
var char_name_label = slot.get_node_or_null("CharacterNameLabel%d" % slot_num)
|
||||
if char_name_label:
|
||||
char_name_label.text = char_name
|
||||
char_name_label.visible = not is_local_player
|
||||
|
||||
# Show/hide character navigation (only for local player)
|
||||
var char_nav = slot.get_node_or_null("CharacterNav%d" % slot_num)
|
||||
if char_nav:
|
||||
char_nav.visible = is_local_player
|
||||
|
||||
# Update ready status
|
||||
var ready_label = slot.get_node_or_null("ReadyStatus%d" % slot_num)
|
||||
if ready_label:
|
||||
var is_ready = player.get("is_ready", false)
|
||||
ready_label.text = "READY ✓" if is_ready else "NOT READY"
|
||||
ready_label.add_theme_color_override("font_color",
|
||||
Color(0.4, 0.8, 0.4) if is_ready else Color(0.6, 0.6, 0.6))
|
||||
else:
|
||||
slot.visible = false
|
||||
|
||||
func _update_status() -> void:
|
||||
var players = LobbyManager.get_players()
|
||||
@@ -400,3 +413,23 @@ func _update_status() -> void:
|
||||
|
||||
if LobbyManager.is_host:
|
||||
start_game_btn.disabled = not LobbyManager.is_all_ready()
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# =============================================================================
|
||||
|
||||
func _update_duration_text_label(duration_seconds: int) -> void:
|
||||
var duration_text: String
|
||||
match duration_seconds:
|
||||
60: duration_text = "1 min"
|
||||
120: duration_text = "2 min"
|
||||
180: duration_text = "3 min"
|
||||
300: duration_text = "5 min"
|
||||
600: duration_text = "10 min"
|
||||
_: duration_text = "%d sec" % duration_seconds
|
||||
duration_text_label.text = duration_text
|
||||
|
||||
func _truncate_id(id: String) -> String:
|
||||
if id.length() > 16:
|
||||
return id.substr(0, 8) + "..." + id.substr(-4)
|
||||
return id
|
||||
|
||||
+479
-59
@@ -20,7 +20,7 @@ anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.12, 0.1, 0.08, 1)
|
||||
color = Color(0.08, 0.12, 0.18, 1)
|
||||
|
||||
[node name="MainMenuPanel" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
@@ -165,90 +165,507 @@ custom_minimum_size = Vector2(110, 44)
|
||||
layout_mode = 2
|
||||
text = "BACK"
|
||||
|
||||
[node name="LobbyPanel" type="PanelContainer" parent="."]
|
||||
[node name="LobbyPanel" type="Control" parent="."]
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="LobbyBackground" type="ColorRect" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
color = Color(0.06, 0.08, 0.12, 1)
|
||||
|
||||
[node name="TopBar" type="HBoxContainer" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 10
|
||||
anchor_right = 1.0
|
||||
offset_left = 20.0
|
||||
offset_top = 10.0
|
||||
offset_right = -20.0
|
||||
offset_bottom = 50.0
|
||||
grow_horizontal = 2
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="ProfileSection" type="HBoxContainer" parent="LobbyPanel/TopBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="ProfileBtn" type="Button" parent="LobbyPanel/TopBar/ProfileSection"]
|
||||
custom_minimum_size = Vector2(80, 32)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "Profile"
|
||||
|
||||
[node name="LogoutBtn" type="Button" parent="LobbyPanel/TopBar/ProfileSection"]
|
||||
custom_minimum_size = Vector2(70, 32)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "Logout"
|
||||
|
||||
[node name="Spacer" type="Control" parent="LobbyPanel/TopBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="MatchIdContainer" type="HBoxContainer" parent="LobbyPanel/TopBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="MatchIdDisplay" type="Label" parent="LobbyPanel/TopBar/MatchIdContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.5, 0.5, 0.55, 1)
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "Match ID: "
|
||||
|
||||
[node name="CopyIdBtn" type="Button" parent="LobbyPanel/TopBar/MatchIdContainer"]
|
||||
custom_minimum_size = Vector2(60, 28)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "COPY"
|
||||
|
||||
[node name="Spacer2" type="Control" parent="LobbyPanel/TopBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="SettingsSection" type="HBoxContainer" parent="LobbyPanel/TopBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="DurationLabel" type="Label" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "Duration:"
|
||||
|
||||
[node name="DurationOption" type="OptionButton" parent="LobbyPanel/TopBar/SettingsSection"]
|
||||
custom_minimum_size = Vector2(90, 28)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 11
|
||||
selected = 2
|
||||
item_count = 5
|
||||
popup/item_0/text = "1 min"
|
||||
popup/item_0/id = 0
|
||||
popup/item_1/text = "2 min"
|
||||
popup/item_1/id = 1
|
||||
popup/item_2/text = "3 min"
|
||||
popup/item_2/id = 2
|
||||
popup/item_3/text = "5 min"
|
||||
popup/item_3/id = 3
|
||||
popup/item_4/text = "10 min"
|
||||
popup/item_4/id = 4
|
||||
|
||||
[node name="DurationTextLabel" 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 = "3 min"
|
||||
|
||||
[node name="HostBanner" type="PanelContainer" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -150.0
|
||||
offset_top = 99.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = 159.0
|
||||
grow_horizontal = 2
|
||||
|
||||
[node name="HostBannerLabel" type="Label" parent="LobbyPanel/HostBanner"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.1, 0.1, 0.1, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
text = "HOST"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="RoomNameHeader" type="Label" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
anchor_right = 0.5
|
||||
offset_left = -200.0
|
||||
offset_top = 55.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 95.0
|
||||
grow_horizontal = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 20
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="PlayersContainer" type="HBoxContainer" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -300.0
|
||||
offset_top = -260.0
|
||||
offset_right = 300.0
|
||||
offset_bottom = 260.0
|
||||
offset_left = -450.0
|
||||
offset_top = -120.0
|
||||
offset_right = 450.0
|
||||
offset_bottom = 200.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 30
|
||||
alignment = 1
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="LobbyPanel"]
|
||||
[node name="PlayerSlot1" type="VBoxContainer" parent="LobbyPanel/PlayersContainer"]
|
||||
custom_minimum_size = Vector2(180, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="RoomNameHeader" type="Label" parent="LobbyPanel/VBoxContainer"]
|
||||
[node name="PlayerName1" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot1"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "ROOM: "
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Player 1"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="MatchIdContainer" type="HBoxContainer" parent="LobbyPanel/VBoxContainer"]
|
||||
[node name="CharacterPreview1" type="TextureRect" parent="LobbyPanel/PlayersContainer/PlayerSlot1"]
|
||||
custom_minimum_size = Vector2(140, 140)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CharacterNav1" type="HBoxContainer" parent="LobbyPanel/PlayersContainer/PlayerSlot1"]
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="MatchIdDisplay" type="Label" parent="LobbyPanel/VBoxContainer/MatchIdContainer"]
|
||||
[node name="CharLeftBtn1" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot1/CharacterNav1"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.5, 0.5, 0.55, 1)
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "Match ID: "
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "◀"
|
||||
|
||||
[node name="CopyIdBtn" type="Button" parent="LobbyPanel/VBoxContainer/MatchIdContainer"]
|
||||
custom_minimum_size = Vector2(80, 32)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "COPY"
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="LobbyPanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PlayersLabel" type="Label" parent="LobbyPanel/VBoxContainer"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "PLAYERS"
|
||||
|
||||
[node name="PlayerList" type="ItemList" parent="LobbyPanel/VBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 160)
|
||||
layout_mode = 2
|
||||
allow_reselect = true
|
||||
|
||||
[node name="StatusLabel" type="Label" parent="LobbyPanel/VBoxContainer"]
|
||||
[node name="CharacterName1" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot1/CharacterNav1"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Waiting for players..."
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="CharRightBtn1" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot1/CharacterNav1"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "▶"
|
||||
|
||||
[node name="CharacterNameLabel1" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot1"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ButtonContainer" type="HBoxContainer" parent="LobbyPanel/VBoxContainer"]
|
||||
[node name="ReadyStatus1" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot1"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "NOT READY"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayerSlot2" type="VBoxContainer" parent="LobbyPanel/PlayersContainer"]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(180, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="ReadyBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"]
|
||||
custom_minimum_size = Vector2(110, 48)
|
||||
[node name="PlayerName2" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot2"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Player 2"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CharacterPreview2" type="TextureRect" parent="LobbyPanel/PlayersContainer/PlayerSlot2"]
|
||||
custom_minimum_size = Vector2(140, 140)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CharacterNav2" type="HBoxContainer" parent="LobbyPanel/PlayersContainer/PlayerSlot2"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="CharLeftBtn2" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot2/CharacterNav2"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "◀"
|
||||
|
||||
[node name="CharacterName2" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot2/CharacterNav2"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="CharRightBtn2" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot2/CharacterNav2"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "▶"
|
||||
|
||||
[node name="CharacterNameLabel2" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot2"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ReadyStatus2" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot2"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "NOT READY"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayerSlot3" type="VBoxContainer" parent="LobbyPanel/PlayersContainer"]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(180, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="PlayerName3" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot3"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Player 3"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CharacterPreview3" type="TextureRect" parent="LobbyPanel/PlayersContainer/PlayerSlot3"]
|
||||
custom_minimum_size = Vector2(140, 140)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CharacterNav3" type="HBoxContainer" parent="LobbyPanel/PlayersContainer/PlayerSlot3"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="CharLeftBtn3" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot3/CharacterNav3"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "◀"
|
||||
|
||||
[node name="CharacterName3" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot3/CharacterNav3"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="CharRightBtn3" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot3/CharacterNav3"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "▶"
|
||||
|
||||
[node name="CharacterNameLabel3" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot3"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ReadyStatus3" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot3"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "NOT READY"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayerSlot4" type="VBoxContainer" parent="LobbyPanel/PlayersContainer"]
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(180, 0)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
alignment = 1
|
||||
|
||||
[node name="PlayerName4" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot4"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(1, 1, 1, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Player 4"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="CharacterPreview4" type="TextureRect" parent="LobbyPanel/PlayersContainer/PlayerSlot4"]
|
||||
custom_minimum_size = Vector2(140, 140)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="CharacterNav4" type="HBoxContainer" parent="LobbyPanel/PlayersContainer/PlayerSlot4"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
alignment = 1
|
||||
|
||||
[node name="CharLeftBtn4" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot4/CharacterNav4"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "◀"
|
||||
|
||||
[node name="CharacterName4" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot4/CharacterNav4"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="CharRightBtn4" type="Button" parent="LobbyPanel/PlayersContainer/PlayerSlot4/CharacterNav4"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "▶"
|
||||
|
||||
[node name="CharacterNameLabel4" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot4"]
|
||||
custom_minimum_size = Vector2(80, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "Bob"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="ReadyStatus4" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot4"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "NOT READY"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="AreaSelector" type="HBoxContainer" parent="LobbyPanel"]
|
||||
clip_contents = true
|
||||
layout_mode = 1
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -660.0
|
||||
offset_top = -137.0
|
||||
offset_right = -412.0
|
||||
offset_bottom = -87.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
[node name="AreaLabel" type="Label" parent="LobbyPanel/AreaSelector"]
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.69, 0.529, 0.357, 1)
|
||||
theme_override_font_sizes/font_size = 13
|
||||
text = "AREA: "
|
||||
|
||||
[node name="AreaLeftBtn" type="Button" parent="LobbyPanel/AreaSelector"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "◀"
|
||||
|
||||
[node name="AreaName" type="Label" parent="LobbyPanel/AreaSelector"]
|
||||
custom_minimum_size = Vector2(100, 0)
|
||||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = "Desert"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AreaRightBtn" type="Button" parent="LobbyPanel/AreaSelector"]
|
||||
custom_minimum_size = Vector2(40, 36)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "▶"
|
||||
|
||||
[node name="BottomBar" type="HBoxContainer" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 20.0
|
||||
offset_top = -70.0
|
||||
offset_right = -20.0
|
||||
offset_bottom = -20.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
theme_override_constants/separation = 20
|
||||
|
||||
[node name="LeaveBtn" type="Button" parent="LobbyPanel/BottomBar"]
|
||||
custom_minimum_size = Vector2(120, 44)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "LEAVE"
|
||||
|
||||
[node name="Spacer" type="Control" parent="LobbyPanel/BottomBar"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ReadyBtn" type="Button" parent="LobbyPanel/BottomBar"]
|
||||
custom_minimum_size = Vector2(140, 50)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
toggle_mode = true
|
||||
text = "READY"
|
||||
|
||||
[node name="StartGameBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"]
|
||||
custom_minimum_size = Vector2(140, 48)
|
||||
[node name="StartGameBtn" type="Button" parent="LobbyPanel/BottomBar"]
|
||||
custom_minimum_size = Vector2(140, 50)
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 16
|
||||
disabled = true
|
||||
text = "START GAME"
|
||||
text = "MATCH"
|
||||
|
||||
[node name="LeaveBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"]
|
||||
custom_minimum_size = Vector2(110, 48)
|
||||
layout_mode = 2
|
||||
text = "LEAVE"
|
||||
[node name="StatusLabel" type="Label" parent="LobbyPanel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 7
|
||||
anchor_left = 0.5
|
||||
anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -150.0
|
||||
offset_top = -90.0
|
||||
offset_right = 150.0
|
||||
offset_bottom = -75.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
|
||||
theme_override_font_sizes/font_size = 12
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="PlayerList" type="ItemList" parent="LobbyPanel"]
|
||||
visible = false
|
||||
layout_mode = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
|
||||
[node name="StatusBar" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
@@ -256,10 +673,10 @@ anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 24.0
|
||||
offset_top = -72.0
|
||||
offset_right = -24.0
|
||||
offset_bottom = -24.0
|
||||
offset_left = 464.0
|
||||
offset_top = -70.0
|
||||
offset_right = -461.0
|
||||
offset_bottom = -21.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
|
||||
@@ -272,14 +689,17 @@ horizontal_alignment = 1
|
||||
|
||||
[node name="VersionLabel" type="Label" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 1
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -130.0
|
||||
offset_top = 20.0
|
||||
offset_right = -20.0
|
||||
offset_bottom = 40.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -124.0
|
||||
offset_top = -95.0
|
||||
offset_right = -14.0
|
||||
offset_bottom = -75.0
|
||||
grow_horizontal = 0
|
||||
grow_vertical = 0
|
||||
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 0.6)
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "v0.1.0 ALPHA"
|
||||
|
||||
+203
-2
@@ -30,6 +30,7 @@ func _ready():
|
||||
ui_manager.setup_leaderboard_ui(self)
|
||||
ui_manager.setup_powerup_bar_ui(self)
|
||||
_setup_obstacle_ui()
|
||||
_setup_global_match_timer_ui()
|
||||
|
||||
# Auto-start game if coming from lobby (already connected to match)
|
||||
if NakamaManager.is_connected_to_nakama() and multiplayer.get_unique_id() != 0:
|
||||
@@ -59,6 +60,8 @@ func _init_managers():
|
||||
goals_cycle_manager.timer_updated.connect(_on_timer_updated)
|
||||
goals_cycle_manager.score_updated.connect(_on_score_updated)
|
||||
goals_cycle_manager.leaderboard_updated.connect(_on_leaderboard_updated)
|
||||
goals_cycle_manager.global_timer_updated.connect(_on_global_timer_updated)
|
||||
goals_cycle_manager.match_ended.connect(_on_match_ended)
|
||||
|
||||
# Message Bar Configuration
|
||||
const MAX_MESSAGES := 5
|
||||
@@ -181,6 +184,54 @@ func _setup_obstacle_ui():
|
||||
)
|
||||
$ActionMenu/ActionButtonContainer.add_child(type_button)
|
||||
|
||||
func _setup_global_match_timer_ui():
|
||||
"""Create the global match timer display at the top of the screen."""
|
||||
var existing = get_node_or_null("GlobalMatchTimer")
|
||||
if existing:
|
||||
return
|
||||
|
||||
# Create timer panel
|
||||
var panel = PanelContainer.new()
|
||||
panel.name = "GlobalMatchTimer"
|
||||
|
||||
# Position at top center
|
||||
panel.set_anchors_preset(Control.PRESET_CENTER_TOP)
|
||||
panel.offset_left = -80
|
||||
panel.offset_right = 80
|
||||
panel.offset_top = 10
|
||||
panel.offset_bottom = 60
|
||||
|
||||
# Style
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.1, 0.1, 0.15, 0.9)
|
||||
style.border_width_left = 2
|
||||
style.border_width_top = 2
|
||||
style.border_width_right = 2
|
||||
style.border_width_bottom = 2
|
||||
style.border_color = Color(0.647, 0.996, 0.224, 0.8)
|
||||
style.corner_radius_top_left = 8
|
||||
style.corner_radius_top_right = 8
|
||||
style.corner_radius_bottom_right = 8
|
||||
style.corner_radius_bottom_left = 8
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# VBox for content
|
||||
var vbox = VBoxContainer.new()
|
||||
vbox.name = "VBox"
|
||||
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
panel.add_child(vbox)
|
||||
|
||||
# Label
|
||||
var label = Label.new()
|
||||
label.name = "TimerLabel"
|
||||
label.text = "3:00"
|
||||
label.add_theme_font_size_override("font_size", 28)
|
||||
label.add_theme_color_override("font_color", Color(0.647, 0.996, 0.224))
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
vbox.add_child(label)
|
||||
|
||||
add_child(panel)
|
||||
|
||||
func _process(delta):
|
||||
if multiplayer.is_server() and GameStateManager.is_game_started():
|
||||
if TurnManager.turn_based_mode:
|
||||
@@ -322,9 +373,10 @@ func _start_game():
|
||||
var next_player = TurnManager.next_turn(GameStateManager.players)
|
||||
rpc("set_current_turn", next_player)
|
||||
|
||||
# Start the goals cycle timer
|
||||
# Start the global match timer (this also starts the first cycle)
|
||||
if goals_cycle_manager:
|
||||
goals_cycle_manager.start_cycle()
|
||||
var match_duration = LobbyManager.get_match_duration()
|
||||
goals_cycle_manager.start_match(float(match_duration))
|
||||
|
||||
# Initialize leaderboard with all players
|
||||
if ui_manager:
|
||||
@@ -830,6 +882,155 @@ func _on_leaderboard_updated(sorted_scores: Array):
|
||||
})
|
||||
rpc("sync_leaderboard_data", player_data)
|
||||
|
||||
func _on_global_timer_updated(time_remaining: float):
|
||||
"""Update the global match timer display."""
|
||||
var global_timer_panel = get_node_or_null("GlobalMatchTimer")
|
||||
if global_timer_panel:
|
||||
var timer_label = global_timer_panel.get_node_or_null("VBox/TimerLabel")
|
||||
if timer_label:
|
||||
var minutes = int(time_remaining) / 60
|
||||
var seconds = int(time_remaining) % 60
|
||||
timer_label.text = "%d:%02d" % [minutes, seconds]
|
||||
|
||||
func _on_match_ended():
|
||||
"""Called when the global match timer ends - show game over screen."""
|
||||
print("[Main] Match ended! Showing game over screen...")
|
||||
|
||||
# Disable player controls
|
||||
var local_player = GameStateManager.local_player_character
|
||||
if local_player:
|
||||
local_player.action_points = 0
|
||||
|
||||
# Show game over overlay
|
||||
_show_game_over_panel()
|
||||
|
||||
func _show_game_over_panel():
|
||||
"""Create and display the game over panel with final leaderboard."""
|
||||
# Check if panel already exists
|
||||
var existing_panel = get_node_or_null("GameOverPanel")
|
||||
if existing_panel:
|
||||
existing_panel.show()
|
||||
return
|
||||
|
||||
# Create game over panel
|
||||
var panel = PanelContainer.new()
|
||||
panel.name = "GameOverPanel"
|
||||
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
|
||||
# Semi-transparent dark background
|
||||
var style = StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.0, 0.0, 0.0, 0.85)
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
# Content container
|
||||
var vbox = VBoxContainer.new()
|
||||
vbox.name = "VBox"
|
||||
vbox.set_anchors_preset(Control.PRESET_CENTER)
|
||||
vbox.add_theme_constant_override("separation", 20)
|
||||
vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
panel.add_child(vbox)
|
||||
|
||||
# Center the vbox
|
||||
var margin = MarginContainer.new()
|
||||
margin.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
margin.add_theme_constant_override("margin_left", 200)
|
||||
margin.add_theme_constant_override("margin_right", 200)
|
||||
margin.add_theme_constant_override("margin_top", 100)
|
||||
margin.add_theme_constant_override("margin_bottom", 100)
|
||||
panel.add_child(margin)
|
||||
|
||||
var inner_vbox = VBoxContainer.new()
|
||||
inner_vbox.add_theme_constant_override("separation", 30)
|
||||
inner_vbox.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
margin.add_child(inner_vbox)
|
||||
|
||||
# Title
|
||||
var title = Label.new()
|
||||
title.text = "⏱️ TIME'S UP!"
|
||||
title.add_theme_font_size_override("font_size", 64)
|
||||
title.add_theme_color_override("font_color", Color(0.992, 0.796, 0.047))
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
inner_vbox.add_child(title)
|
||||
|
||||
# Subtitle
|
||||
var subtitle = Label.new()
|
||||
subtitle.text = "FINAL STANDINGS"
|
||||
subtitle.add_theme_font_size_override("font_size", 24)
|
||||
subtitle.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7))
|
||||
subtitle.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
inner_vbox.add_child(subtitle)
|
||||
|
||||
# Leaderboard container
|
||||
var leaderboard_container = VBoxContainer.new()
|
||||
leaderboard_container.add_theme_constant_override("separation", 15)
|
||||
inner_vbox.add_child(leaderboard_container)
|
||||
|
||||
# Get final scores
|
||||
var player_scores = []
|
||||
for p in get_tree().get_nodes_in_group("Players"):
|
||||
player_scores.append({
|
||||
"name": p.name,
|
||||
"score": goals_cycle_manager.get_player_score(p.get_multiplayer_authority()) if goals_cycle_manager else 0
|
||||
})
|
||||
player_scores.sort_custom(func(a, b): return a.score > b.score)
|
||||
|
||||
# Display each player
|
||||
for i in range(min(player_scores.size(), 4)):
|
||||
var entry = HBoxContainer.new()
|
||||
entry.add_theme_constant_override("separation", 20)
|
||||
|
||||
var rank_colors = [Color(1.0, 0.84, 0.0), Color(0.75, 0.75, 0.75), Color(0.8, 0.5, 0.2), Color(0.5, 0.5, 0.5)]
|
||||
var rank_emojis = ["🥇", "🥈", "🥉", "4th"]
|
||||
|
||||
var rank_label = Label.new()
|
||||
rank_label.text = rank_emojis[i]
|
||||
rank_label.add_theme_font_size_override("font_size", 32)
|
||||
entry.add_child(rank_label)
|
||||
|
||||
var name_label = Label.new()
|
||||
name_label.text = "Player %s" % player_scores[i].name
|
||||
name_label.add_theme_font_size_override("font_size", 28)
|
||||
name_label.add_theme_color_override("font_color", rank_colors[i])
|
||||
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.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)
|
||||
|
||||
# Back to Menu button
|
||||
var back_btn = Button.new()
|
||||
back_btn.name = "BackToMenuBtn"
|
||||
back_btn.text = "BACK TO MAIN MENU"
|
||||
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
|
||||
var btn_container = HBoxContainer.new()
|
||||
btn_container.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
btn_container.add_child(back_btn)
|
||||
inner_vbox.add_child(btn_container)
|
||||
|
||||
add_child(panel)
|
||||
|
||||
func _on_back_to_menu_pressed():
|
||||
"""Return to lobby/main menu and clean up game state."""
|
||||
print("[Main] Returning to lobby...")
|
||||
|
||||
# Clean up game state
|
||||
GameStateManager.end_game()
|
||||
LobbyManager.reset()
|
||||
|
||||
# Go back to lobby
|
||||
if get_tree():
|
||||
get_tree().change_scene_to_file("res://scenes/lobby.tscn")
|
||||
|
||||
func _deferred_init_leaderboard():
|
||||
"""Initialize leaderboard after a delay to ensure all players are loaded."""
|
||||
# Longer delay ensures players are synced
|
||||
|
||||
+88
-82
@@ -23,6 +23,22 @@
|
||||
[ext_resource type="StyleBox" uid="uid://cdhnwvcklbyl8" path="res://assets/styles/ribbon_hovered_gui.tres" id="19_w1rqq"]
|
||||
[ext_resource type="StyleBox" uid="uid://3yog1weaqhxb" path="res://assets/styles/ribbon_unselected_gui.tres" id="20_q6bc1"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerboard"]
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
bg_color = Color(0.08, 0.08, 0.12, 0.9)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.3, 0.3, 0.4, 0.8)
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1cewu"]
|
||||
|
||||
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_5oeq4"]
|
||||
@@ -30,22 +46,6 @@ texture = ExtResource("13_ahjgs")
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_s1l63"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerboard"]
|
||||
bg_color = Color(0.08, 0.08, 0.12, 0.9)
|
||||
border_color = Color(0.3, 0.3, 0.4, 0.8)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
corner_radius_top_left = 8
|
||||
corner_radius_top_right = 8
|
||||
corner_radius_bottom_right = 8
|
||||
corner_radius_bottom_left = 8
|
||||
content_margin_left = 8.0
|
||||
content_margin_top = 8.0
|
||||
content_margin_right = 8.0
|
||||
content_margin_bottom = 8.0
|
||||
|
||||
[node name="Main" type="Node3D"]
|
||||
script = ExtResource("1_xcpe3")
|
||||
|
||||
@@ -77,26 +77,31 @@ fov = 35.5
|
||||
size = 23.0
|
||||
|
||||
[node name="NetworkPanel" type="Panel" parent="."]
|
||||
anchors_preset = 4
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 1208.0
|
||||
offset_top = 280.0
|
||||
offset_right = 1344.0
|
||||
offset_bottom = 333.0
|
||||
grow_vertical = 2
|
||||
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="VBoxContainer" parent="NetworkPanel"]
|
||||
layout_mode = 0
|
||||
offset_left = 8.0
|
||||
offset_right = 124.0
|
||||
offset_bottom = 50.0
|
||||
[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
|
||||
@@ -108,10 +113,10 @@ vertical_alignment = 1
|
||||
anchors_preset = 4
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 10.0
|
||||
offset_top = -120.0
|
||||
offset_right = 216.0
|
||||
offset_bottom = 120.0
|
||||
offset_left = 19.0
|
||||
offset_top = -233.0
|
||||
offset_right = 232.0
|
||||
offset_bottom = 7.0
|
||||
grow_vertical = 2
|
||||
theme_override_styles/panel = SubResource("StyleBoxFlat_playerboard")
|
||||
|
||||
@@ -120,10 +125,10 @@ clip_contents = true
|
||||
anchors_preset = 4
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 18.0
|
||||
offset_top = -88.0
|
||||
offset_right = 208.0
|
||||
offset_bottom = 102.0
|
||||
offset_left = 28.0
|
||||
offset_top = -199.0
|
||||
offset_right = 224.0
|
||||
offset_bottom = -3.0
|
||||
grow_vertical = 2
|
||||
size_flags_horizontal = 3
|
||||
columns = 5
|
||||
@@ -978,7 +983,43 @@ grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
texture = ExtResource("9_6gcb6")
|
||||
|
||||
[node name="PowerUpBar" type="PanelContainer" parent="."]
|
||||
anchors_preset = 4
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 29.0
|
||||
offset_top = -228.0
|
||||
offset_right = 149.0
|
||||
offset_bottom = -202.0
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="PowerUpBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="PowerLabel" type="Label" parent="PowerUpBar/HBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "⚡"
|
||||
|
||||
[node name="Segment0" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Segment1" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Segment2" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Segment3" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="ActionMenu" type="Control" parent="."]
|
||||
visible = false
|
||||
layout_mode = 3
|
||||
anchors_preset = 3
|
||||
anchor_left = 1.0
|
||||
@@ -9334,9 +9375,9 @@ anchor_top = 1.0
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 1.0
|
||||
offset_left = -200.0
|
||||
offset_top = -99.0
|
||||
offset_top = -87.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = -41.0
|
||||
offset_bottom = -53.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
theme_override_styles/panel = ExtResource("5_dvx6y")
|
||||
@@ -9352,57 +9393,22 @@ theme_override_constants/margin_bottom = 5
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="PowerUpBar" type="PanelContainer" parent="."]
|
||||
anchors_preset = 4
|
||||
anchor_top = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = 18.0
|
||||
offset_top = -115.0
|
||||
offset_right = 110.0
|
||||
offset_bottom = -92.0
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HBox" type="HBoxContainer" parent="PowerUpBar"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="PowerLabel" type="Label" parent="PowerUpBar/HBox"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "⚡"
|
||||
|
||||
[node name="Segment0" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Segment1" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Segment2" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Segment3" type="Panel" parent="PowerUpBar/HBox"]
|
||||
custom_minimum_size = Vector2(20, 16)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LeaderboardPanel" type="PanelContainer" parent="."]
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -210.0
|
||||
offset_top = 80.0
|
||||
offset_right = -10.0
|
||||
offset_bottom = 280.0
|
||||
offset_left = -218.0
|
||||
offset_top = 15.0
|
||||
offset_right = -18.0
|
||||
offset_bottom = 215.0
|
||||
grow_horizontal = 0
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="LeaderboardPanel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 8
|
||||
theme_override_constants/margin_top = 8
|
||||
theme_override_constants/margin_right = 8
|
||||
theme_override_constants/margin_bottom = 8
|
||||
theme_override_constants/margin_left = 18
|
||||
theme_override_constants/margin_top = 18
|
||||
theme_override_constants/margin_right = 18
|
||||
theme_override_constants/margin_bottom = 18
|
||||
|
||||
[node name="VBox" type="VBoxContainer" parent="LeaderboardPanel/MarginContainer"]
|
||||
layout_mode = 2
|
||||
|
||||
@@ -40,6 +40,11 @@ func remove_bot(bot_id: int):
|
||||
players.erase(bot_id)
|
||||
emit_signal("game_state_changed")
|
||||
|
||||
func end_game():
|
||||
"""End the current game and prepare for return to lobby."""
|
||||
game_started_flag = false
|
||||
emit_signal("game_state_changed")
|
||||
|
||||
func reset():
|
||||
players.clear()
|
||||
bots.clear()
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
extends Node
|
||||
|
||||
# GoalsCycleManager - Handles 60-second goal cycles, scoring, and goal regeneration
|
||||
# Also handles global match timer that ends the game
|
||||
|
||||
const CYCLE_DURATION: float = 30.0
|
||||
const BASE_SCORE: int = 100
|
||||
const TIME_BONUS_MULTIPLIER: float = 2.0
|
||||
|
||||
# Timer state
|
||||
# Cycle timer state (30-second cycles)
|
||||
var current_cycle_timer: float = 0.0
|
||||
var is_cycle_active: bool = false
|
||||
|
||||
# Global match timer state
|
||||
var global_match_timer: float = 0.0
|
||||
var match_duration: float = 180.0 # Default 3 minutes
|
||||
var is_match_active: bool = false
|
||||
|
||||
# Score tracking: peer_id -> score
|
||||
var player_scores: Dictionary = {}
|
||||
|
||||
@@ -22,6 +28,11 @@ signal timer_updated(time_remaining: float)
|
||||
signal score_updated(peer_id: int, new_score: int)
|
||||
signal leaderboard_updated(sorted_scores: Array)
|
||||
|
||||
# Global match signals
|
||||
signal match_started()
|
||||
signal match_ended()
|
||||
signal global_timer_updated(time_remaining: float)
|
||||
|
||||
func _ready():
|
||||
set_process(false)
|
||||
|
||||
@@ -29,6 +40,21 @@ func initialize(main: Node):
|
||||
main_scene = main
|
||||
|
||||
func _process(delta):
|
||||
# Update global match timer if active
|
||||
if is_match_active:
|
||||
global_match_timer -= delta
|
||||
|
||||
if global_match_timer <= 0:
|
||||
global_match_timer = 0
|
||||
_on_match_end()
|
||||
else:
|
||||
emit_signal("global_timer_updated", global_match_timer)
|
||||
|
||||
# Server broadcasts global timer sync every second
|
||||
if multiplayer.is_server() and int(global_match_timer) != int(global_match_timer + delta):
|
||||
rpc("sync_global_timer", global_match_timer)
|
||||
|
||||
# Update cycle timer if cycle is active
|
||||
if not is_cycle_active:
|
||||
return
|
||||
|
||||
@@ -44,6 +70,57 @@ func _process(delta):
|
||||
if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta):
|
||||
rpc("sync_timer", current_cycle_timer)
|
||||
|
||||
# =============================================================================
|
||||
# Global Match Control
|
||||
# =============================================================================
|
||||
|
||||
func start_match(duration_seconds: float):
|
||||
"""Start the global match timer. Called by server when game starts."""
|
||||
match_duration = duration_seconds
|
||||
global_match_timer = duration_seconds
|
||||
is_match_active = true
|
||||
set_process(true)
|
||||
emit_signal("match_started")
|
||||
|
||||
if multiplayer.is_server():
|
||||
rpc("sync_match_start", duration_seconds)
|
||||
# Also start the first cycle
|
||||
start_cycle()
|
||||
|
||||
func _on_match_end():
|
||||
"""Called when global match timer reaches zero - game over!"""
|
||||
is_match_active = false
|
||||
is_cycle_active = false
|
||||
emit_signal("match_ended")
|
||||
|
||||
if multiplayer.is_server():
|
||||
rpc("sync_match_end")
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_match_start(duration_seconds: float):
|
||||
match_duration = duration_seconds
|
||||
global_match_timer = duration_seconds
|
||||
is_match_active = true
|
||||
set_process(true)
|
||||
emit_signal("match_started")
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_match_end():
|
||||
is_match_active = false
|
||||
is_cycle_active = false
|
||||
emit_signal("match_ended")
|
||||
|
||||
@rpc("authority", "call_local", "unreliable")
|
||||
func sync_global_timer(time_remaining: float):
|
||||
global_match_timer = time_remaining
|
||||
emit_signal("global_timer_updated", time_remaining)
|
||||
|
||||
func get_global_time_remaining() -> float:
|
||||
return global_match_timer
|
||||
|
||||
func is_match_running() -> bool:
|
||||
return is_match_active
|
||||
|
||||
# =============================================================================
|
||||
# Cycle Control
|
||||
# =============================================================================
|
||||
@@ -82,7 +159,6 @@ func sync_timer(time_remaining: float):
|
||||
|
||||
func _on_cycle_end():
|
||||
is_cycle_active = false
|
||||
set_process(false)
|
||||
emit_signal("cycle_ended")
|
||||
|
||||
if multiplayer.is_server():
|
||||
@@ -90,14 +166,15 @@ func _on_cycle_end():
|
||||
_process_cycle_end_for_all_players()
|
||||
rpc("sync_cycle_end")
|
||||
|
||||
# Start new cycle after a brief delay
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
start_cycle()
|
||||
# Only start new cycle if match is still active
|
||||
if is_match_active:
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
if is_match_active: # Check again in case match ended during delay
|
||||
start_cycle()
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_cycle_end():
|
||||
is_cycle_active = false
|
||||
set_process(false)
|
||||
emit_signal("cycle_ended")
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -11,6 +11,10 @@ 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 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 = {}
|
||||
@@ -19,6 +23,15 @@ 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
|
||||
|
||||
# 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
|
||||
|
||||
@@ -133,6 +146,101 @@ func is_all_ready() -> bool:
|
||||
# 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
|
||||
|
||||
# =============================================================================
|
||||
# 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:
|
||||
@@ -143,6 +251,9 @@ func start_game() -> void:
|
||||
push_error("Not all players are ready")
|
||||
return
|
||||
|
||||
# Sync match duration to all clients before starting
|
||||
rpc("sync_match_duration", match_duration)
|
||||
|
||||
# Notify all clients to start
|
||||
rpc("_on_game_starting")
|
||||
|
||||
@@ -168,7 +279,8 @@ func _on_match_joined(match_id: String) -> void:
|
||||
var my_data = {
|
||||
"id": my_id,
|
||||
"name": local_player_name,
|
||||
"is_ready": false
|
||||
"is_ready": false,
|
||||
"character": available_characters[local_character_index]
|
||||
}
|
||||
players_in_room.append(my_data)
|
||||
|
||||
@@ -202,7 +314,8 @@ func _on_peer_connected(peer_id: int) -> void:
|
||||
var new_player = {
|
||||
"id": peer_id,
|
||||
"name": "Player %d" % peer_id,
|
||||
"is_ready": false
|
||||
"is_ready": false,
|
||||
"character": available_characters[0]
|
||||
}
|
||||
players_in_room.append(new_player)
|
||||
|
||||
@@ -247,3 +360,6 @@ func reset() -> void:
|
||||
available_rooms.clear()
|
||||
is_host = false
|
||||
_all_ready = false
|
||||
match_duration = 180 # Reset to default 3 minutes
|
||||
selected_area = "Desert"
|
||||
local_character_index = 0
|
||||
|
||||
@@ -22,7 +22,7 @@ signal player_banned(player_id: String)
|
||||
|
||||
# Player data cache
|
||||
var players: Array = []
|
||||
var banned_players: Array = [] # [{user_id, username, banned_at, reason, expires}]
|
||||
var banned_players: Array = [] # [{user_id, username, banned_at, reason, expires}]
|
||||
var is_admin: bool = false
|
||||
var is_host: bool = false
|
||||
|
||||
@@ -77,7 +77,7 @@ func _rpc_call(rpc_name: String, payload: Dictionary) -> Dictionary:
|
||||
push_error("[AdminPanel] Not connected to Nakama")
|
||||
return {"error": "Not connected"}
|
||||
|
||||
var result := await NakamaManager.client.rpc_async(
|
||||
var result = await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
rpc_name,
|
||||
JSON.stringify(payload)
|
||||
@@ -185,10 +185,10 @@ func _update_action_buttons() -> void:
|
||||
|
||||
var idx: int = selected[0]
|
||||
var meta: Dictionary = player_list.get_item_metadata(idx)
|
||||
var is_player_host := meta.get("peer_id", 0) == 1
|
||||
var is_player_host: bool = meta.get("peer_id", 0) == 1
|
||||
|
||||
# Can't kick/ban the host or yourself
|
||||
var is_self := meta.get("user_id", "") == AuthManager.current_user.get("user_id", "")
|
||||
var is_self: bool = meta.get("user_id", "") == AuthManager.current_user.get("user_id", "")
|
||||
|
||||
kick_btn.disabled = is_player_host or is_self
|
||||
ban_btn.disabled = is_player_host or is_self or not is_admin
|
||||
@@ -267,8 +267,8 @@ func _create_ban_dialog(user_id: String, player_name: String) -> ConfirmationDia
|
||||
|
||||
var duration_input := SpinBox.new()
|
||||
duration_input.min_value = 0
|
||||
duration_input.max_value = 8760 # 1 year
|
||||
duration_input.value = 24 # Default 24 hours
|
||||
duration_input.max_value = 8760 # 1 year
|
||||
duration_input.value = 24 # Default 24 hours
|
||||
vbox.add_child(duration_input)
|
||||
|
||||
dialog.add_child(vbox)
|
||||
|
||||
@@ -15,7 +15,7 @@ extends Control
|
||||
|
||||
var update_manager: Node
|
||||
var update_info: Dictionary = {}
|
||||
var main_scene_path := "res://scenes/main.tscn" # Your main game scene
|
||||
var main_scene_path := "res://scenes/main.tscn" # Your main game scene
|
||||
|
||||
func _ready() -> void:
|
||||
# Get or create the update manager
|
||||
@@ -49,7 +49,7 @@ func _get_update_manager() -> Node:
|
||||
|
||||
# Otherwise, create instance
|
||||
var manager_script := load("res://scripts/managers/game_update_manager.gd")
|
||||
var manager := manager_script.new()
|
||||
var manager: Node = manager_script.new()
|
||||
manager.name = "GameUpdateManager"
|
||||
get_tree().root.add_child(manager)
|
||||
return manager
|
||||
|
||||
Reference in New Issue
Block a user