feat: Implement a new lobby system with configurable match duration, game over screen, and core game state management.

This commit is contained in:
2025-12-20 01:10:49 +08:00
parent 75eb398649
commit b0d45d4569
12 changed files with 1241 additions and 338 deletions
+9
View File
@@ -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.
+36
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
[gd_resource type="StyleBoxFlat" format=3 uid="uid://dlw1ogamn741n"] [gd_resource type="StyleBoxFlat" format=3 uid="uid://dlw1ogamn741n"]
[resource] [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_left = 5
corner_radius_top_right = 5 corner_radius_top_right = 5
corner_radius_bottom_right = 5 corner_radius_bottom_right = 5
+211 -178
View File
@@ -14,22 +14,44 @@ extends Control
@onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn @onready var join_btn = $RoomListPanel/VBoxContainer/ButtonContainer/JoinBtn
@onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn @onready var back_btn = $RoomListPanel/VBoxContainer/ButtonContainer/BackBtn
# UI References - Lobby # UI References - Lobby Panel
@onready var lobby_panel = $LobbyPanel @onready var lobby_panel = $LobbyPanel
@onready var room_name_header = $LobbyPanel/VBoxContainer/RoomNameHeader @onready var room_name_header = $LobbyPanel/RoomNameHeader
@onready var match_id_display = $LobbyPanel/VBoxContainer/MatchIdContainer/MatchIdDisplay @onready var host_banner = $LobbyPanel/HostBanner
@onready var copy_id_btn = $LobbyPanel/VBoxContainer/MatchIdContainer/CopyIdBtn @onready var host_banner_label = $LobbyPanel/HostBanner/HostBannerLabel
@onready var player_list = $LobbyPanel/VBoxContainer/PlayerList
@onready var status_label = $LobbyPanel/VBoxContainer/StatusLabel
@onready var ready_btn = $LobbyPanel/VBoxContainer/ButtonContainer/ReadyBtn # UI References - Top Bar
@onready var start_game_btn = $LobbyPanel/VBoxContainer/ButtonContainer/StartGameBtn @onready var profile_btn = $LobbyPanel/TopBar/ProfileSection/ProfileBtn
@onready var leave_btn = $LobbyPanel/VBoxContainer/ButtonContainer/LeaveBtn @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 # UI References - Status
@onready var connection_status = $StatusBar/ConnectionStatus @onready var connection_status = $StatusBar/ConnectionStatus
# UI References - User Profile Bar (will be added to scene) # Character preview textures
var user_profile_bar: Control var character_textures: Dictionary = {}
# Profile panel instance
var profile_panel_instance: Control var profile_panel_instance: Control
var admin_panel_instance: Control var admin_panel_instance: Control
@@ -39,27 +61,38 @@ var current_match_id: String = ""
func _ready(): func _ready():
# Check if user is authenticated # Check if user is authenticated
if not AuthManager.is_logged_in(): if not AuthManager.is_logged_in():
# Redirect to login screen - must use deferred call during _ready()
call_deferred("_go_to_login") call_deferred("_go_to_login")
return return
# Initialize user profile bar # Load character textures
_setup_user_profile_bar() _load_character_textures()
# Get player slot references
_setup_player_slots()
# Set player name from profile # Set player name from profile
if player_name_input: if player_name_input:
player_name_input.text = UserProfileManager.get_display_name() 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) create_room_btn.pressed.connect(_on_create_room_pressed)
browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed) browse_rooms_btn.pressed.connect(_on_browse_rooms_pressed)
# Connect button signals - Room List
refresh_btn.pressed.connect(_on_refresh_pressed) refresh_btn.pressed.connect(_on_refresh_pressed)
join_btn.pressed.connect(_on_join_pressed) join_btn.pressed.connect(_on_join_pressed)
back_btn.pressed.connect(_on_back_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) ready_btn.toggled.connect(_on_ready_toggled)
start_game_btn.pressed.connect(_on_start_game_pressed) 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 # Connect LobbyManager signals
LobbyManager.room_list_updated.connect(_on_room_list_updated) 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.ready_state_changed.connect(_on_ready_state_changed)
LobbyManager.all_players_ready.connect(_on_all_players_ready) LobbyManager.all_players_ready.connect(_on_all_players_ready)
LobbyManager.game_starting.connect(_on_game_starting) 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 # Connect NakamaManager signals
NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama) NakamaManager.connected_to_nakama.connect(_on_connected_to_nakama)
@@ -77,144 +114,35 @@ func _ready():
# Show main menu initially # Show main menu initially
_show_panel("main_menu") _show_panel("main_menu")
_update_profile_bar()
# ============================================================================= # =============================================================================
# User Profile Bar # Setup
# ============================================================================= # =============================================================================
func _setup_user_profile_bar() -> void: func _load_character_textures() -> void:
# Create profile bar dynamically (or get reference if in scene) """Load character expression textures for preview."""
user_profile_bar = _create_profile_bar() var characters = ["Bob", "Gatot", "Masbro", "Oldpop"]
add_child(user_profile_bar) 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: func _setup_player_slots() -> void:
var bar := HBoxContainer.new() """Get references to all player slot nodes."""
bar.name = "UserProfileBar" player_slots.clear()
bar.set_anchors_preset(Control.PRESET_TOP_WIDE) for i in range(1, 5):
bar.offset_top = 5 var slot = players_container.get_node_or_null("PlayerSlot%d" % i)
bar.offset_bottom = 45 if slot:
bar.offset_left = 10 player_slots.append(slot)
bar.offset_right = -10 # Connect character navigation buttons for all slots
var left_btn = slot.get_node_or_null("CharacterNav%d/CharLeftBtn%d" % [i, i])
# Avatar var right_btn = slot.get_node_or_null("CharacterNav%d/CharRightBtn%d" % [i, i])
var avatar := TextureRect.new() if left_btn:
avatar.name = "Avatar" left_btn.pressed.connect(func(): LobbyManager.cycle_character(-1))
avatar.custom_minimum_size = Vector2(35, 35) if right_btn:
avatar.expand_mode = TextureRect.EXPAND_FIT_WIDTH right_btn.pressed.connect(func(): LobbyManager.cycle_character(1))
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
# ============================================================================= # =============================================================================
# Panel Management # Panel Management
@@ -230,13 +158,11 @@ func _show_panel(panel_name: String) -> void:
# ============================================================================= # =============================================================================
func _on_create_room_pressed() -> void: func _on_create_room_pressed() -> void:
# Set player name
LobbyManager.local_player_name = player_name_input.text.strip_edges() LobbyManager.local_player_name = player_name_input.text.strip_edges()
if LobbyManager.local_player_name.is_empty(): if LobbyManager.local_player_name.is_empty():
LobbyManager.local_player_name = "Host" LobbyManager.local_player_name = "Host"
connection_status.text = "Creating room..." 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)) LobbyManager.create_room("Room %d" % randi_range(1000, 9999))
func _on_browse_rooms_pressed() -> void: func _on_browse_rooms_pressed() -> void:
@@ -254,11 +180,9 @@ func _on_refresh_pressed() -> void:
LobbyManager.refresh_room_list() LobbyManager.refresh_room_list()
func _on_join_pressed() -> void: func _on_join_pressed() -> void:
# First check if there's a match ID entered
var match_id = match_id_input.text.strip_edges() var match_id = match_id_input.text.strip_edges()
if match_id.is_empty(): if match_id.is_empty():
# Try to use selected room from list
var selected_items = room_list.get_selected_items() var selected_items = room_list.get_selected_items()
if selected_items.size() == 0: if selected_items.size() == 0:
connection_status.text = "Please select a room or enter Match ID" 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" connection_status.text = "No room selected"
return return
# Set player name
LobbyManager.local_player_name = player_name_input.text.strip_edges() LobbyManager.local_player_name = player_name_input.text.strip_edges()
if LobbyManager.local_player_name.is_empty(): if LobbyManager.local_player_name.is_empty():
LobbyManager.local_player_name = "Player" LobbyManager.local_player_name = "Player"
@@ -303,7 +226,33 @@ func _on_leave_pressed() -> void:
func _on_copy_id_pressed() -> void: func _on_copy_id_pressed() -> void:
DisplayServer.clipboard_set(current_match_id) 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 # LobbyManager Signal Handlers
@@ -327,12 +276,25 @@ func _on_room_joined(room_data: Dictionary) -> void:
_show_panel("lobby") _show_panel("lobby")
current_match_id = room_data.get("match_id", "") current_match_id = room_data.get("match_id", "")
room_name_header.text = "ROOM: %s" % room_data.get("room_name", "Unknown") 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) # Configure host-specific UI
start_game_btn.visible = LobbyManager.is_host 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" connection_status.text = "Connected to room"
func _on_room_left() -> void: func _on_room_left() -> void:
@@ -340,33 +302,38 @@ func _on_room_left() -> void:
connection_status.text = "Left room" connection_status.text = "Left room"
func _on_player_joined(player_data: Dictionary) -> void: func _on_player_joined(player_data: Dictionary) -> void:
_update_player_list() _update_player_slots()
status_label.text = "%s joined!" % player_data.get("name", "Player") status_label.text = "%s joined!" % player_data.get("name", "Player")
func _on_player_left(_player_id: int) -> void: func _on_player_left(_player_id: int) -> void:
_update_player_list() _update_player_slots()
status_label.text = "A player left" status_label.text = "A player left"
func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void: func _on_ready_state_changed(_player_id: int, _is_ready: bool) -> void:
_update_player_list() _update_player_slots()
_update_status() _update_status()
func _on_all_players_ready() -> void: func _on_all_players_ready() -> void:
if LobbyManager.is_host: if LobbyManager.is_host:
start_game_btn.disabled = false start_game_btn.disabled = false
status_label.text = "All players ready! Host can start." status_label.text = "All ready! Start the match!"
else: else:
status_label.text = "All players ready! Waiting for host..." status_label.text = "All ready! Waiting for host..."
func _on_game_starting() -> void: func _on_game_starting() -> void:
connection_status.text = "Starting game..." connection_status.text = "Starting game..."
# Small delay for visual feedback
await get_tree().create_timer(0.5).timeout await get_tree().create_timer(0.5).timeout
get_tree().change_scene_to_file("res://scenes/main.tscn") get_tree().change_scene_to_file("res://scenes/main.tscn")
# ============================================================================= func _on_match_duration_changed(duration_seconds: int) -> void:
# NakamaManager Signal Handlers 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: func _on_connected_to_nakama() -> void:
connection_status.text = "Connected to server" connection_status.text = "Connected to server"
@@ -376,18 +343,64 @@ func _on_connection_failed(error_message: String) -> void:
_show_panel("main_menu") _show_panel("main_menu")
# ============================================================================= # =============================================================================
# Helper Functions # Player Slot Updates
# ============================================================================= # =============================================================================
func _update_player_list() -> void: func _update_player_slots() -> void:
player_list.clear() """Update all player slot visuals based on current player list."""
var players = LobbyManager.get_players() var players = LobbyManager.get_players()
for player in players: var my_id = multiplayer.get_unique_id()
var player_name = player.get("name", "Unknown")
var is_ready = player.get("is_ready", false) for i in range(player_slots.size()):
var ready_icon = "" if is_ready else "" var slot = player_slots[i]
var host_tag = " (Host)" if player.get("id") == 1 else "" var slot_num = i + 1
player_list.add_item("%s%s%s" % [player_name, host_tag, ready_icon])
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: func _update_status() -> void:
var players = LobbyManager.get_players() var players = LobbyManager.get_players()
@@ -400,3 +413,23 @@ func _update_status() -> void:
if LobbyManager.is_host: if LobbyManager.is_host:
start_game_btn.disabled = not LobbyManager.is_all_ready() 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
View File
@@ -20,7 +20,7 @@ anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 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="."] [node name="MainMenuPanel" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1
@@ -165,90 +165,507 @@ custom_minimum_size = Vector2(110, 44)
layout_mode = 2 layout_mode = 2
text = "BACK" text = "BACK"
[node name="LobbyPanel" type="PanelContainer" parent="."] [node name="LobbyPanel" type="Control" parent="."]
visible = false visible = false
layout_mode = 1 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 anchors_preset = 8
anchor_left = 0.5 anchor_left = 0.5
anchor_top = 0.5 anchor_top = 0.5
anchor_right = 0.5 anchor_right = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = -300.0 offset_left = -450.0
offset_top = -260.0 offset_top = -120.0
offset_right = 300.0 offset_right = 450.0
offset_bottom = 260.0 offset_bottom = 200.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 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 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 layout_mode = 2
theme_override_colors/font_color = Color(0.647, 0.996, 0.224, 1) theme_override_colors/font_color = Color(1, 1, 1, 1)
theme_override_font_sizes/font_size = 26 theme_override_font_sizes/font_size = 16
text = "ROOM: " text = "Player 1"
horizontal_alignment = 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 layout_mode = 2
alignment = 1 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 layout_mode = 2
theme_override_colors/font_color = Color(0.5, 0.5, 0.55, 1) theme_override_font_sizes/font_size = 18
theme_override_font_sizes/font_size = 10 text = "◀"
text = "Match ID: "
[node name="CopyIdBtn" type="Button" parent="LobbyPanel/VBoxContainer/MatchIdContainer"] [node name="CharacterName1" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot1/CharacterNav1"]
custom_minimum_size = Vector2(80, 32) custom_minimum_size = Vector2(80, 0)
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"]
layout_mode = 2 layout_mode = 2
theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1) theme_override_colors/font_color = Color(0.992, 0.796, 0.047, 1)
theme_override_font_sizes/font_size = 14 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 horizontal_alignment = 1
[node name="ButtonContainer" type="HBoxContainer" parent="LobbyPanel/VBoxContainer"] [node name="ReadyStatus1" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot1"]
layout_mode = 2 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 alignment = 1
[node name="ReadyBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"] [node name="PlayerName2" type="Label" parent="LobbyPanel/PlayersContainer/PlayerSlot2"]
custom_minimum_size = Vector2(110, 48)
layout_mode = 2 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 toggle_mode = true
text = "READY" text = "READY"
[node name="StartGameBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"] [node name="StartGameBtn" type="Button" parent="LobbyPanel/BottomBar"]
custom_minimum_size = Vector2(140, 48) custom_minimum_size = Vector2(140, 50)
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 16
disabled = true disabled = true
text = "START GAME" text = "MATCH"
[node name="LeaveBtn" type="Button" parent="LobbyPanel/VBoxContainer/ButtonContainer"] [node name="StatusLabel" type="Label" parent="LobbyPanel"]
custom_minimum_size = Vector2(110, 48) layout_mode = 1
layout_mode = 2 anchors_preset = 7
text = "LEAVE" 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="."] [node name="StatusBar" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1
@@ -256,10 +673,10 @@ anchors_preset = 12
anchor_top = 1.0 anchor_top = 1.0
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = 24.0 offset_left = 464.0
offset_top = -72.0 offset_top = -70.0
offset_right = -24.0 offset_right = -461.0
offset_bottom = -24.0 offset_bottom = -21.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
@@ -272,14 +689,17 @@ horizontal_alignment = 1
[node name="VersionLabel" type="Label" parent="."] [node name="VersionLabel" type="Label" parent="."]
layout_mode = 1 layout_mode = 1
anchors_preset = 1 anchors_preset = 3
anchor_left = 1.0 anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0 anchor_right = 1.0
offset_left = -130.0 anchor_bottom = 1.0
offset_top = 20.0 offset_left = -124.0
offset_right = -20.0 offset_top = -95.0
offset_bottom = 40.0 offset_right = -14.0
offset_bottom = -75.0
grow_horizontal = 0 grow_horizontal = 0
grow_vertical = 0
theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 0.6) theme_override_colors/font_color = Color(0.6, 0.6, 0.6, 0.6)
theme_override_font_sizes/font_size = 11 theme_override_font_sizes/font_size = 11
text = "v0.1.0 ALPHA" text = "v0.1.0 ALPHA"
+203 -2
View File
@@ -30,6 +30,7 @@ func _ready():
ui_manager.setup_leaderboard_ui(self) ui_manager.setup_leaderboard_ui(self)
ui_manager.setup_powerup_bar_ui(self) ui_manager.setup_powerup_bar_ui(self)
_setup_obstacle_ui() _setup_obstacle_ui()
_setup_global_match_timer_ui()
# Auto-start game if coming from lobby (already connected to match) # Auto-start game if coming from lobby (already connected to match)
if NakamaManager.is_connected_to_nakama() and multiplayer.get_unique_id() != 0: 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.timer_updated.connect(_on_timer_updated)
goals_cycle_manager.score_updated.connect(_on_score_updated) goals_cycle_manager.score_updated.connect(_on_score_updated)
goals_cycle_manager.leaderboard_updated.connect(_on_leaderboard_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 # Message Bar Configuration
const MAX_MESSAGES := 5 const MAX_MESSAGES := 5
@@ -181,6 +184,54 @@ func _setup_obstacle_ui():
) )
$ActionMenu/ActionButtonContainer.add_child(type_button) $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): func _process(delta):
if multiplayer.is_server() and GameStateManager.is_game_started(): if multiplayer.is_server() and GameStateManager.is_game_started():
if TurnManager.turn_based_mode: if TurnManager.turn_based_mode:
@@ -322,9 +373,10 @@ func _start_game():
var next_player = TurnManager.next_turn(GameStateManager.players) var next_player = TurnManager.next_turn(GameStateManager.players)
rpc("set_current_turn", next_player) 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: 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 # Initialize leaderboard with all players
if ui_manager: if ui_manager:
@@ -830,6 +882,155 @@ func _on_leaderboard_updated(sorted_scores: Array):
}) })
rpc("sync_leaderboard_data", player_data) 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(): func _deferred_init_leaderboard():
"""Initialize leaderboard after a delay to ensure all players are loaded.""" """Initialize leaderboard after a delay to ensure all players are loaded."""
# Longer delay ensures players are synced # Longer delay ensures players are synced
+88 -82
View File
@@ -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://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"] [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="StyleBoxFlat" id="StyleBoxFlat_1cewu"]
[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_5oeq4"] [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="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"] [node name="Main" type="Node3D"]
script = ExtResource("1_xcpe3") script = ExtResource("1_xcpe3")
@@ -77,26 +77,31 @@ fov = 35.5
size = 23.0 size = 23.0
[node name="NetworkPanel" type="Panel" parent="."] [node name="NetworkPanel" type="Panel" parent="."]
anchors_preset = 4 anchors_preset = 5
anchor_top = 0.5 anchor_left = 0.5
anchor_bottom = 0.5 anchor_right = 0.5
offset_left = 1208.0 offset_left = -185.0
offset_top = 280.0 offset_top = 25.0
offset_right = 1344.0 offset_right = 185.0
offset_bottom = 333.0 offset_bottom = 78.0
grow_vertical = 2 grow_horizontal = 2
theme_override_styles/panel = ExtResource("5_dvx6y") theme_override_styles/panel = ExtResource("5_dvx6y")
[node name="NetworkInfo" type="VBoxContainer" parent="NetworkPanel"] [node name="NetworkInfo" type="HBoxContainer" parent="NetworkPanel"]
layout_mode = 0 layout_mode = 1
offset_left = 8.0 anchors_preset = 15
offset_right = 124.0 anchor_right = 1.0
offset_bottom = 50.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"] [node name="NetworkSideDisplay" type="Label" parent="NetworkPanel/NetworkInfo"]
layout_mode = 2 layout_mode = 2
text = "Network Side" text = "Network Side"
horizontal_alignment = 1 horizontal_alignment = 1
vertical_alignment = 1
[node name="UniquePeerID" type="Label" parent="NetworkPanel/NetworkInfo"] [node name="UniquePeerID" type="Label" parent="NetworkPanel/NetworkInfo"]
layout_mode = 2 layout_mode = 2
@@ -108,10 +113,10 @@ vertical_alignment = 1
anchors_preset = 4 anchors_preset = 4
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = 10.0 offset_left = 19.0
offset_top = -120.0 offset_top = -233.0
offset_right = 216.0 offset_right = 232.0
offset_bottom = 120.0 offset_bottom = 7.0
grow_vertical = 2 grow_vertical = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_playerboard") theme_override_styles/panel = SubResource("StyleBoxFlat_playerboard")
@@ -120,10 +125,10 @@ clip_contents = true
anchors_preset = 4 anchors_preset = 4
anchor_top = 0.5 anchor_top = 0.5
anchor_bottom = 0.5 anchor_bottom = 0.5
offset_left = 18.0 offset_left = 28.0
offset_top = -88.0 offset_top = -199.0
offset_right = 208.0 offset_right = 224.0
offset_bottom = 102.0 offset_bottom = -3.0
grow_vertical = 2 grow_vertical = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
columns = 5 columns = 5
@@ -978,7 +983,43 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
texture = ExtResource("9_6gcb6") 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="."] [node name="ActionMenu" type="Control" parent="."]
visible = false
layout_mode = 3 layout_mode = 3
anchors_preset = 3 anchors_preset = 3
anchor_left = 1.0 anchor_left = 1.0
@@ -9334,9 +9375,9 @@ anchor_top = 1.0
anchor_right = 0.5 anchor_right = 0.5
anchor_bottom = 1.0 anchor_bottom = 1.0
offset_left = -200.0 offset_left = -200.0
offset_top = -99.0 offset_top = -87.0
offset_right = 200.0 offset_right = 200.0
offset_bottom = -41.0 offset_bottom = -53.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 0 grow_vertical = 0
theme_override_styles/panel = ExtResource("5_dvx6y") theme_override_styles/panel = ExtResource("5_dvx6y")
@@ -9352,57 +9393,22 @@ theme_override_constants/margin_bottom = 5
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4 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="."] [node name="LeaderboardPanel" type="PanelContainer" parent="."]
anchors_preset = 1 anchors_preset = 1
anchor_left = 1.0 anchor_left = 1.0
anchor_right = 1.0 anchor_right = 1.0
offset_left = -210.0 offset_left = -218.0
offset_top = 80.0 offset_top = 15.0
offset_right = -10.0 offset_right = -18.0
offset_bottom = 280.0 offset_bottom = 215.0
grow_horizontal = 0 grow_horizontal = 0
[node name="MarginContainer" type="MarginContainer" parent="LeaderboardPanel"] [node name="MarginContainer" type="MarginContainer" parent="LeaderboardPanel"]
layout_mode = 2 layout_mode = 2
theme_override_constants/margin_left = 8 theme_override_constants/margin_left = 18
theme_override_constants/margin_top = 8 theme_override_constants/margin_top = 18
theme_override_constants/margin_right = 8 theme_override_constants/margin_right = 18
theme_override_constants/margin_bottom = 8 theme_override_constants/margin_bottom = 18
[node name="VBox" type="VBoxContainer" parent="LeaderboardPanel/MarginContainer"] [node name="VBox" type="VBoxContainer" parent="LeaderboardPanel/MarginContainer"]
layout_mode = 2 layout_mode = 2
+5
View File
@@ -40,6 +40,11 @@ func remove_bot(bot_id: int):
players.erase(bot_id) players.erase(bot_id)
emit_signal("game_state_changed") 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(): func reset():
players.clear() players.clear()
bots.clear() bots.clear()
+83 -6
View File
@@ -1,15 +1,21 @@
extends Node extends Node
# GoalsCycleManager - Handles 60-second goal cycles, scoring, and goal regeneration # 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 CYCLE_DURATION: float = 30.0
const BASE_SCORE: int = 100 const BASE_SCORE: int = 100
const TIME_BONUS_MULTIPLIER: float = 2.0 const TIME_BONUS_MULTIPLIER: float = 2.0
# Timer state # Cycle timer state (30-second cycles)
var current_cycle_timer: float = 0.0 var current_cycle_timer: float = 0.0
var is_cycle_active: bool = false 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 # Score tracking: peer_id -> score
var player_scores: Dictionary = {} var player_scores: Dictionary = {}
@@ -22,6 +28,11 @@ signal timer_updated(time_remaining: float)
signal score_updated(peer_id: int, new_score: int) signal score_updated(peer_id: int, new_score: int)
signal leaderboard_updated(sorted_scores: Array) signal leaderboard_updated(sorted_scores: Array)
# Global match signals
signal match_started()
signal match_ended()
signal global_timer_updated(time_remaining: float)
func _ready(): func _ready():
set_process(false) set_process(false)
@@ -29,6 +40,21 @@ func initialize(main: Node):
main_scene = main main_scene = main
func _process(delta): 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: if not is_cycle_active:
return return
@@ -44,6 +70,57 @@ func _process(delta):
if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta): if multiplayer.is_server() and int(current_cycle_timer) != int(current_cycle_timer + delta):
rpc("sync_timer", current_cycle_timer) 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 # Cycle Control
# ============================================================================= # =============================================================================
@@ -82,7 +159,6 @@ func sync_timer(time_remaining: float):
func _on_cycle_end(): func _on_cycle_end():
is_cycle_active = false is_cycle_active = false
set_process(false)
emit_signal("cycle_ended") emit_signal("cycle_ended")
if multiplayer.is_server(): if multiplayer.is_server():
@@ -90,14 +166,15 @@ func _on_cycle_end():
_process_cycle_end_for_all_players() _process_cycle_end_for_all_players()
rpc("sync_cycle_end") rpc("sync_cycle_end")
# Start new cycle after a brief delay # Only start new cycle if match is still active
await get_tree().create_timer(2.0).timeout if is_match_active:
start_cycle() 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") @rpc("authority", "call_local", "reliable")
func sync_cycle_end(): func sync_cycle_end():
is_cycle_active = false is_cycle_active = false
set_process(false)
emit_signal("cycle_ended") emit_signal("cycle_ended")
# ============================================================================= # =============================================================================
+118 -2
View File
@@ -11,6 +11,10 @@ signal player_left(player_id: int)
signal ready_state_changed(player_id: int, is_ready: bool) signal ready_state_changed(player_id: int, is_ready: bool)
signal all_players_ready() signal all_players_ready()
signal game_starting() 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 # Room data structure
var current_room: Dictionary = {} var current_room: Dictionary = {}
@@ -19,6 +23,15 @@ var available_rooms: Array = []
var is_host: bool = false var is_host: bool = false
var local_player_name: String = "Player" 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 # Ready to start game check
var _all_ready: bool = false var _all_ready: bool = false
@@ -133,6 +146,101 @@ func is_all_ready() -> bool:
# Game Start # 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: func start_game() -> void:
"""Host triggers game start (transitions all players to main.tscn).""" """Host triggers game start (transitions all players to main.tscn)."""
if not is_host: if not is_host:
@@ -143,6 +251,9 @@ func start_game() -> void:
push_error("Not all players are ready") push_error("Not all players are ready")
return return
# Sync match duration to all clients before starting
rpc("sync_match_duration", match_duration)
# Notify all clients to start # Notify all clients to start
rpc("_on_game_starting") rpc("_on_game_starting")
@@ -168,7 +279,8 @@ func _on_match_joined(match_id: String) -> void:
var my_data = { var my_data = {
"id": my_id, "id": my_id,
"name": local_player_name, "name": local_player_name,
"is_ready": false "is_ready": false,
"character": available_characters[local_character_index]
} }
players_in_room.append(my_data) players_in_room.append(my_data)
@@ -202,7 +314,8 @@ func _on_peer_connected(peer_id: int) -> void:
var new_player = { var new_player = {
"id": peer_id, "id": peer_id,
"name": "Player %d" % peer_id, "name": "Player %d" % peer_id,
"is_ready": false "is_ready": false,
"character": available_characters[0]
} }
players_in_room.append(new_player) players_in_room.append(new_player)
@@ -247,3 +360,6 @@ func reset() -> void:
available_rooms.clear() available_rooms.clear()
is_host = false is_host = false
_all_ready = false _all_ready = false
match_duration = 180 # Reset to default 3 minutes
selected_area = "Desert"
local_character_index = 0
+6 -6
View File
@@ -22,7 +22,7 @@ signal player_banned(player_id: String)
# Player data cache # Player data cache
var players: Array = [] 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_admin: bool = false
var is_host: 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") push_error("[AdminPanel] Not connected to Nakama")
return {"error": "Not connected"} return {"error": "Not connected"}
var result := await NakamaManager.client.rpc_async( var result = await NakamaManager.client.rpc_async(
NakamaManager.session, NakamaManager.session,
rpc_name, rpc_name,
JSON.stringify(payload) JSON.stringify(payload)
@@ -185,10 +185,10 @@ func _update_action_buttons() -> void:
var idx: int = selected[0] var idx: int = selected[0]
var meta: Dictionary = player_list.get_item_metadata(idx) 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 # 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 kick_btn.disabled = is_player_host or is_self
ban_btn.disabled = is_player_host or is_self or not is_admin 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() var duration_input := SpinBox.new()
duration_input.min_value = 0 duration_input.min_value = 0
duration_input.max_value = 8760 # 1 year duration_input.max_value = 8760 # 1 year
duration_input.value = 24 # Default 24 hours duration_input.value = 24 # Default 24 hours
vbox.add_child(duration_input) vbox.add_child(duration_input)
dialog.add_child(vbox) dialog.add_child(vbox)
+2 -2
View File
@@ -15,7 +15,7 @@ extends Control
var update_manager: Node var update_manager: Node
var update_info: Dictionary = {} 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: func _ready() -> void:
# Get or create the update manager # Get or create the update manager
@@ -49,7 +49,7 @@ func _get_update_manager() -> Node:
# Otherwise, create instance # Otherwise, create instance
var manager_script := load("res://scripts/managers/game_update_manager.gd") 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" manager.name = "GameUpdateManager"
get_tree().root.add_child(manager) get_tree().root.add_child(manager)
return manager return manager