extends Control # UI References - Main Menu @onready var main_menu_panel = $MainMenuPanel @onready var main_title = %Title @onready var username_label = %Username @onready var main_subtitle = %Subtitle @onready var create_room_btn = %CreateRoomBtn @onready var browse_rooms_btn = %BrowseRoomsBtn @onready var tutorial_btn = %TutorialBtn @onready var main_menu_profile_btn = %MainProfileBtn @onready var avatar_display = %AvatarDisplay @onready var lobby_settings_btn = %SettingsBtn @onready var quit_btn = %QuitBtn # Main Menu 3D Preview @onready var character_root = %CharacterRoot @onready var anim_player = %AnimationPlayer # UI References - Currencies @onready var gold_label = %GoldLabel @onready var star_label = %StarLabel # UI References - Server Selection @onready var server_option = %ServerOption @onready var server_ip_input = %ServerIPInput # Leaderboard Reference @onready var leaderboard_btn = %LeaderboardBtn @onready var shop_btn = %CartBtn @onready var top_right_profile_btn = %ProfileBtn @onready var mailbox_btn = get_node_or_null("%MailboxBtn") @onready var mail_badge = get_node_or_null("%MailBadge") @onready var banner1_btn = get_node_or_null("%Banner1") @onready var ticket_btn = get_node_or_null("%TicketBtn") @onready var mailbox_panel = get_node_or_null("MailboxPanel") # UI References - Room List @onready var room_list_panel = %RoomListPanel @onready var room_list = get_node_or_null("%RoomList") @onready var match_id_input = get_node_or_null("%MatchIdInput") @onready var refresh_btn = get_node_or_null("%RefreshBtn") @onready var join_btn = get_node_or_null("%JoinBtn") @onready var back_btn = get_node_or_null("%RoomListCloseBtn") @onready var room_list_profile_btn = get_node_or_null("%RoomListProfileBtn") @onready var item_template = get_node_or_null("%ItemTemplate") @onready var room_player_username = get_node_or_null("%PlayerUsername") @onready var room_player_score = get_node_or_null("%PlayerScore") @onready var room_player_rank = get_node_or_null("%Rank") @onready var room_avatar = get_node_or_null("%Avatar") # Cached leaderboard rank for local player var _local_player_rank: int = 0 # UI References - Lobby Panel @onready var lobby_panel = $LobbyPanel @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 @onready var lobby_top_settings_btn = $LobbyPanel/TopBar/ProfileSection/LobbySettingsBtn @onready var random_spawn_check = $LobbyPanel/TopBar/SettingsSection/RandomSpawnCheck @onready var random_spawn_label = $LobbyPanel/TopBar/SettingsSection/RandomSpawnLabel @onready var enable_timer_check = $LobbyPanel/TopBar/SettingsSection/EnableTimerCheck @onready var enable_timer_label = $LobbyPanel/TopBar/SettingsSection/EnableTimerLabel @onready var scarcity_option = $LobbyPanel/TopBar/SettingsSection/ScarcityOption @onready var scarcity_label = $LobbyPanel/TopBar/SettingsSection/ScarcityLabel @onready var scarcity_spacer = $LobbyPanel/TopBar/SettingsSection/ScarcitySpacer @onready var spawn_spacer = $LobbyPanel/TopBar/SettingsSection/SpawnSpacer @onready var timer_spacer = $LobbyPanel/TopBar/SettingsSection/TimerSpacer @onready var game_mode_option = $LobbyPanel/TopBar/SettingsSection/GameModeOption @onready var game_mode_text_label = $LobbyPanel/TopBar/SettingsSection/GameModeTextLabel # Custom Settings Containers var sng_settings_container: HBoxContainer var sng_go_option: OptionButton var sng_stop_option: OptionButton var sng_goals_option: OptionButton var doors_settings_container: HBoxContainer var doors_swap_option: OptionButton var doors_refresh_option: OptionButton var doors_goals_option: OptionButton # UI References - Player Slots @onready var players_container = $LobbyPanel/PlayersContainer @onready var players_container2 = $LobbyPanel/PlayersContainer2 @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 var invite_btn: Button @onready var status_label = $LobbyPanel/StatusLabel # Social Panel instance var social_panel_instance: Control # Lobby invite popup var _invite_popup: AcceptDialog var _pending_invite_match_id: String = "" # UI References - Status @onready var connection_status = $StatusBar/ConnectionStatus # Character preview textures var character_textures: Dictionary = {} # Profile panel instance var profile_panel_instance: Control var admin_panel_instance: Control # Store current match ID for copy function var current_match_id: String = "" var leaderboard_panel_instance: Control var shop_panel_instance: Control var daily_reward_panel_instance: Control var _mailbox_panel_instance: Control # Bot name tracking keyed by slot index to avoid re-generating on each update var _bot_names: Dictionary = {} # Room list filter ("" = all, "Freemode", "Stop n Go", etc.) var _room_mode_filter: String = "" # ============================================================================= # Chat System # ============================================================================= var chat: LobbyChat var main_menu: LobbyMainMenu var room_list_helper: LobbyRoomList var room_helper: LobbyRoom @onready var chat_display: RichTextLabel = %RichTextLabel @onready var chat_input: LineEdit = %ChatInput @onready var chat_send_btn: Button = %SendBtn var _friend_suggest_panel: PanelContainer var _friend_suggest_list: ItemList # Server Selection Controls (Now in tscn) # var server_option: OptionButton # var server_ip_input: LineEdit func _ready(): chat = LobbyChat.new(self) main_menu = LobbyMainMenu.new(self) room_list_helper = LobbyRoomList.new(self) room_helper = LobbyRoom.new(self) # Start background music for Lobby MusicManager.start_music() # Load character textures _load_character_textures() # Server config UI is now in tscn # Get player slot references _setup_player_slots() # Setup Game Mode specific UI dynamically _create_custom_settings_ui() # Initial UI update _sync_room_profile_card() # Connect Social / Friend UI var global_chat_tab_btn = get_node_or_null("%GlobalChatTabBtn") if global_chat_tab_btn: global_chat_tab_btn.pressed.connect(func(): chat.switch_chat_tab("global")) FriendManager.dm_message_received.connect(chat.on_lobby_dm_received) # Connect Server Selection signals if server_option: server_option.item_selected.connect(_on_server_option_selected) # Initialize state _on_server_option_selected(server_option.selected) if server_ip_input: server_ip_input.text_submitted.connect(_on_server_ip_submitted) server_ip_input.focus_exited.connect(func(): _on_server_ip_submitted(server_ip_input.text)) if match_id_input: match_id_input.text_submitted.connect(func(_text): if room_list_helper: room_list_helper._on_join_pressed()) # Connect Side Tab switching var play_side_btn = get_node_or_null("%PlayTabSideBtn") var room_side_btn = get_node_or_null("%RoomTabSideBtn") var room_tabs = get_node_or_null("%RoomListTabs") if play_side_btn and room_side_btn and room_tabs: play_side_btn.pressed.connect(func(): room_tabs.current_tab = 0 play_side_btn.button_pressed = true room_side_btn.button_pressed = false ) room_side_btn.pressed.connect(func(): room_tabs.current_tab = 1 play_side_btn.button_pressed = false room_side_btn.button_pressed = true ) # Connect Play Tab mode buttons var room_free_mode_btn = get_node_or_null("%RoomFreeModeBtn") var room_stop_n_go_btn = get_node_or_null("%RoomStopNGoBtn") if room_free_mode_btn: room_free_mode_btn.pressed.connect(func(): _room_mode_filter = "Freemode" if _room_mode_filter != "Freemode" else "" LobbyManager.refresh_room_list() ) if room_stop_n_go_btn: room_stop_n_go_btn.pressed.connect(func(): _room_mode_filter = "Stop n Go" if _room_mode_filter != "Stop n Go" else "" LobbyManager.refresh_room_list() ) # Connect button signals - Lobby if lobby_top_settings_btn: lobby_top_settings_btn.pressed.connect(main_menu.on_settings_pressed) # Connect Social / Friend UI invite_btn = get_node_or_null("LobbyPanel/BottomBar/InviteBtn") if invite_btn: invite_btn.pressed.connect(room_helper.on_invite_friends_pressed) # Connect UserProfileManager signals UserProfileManager.profile_loaded.connect(func(_p): _sync_room_profile_card()) UserProfileManager.profile_updated.connect(func(): _sync_room_profile_card()) # Connect Mailbox UI if MailManager: MailManager.unread_count_changed.connect(_on_mail_unread_count_changed) # Show main menu initially _show_panel("main_menu") # Check for disconnection reason from manager if not LobbyManager.disconnect_reason.is_empty(): connection_status.text = LobbyManager.disconnect_reason LobbyManager.disconnect_reason = "" # Try to join global chat if already connected if NakamaManager.is_connected_to_nakama(): chat.join_global_chat() # ============================================================================= # Setup # ============================================================================= const CHAR_NODE_MAP: Dictionary = { "Copper": "Oldpop", "Dabro": "Masbro", "Gatot": "Gatot", "Pip": "Bob" } func _setup_3d_preview() -> void: """Swaps out the active character inside the 3D MainMenu SubViewport.""" if not character_root: return var target_character = UserProfileManager.profile.get("loadout_character", "Copper") var node_name = CHAR_NODE_MAP.get(target_character, "Bob") for child in character_root.get_children(): if child is Node3D: child.visible = (child.name == node_name) if anim_player: var new_root: Node3D = character_root.get_node_or_null(node_name) as Node3D if new_root: anim_player.root_node = new_root.get_path() if anim_player.has_animation("animation-pack/idle"): anim_player.play("animation-pack/idle") elif anim_player.get_animation_list().size() > 0: anim_player.play(anim_player.get_animation_list()[0]) # Apply equipped cosmetics so the lobby preview shows the current loadout SkinManager.apply_loadout(character_root, UserProfileManager.loadout) func _load_character_textures() -> void: """Load character preview textures.""" var characters = { "Copper": "res://assets/graphics/character_selection/sc_characters/sc_copper.png", "Dabro": "res://assets/graphics/character_selection/sc_characters/sc_dabro.png", "Gatot": "res://assets/graphics/character_selection/sc_characters/sc_gatot.png", "Pip": "res://assets/graphics/character_selection/sc_characters/sc_pip.png", "Random": "res://assets/graphics/character_selection/sc_characters/sc_unknown.png" } for char_name in characters: var tex_path = characters[char_name] if ResourceLoader.exists(tex_path): character_textures[char_name] = load(tex_path) else: print("[Lobby] Character texture not found: ", tex_path) func _on_server_option_selected(index: int) -> void: if main_subtitle and server_option: main_subtitle.text = server_option.get_item_text(index).to_upper() if index == 0: # Nakama Localhost if server_ip_input: server_ip_input.visible = false NakamaManager.set_server("localhost") LobbyManager.is_lan_mode = false connection_status.text = "Mode: Local Testing (Nakama)" elif index == 1: # Nakama Remote if server_ip_input: server_ip_input.visible = true server_ip_input.placeholder_text = "IP (100.x) or Tailscale Funnel URL..." if server_ip_input: NakamaManager.set_server(server_ip_input.text) LobbyManager.is_lan_mode = false connection_status.text = "Mode: Online (Nakama Remote)" elif index == 2: # LAN Direct if server_ip_input: server_ip_input.visible = false LobbyManager.is_lan_mode = true connection_status.text = "Mode: LAN Direct (No Server)" elif index == 3: # Tekton Dash EU if server_ip_input: server_ip_input.visible = false NakamaManager.set_server("tektondash.vps.webdock.cloud") LobbyManager.is_lan_mode = false connection_status.text = "Mode: Online (Tekton Dash EU)" func _on_server_ip_submitted(new_text: String) -> void: if server_option and server_option.selected == 1: var host = new_text.strip_edges() NakamaManager.set_server(host) if host.ends_with(".ts.net"): connection_status.text = "Using Tailscale Funnel: " + host else: connection_status.text = "Server IP updated: " + host func _setup_game_modes() -> void: if not game_mode_option: return game_mode_option.clear() for mode in LobbyManager.available_game_modes: game_mode_option.add_item(mode) # Select current mode for i in range(game_mode_option.item_count): if game_mode_option.get_item_text(i) == LobbyManager.game_mode: game_mode_option.selected = i break func _setup_player_slots() -> void: """Get references to all player slot nodes.""" player_slots.clear() # Slots 1-4 in Container 1 for i in range(1, 5): var slot = players_container.get_node_or_null("PlayerSlot%d" % i) if slot: player_slots.append(slot) _connect_slot_signals(slot, i) # Slots 5-8 in Container 2 if players_container2: for i in range(5, 9): var slot = players_container2.get_node_or_null("PlayerSlot%d" % i) if slot: player_slots.append(slot) _connect_slot_signals(slot, i) func _connect_slot_signals(slot: Control, i: int): # 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 # ============================================================================= func _show_panel(panel_name: String) -> void: main_menu_panel.visible = panel_name == "main_menu" room_list_panel.visible = panel_name == "room_list" lobby_panel.visible = panel_name == "lobby" func _update_settings_visibility() -> void: var is_host = LobbyManager.is_host var is_freemode = LobbyManager.game_mode == "Freemode" # Duration var show_duration = is_freemode duration_option.visible = is_host and show_duration duration_text_label.visible = not is_host and show_duration $LobbyPanel/TopBar/SettingsSection/DurationLabel.visible = show_duration # Random Spawn var show_spawn = is_freemode random_spawn_check.visible = is_host and show_spawn random_spawn_label.visible = not is_host and show_spawn if spawn_spacer: spawn_spacer.visible = show_spawn # Timer var show_timer = is_freemode enable_timer_check.visible = is_host and show_timer enable_timer_label.visible = not is_host and show_timer if timer_spacer: timer_spacer.visible = show_timer # Scarcity var show_scarcity = is_freemode if scarcity_option: scarcity_option.visible = is_host and show_scarcity if scarcity_label: scarcity_label.visible = not is_host and show_scarcity if scarcity_spacer: scarcity_spacer.visible = show_scarcity # Custom mode sets var is_sng = LobbyManager.game_mode == "Stop n Go" if sng_settings_container: sng_settings_container.visible = is_sng sng_go_option.disabled = not is_host sng_stop_option.disabled = not is_host sng_goals_option.disabled = not is_host sng_goals_option.disabled = not is_host func _create_custom_settings_ui() -> void: var settings_section = $LobbyPanel/TopBar/SettingsSection if not settings_section: return # Stop n Go sng_settings_container = HBoxContainer.new() sng_settings_container.visible = false settings_section.add_child(sng_settings_container) _add_label(sng_settings_container, "Go Time:") sng_go_option = OptionButton.new() sng_go_option.add_item("10s"); sng_go_option.add_item("15s"); sng_go_option.add_item("25s") sng_go_option.item_selected.connect(func(idx): if LobbyManager.is_host: LobbyManager.set_sng_go_duration([10, 15, 25][idx])) sng_settings_container.add_child(sng_go_option) _add_label(sng_settings_container, "Stop Time:") sng_stop_option = OptionButton.new() sng_stop_option.add_item("3s"); sng_stop_option.add_item("4s"); sng_stop_option.add_item("5s") sng_stop_option.item_selected.connect(func(idx): if LobbyManager.is_host: LobbyManager.set_sng_stop_duration([3, 4, 5][idx])) sng_settings_container.add_child(sng_stop_option) _add_label(sng_settings_container, "Req Goals:") sng_goals_option = OptionButton.new() sng_goals_option.add_item("5"); sng_goals_option.add_item("8"); sng_goals_option.add_item("12") sng_goals_option.item_selected.connect(func(idx): if LobbyManager.is_host: LobbyManager.set_sng_required_goals([5, 8, 12][idx])) sng_settings_container.add_child(sng_goals_option) # Move Game Mode selector to the far right var gm_spacer = settings_section.get_node_or_null("GameModeSpacer") var gm_option = settings_section.get_node_or_null("GameModeOption") var gm_label = settings_section.get_node_or_null("GameModeTextLabel") if gm_spacer: settings_section.move_child(gm_spacer, -1) if gm_option: settings_section.move_child(gm_option, -1) if gm_label: settings_section.move_child(gm_label, -1) func _add_label(parent: Control, text: String): var spacer = Control.new() spacer.custom_minimum_size = Vector2(10, 0) parent.add_child(spacer) var lbl = Label.new() lbl.text = text parent.add_child(lbl) func _on_mail_unread_count_changed(count: int) -> void: if mail_badge: if count > 0: mail_badge.text = str(count) if count < 100 else "99+" mail_badge.visible = true else: mail_badge.visible = false func _sync_room_profile_card() -> void: if room_player_username: room_player_username.text = UserProfileManager.get_display_name() if room_player_score: room_player_score.text = "Score: %d" % UserProfileManager.get_stats().get("high_score", 0) if room_player_rank: room_player_rank.text = "Rank: %s" % (_local_player_rank if _local_player_rank > 0 else "-") var avatar_path = UserProfileManager.get_avatar_url() if not avatar_path.is_empty() and ResourceLoader.exists(avatar_path): var tex = load(avatar_path) if tex: if avatar_display: avatar_display.texture = tex if room_avatar: if room_avatar is TextureRect: room_avatar.texture = tex elif room_avatar is Panel: var style = StyleBoxTexture.new() style.texture = tex room_avatar.add_theme_stylebox_override("panel", style) if main_menu_profile_btn: var tr = main_menu_profile_btn.get_node_or_null("TextureRect") if tr: tr.texture = tex if top_right_profile_btn: var tr = top_right_profile_btn.get_node_or_null("TextureRect") if tr: tr.texture = tex if room_list_profile_btn: var tr = room_list_profile_btn.get_node_or_null("TextureRect") if tr: tr.texture = tex func _apply_loadout_character() -> void: """Apply the player's saved loadout default character to LobbyManager before entering a room.""" var saved_char: String = UserProfileManager.profile.get("loadout_character", "") if saved_char.is_empty(): return var idx := LobbyManager.available_characters.find(saved_char) if idx != -1: LobbyManager.local_character_index = idx print("[Lobby] Loadout character applied: ", saved_char)