extends Control ## Login screen controller - handles authentication UI # Login panel elements @onready var guest_button := %GuestButton as Button @onready var email_input := %EmailInput as LineEdit @onready var password_input := %PasswordInput as LineEdit @onready var remember_me := %RememberMe as CheckBox @onready var login_button := %LoginButton as Button @onready var register_link := %RegisterLink as LinkButton @onready var google_button := %GoogleButton as Button @onready var apple_button := %AppleButton as Button @onready var facebook_button := %FacebookButton as Button @onready var status_label := %StatusLabel as Label @onready var loading_spinner := %LoadingSpinner as TextureProgressBar # Registration panel elements @onready var registration_panel := %RegistrationPanel as PanelContainer @onready var reg_email_input := %RegEmailInput as LineEdit @onready var reg_username_input := %RegUsernameInput as LineEdit @onready var reg_password_input := %RegPasswordInput as LineEdit @onready var reg_confirm_password_input := %RegConfirmPasswordInput as LineEdit @onready var password_strength := %PasswordStrength as ProgressBar @onready var password_hint := %PasswordHint as Label @onready var reg_captcha_question := %RegCaptchaQuestion as Label @onready var reg_captcha_input := %RegCaptchaInput as LineEdit @onready var register_button := %RegisterButton as Button @onready var back_to_login_link := %BackToLoginLink as LinkButton @onready var reg_status_label := %RegStatusLabel as Label var current_captcha_answer: int = 0 # Main panel reference @onready var main_panel := $CenterContainer/MainPanel as PanelContainer var is_loading: bool = false # Server Selection Controls var server_option: OptionButton var server_ip_input: LineEdit var lan_section: VBoxContainer # LAN-specific controls func _ready() -> void: _connect_signals() _setup_ui() _setup_server_config_ui() # Check if already authenticated if AuthManager.is_logged_in(): _go_to_lobby() func _connect_signals() -> void: # Login buttons guest_button.pressed.connect(_on_guest_pressed) login_button.pressed.connect(_on_login_pressed) register_link.pressed.connect(_show_registration) # Social buttons google_button.pressed.connect(_on_google_pressed) apple_button.pressed.connect(_on_apple_pressed) facebook_button.pressed.connect(_on_facebook_pressed) # Registration buttons register_button.pressed.connect(_on_register_pressed) back_to_login_link.pressed.connect(_show_login) # Password strength checker reg_password_input.text_changed.connect(_check_password_strength) # Auth manager signals AuthManager.auth_started.connect(_on_auth_started) AuthManager.auth_completed.connect(_on_auth_completed) AuthManager.auth_failed.connect(_on_auth_failed) AuthManager.session_restored.connect(_on_session_restored) # Enter key to submit password_input.text_submitted.connect(func(_t): _on_login_pressed()) reg_confirm_password_input.text_submitted.connect(func(_t): _on_register_pressed()) func _setup_ui() -> void: status_label.text = "" reg_status_label.text = "" loading_spinner.visible = false registration_panel.visible = false main_panel.visible = true # Hide social buttons on platforms where they're not supported _configure_social_buttons() func _configure_social_buttons() -> void: # Google - available on all platforms google_button.visible = true # Apple - iOS and macOS only (or hide if not configured) var os := OS.get_name() apple_button.visible = os in ["iOS", "macOS"] # Facebook - available on all platforms facebook_button.visible = true # ============================================================================= # Panel Switching # ============================================================================= func _show_registration() -> void: main_panel.visible = false registration_panel.visible = true reg_status_label.text = "" _generate_captcha() reg_email_input.grab_focus() func _generate_captcha() -> void: var num1 := randi_range(1, 10) var num2 := randi_range(1, 10) current_captcha_answer = num1 + num2 reg_captcha_question.text = "Security Check: %d + %d = ?" % [num1, num2] reg_captcha_input.text = "" func _show_login() -> void: registration_panel.visible = false main_panel.visible = true status_label.text = "" email_input.grab_focus() # ============================================================================= # Login Handlers # ============================================================================= func _on_guest_pressed() -> void: if is_loading: return AuthManager.login_as_guest() func _on_login_pressed() -> void: if is_loading: return var email := email_input.text.strip_edges() var password := password_input.text if email.is_empty(): _show_error("Please enter your email") return if password.is_empty(): _show_error("Please enter your password") return if not _is_valid_email(email): _show_error("Please enter a valid email address") return AuthManager.login_with_email(email, password, remember_me.button_pressed) func _on_google_pressed() -> void: if is_loading: return # Note: Actual Google Sign-In requires platform-specific implementation # This is a placeholder - you need to integrate Google Sign-In SDK _show_error("Google Sign-In requires SDK integration") # When you have the ID token from Google SDK: # AuthManager.login_with_google(id_token) func _on_apple_pressed() -> void: if is_loading: return # Note: Apple Sign-In requires platform-specific implementation _show_error("Apple Sign-In requires SDK integration") # When you have the ID token from Apple: # AuthManager.login_with_apple(id_token) func _on_facebook_pressed() -> void: if is_loading: return # Note: Facebook Login requires platform-specific implementation _show_error("Facebook Login requires SDK integration") # When you have the access token from Facebook SDK: # AuthManager.login_with_facebook(access_token) func _setup_server_config_ui() -> void: """Inject server configuration controls into MainPanel.""" if not main_panel: return var vbox = main_panel.get_node_or_null("VBox") if not vbox: return # Find where to insert (before GuestButton) var insert_pos = 3 # Default position if guest_button: insert_pos = guest_button.get_index() # Create Server Section var server_section = VBoxContainer.new() server_section.name = "ServerSelectionSection" server_section.add_theme_constant_override("separation", 10) vbox.add_child(server_section) vbox.move_child(server_section, insert_pos) # Server Label var label = Label.new() label.text = "CONNECTION MODE" label.add_theme_color_override("font_color", Color(0.69, 0.529, 0.357, 1)) label.add_theme_font_size_override("font_size", 13) server_section.add_child(label) # Server OptionButton server_option = OptionButton.new() server_option.name = "ServerOption" server_option.custom_minimum_size = Vector2(0, 44) server_option.add_item("Nakama - Localhost (Testing)") server_option.add_item("Nakama - Remote Server (Host IP)") server_option.add_item("LAN Direct (No Server)") server_option.add_item("Nakama - Tekton Dash EU") # Set initial state based on NakamaManager if NakamaManager.nakama_host == "localhost": server_option.selected = 0 elif NakamaManager.nakama_host == "tektondash.vps.webdock.cloud": server_option.selected = 3 else: server_option.selected = 1 server_option.item_selected.connect(_on_server_option_selected) server_section.add_child(server_option) # Nakama Server IP Input server_ip_input = LineEdit.new() server_ip_input.name = "ServerIPInput" server_ip_input.custom_minimum_size = Vector2(0, 44) server_ip_input.placeholder_text = "Enter Nakama Server IP..." server_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1" server_ip_input.visible = server_option.selected == 1 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)) server_section.add_child(server_ip_input) # --- LAN Section --- lan_section = VBoxContainer.new() lan_section.name = "LANSection" lan_section.add_theme_constant_override("separation", 8) lan_section.visible = false server_section.add_child(lan_section) var lan_info = Label.new() lan_info.text = "Play over LAN without any server.\nFirewall may need to allow port 7777." lan_info.add_theme_color_override("font_color", Color(0.7, 0.7, 0.7, 1)) lan_info.add_theme_font_size_override("font_size", 12) lan_info.autowrap_mode = TextServer.AUTOWRAP_WORD lan_section.add_child(lan_info) # Host LAN button var lan_host_btn = Button.new() lan_host_btn.name = "LANHostBtn" lan_host_btn.text = "HOST LAN GAME" lan_host_btn.custom_minimum_size = Vector2(0, 44) lan_host_btn.pressed.connect(_on_lan_host_pressed) lan_section.add_child(lan_host_btn) var lan_sep = Label.new() lan_sep.text = "── or join a friend ──" lan_sep.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lan_sep.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1)) lan_sep.add_theme_font_size_override("font_size", 11) lan_section.add_child(lan_sep) # LAN Host IP input var lan_ip = LineEdit.new() lan_ip.name = "LANIPInput" lan_ip.custom_minimum_size = Vector2(0, 44) lan_ip.placeholder_text = "Host IP (e.g. 192.168.1.10)" lan_ip.text = "127.0.0.1" lan_section.add_child(lan_ip) # Join LAN button var lan_join_btn = Button.new() lan_join_btn.name = "LANJoinBtn" lan_join_btn.text = "JOIN LAN GAME" lan_join_btn.custom_minimum_size = Vector2(0, 44) lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text)) lan_section.add_child(lan_join_btn) # Add a separator after the section var separator = HSeparator.new() vbox.add_child(separator) vbox.move_child(separator, insert_pos + 1) func _on_server_option_selected(index: int) -> void: if index == 0: # Nakama Localhost server_ip_input.visible = false if lan_section: lan_section.visible = false NakamaManager.set_server("localhost") elif index == 1: # Nakama Remote server_ip_input.visible = true if lan_section: lan_section.visible = false NakamaManager.set_server(server_ip_input.text) elif index == 2: # LAN Direct server_ip_input.visible = false if lan_section: lan_section.visible = true elif index == 3: # Tekton Dash EU server_ip_input.visible = false if lan_section: lan_section.visible = false NakamaManager.set_server("tektondash.vps.webdock.cloud") func _on_server_ip_submitted(new_text: String) -> void: if server_option and server_option.selected == 1: NakamaManager.set_server(new_text.strip_edges()) func _on_lan_host_pressed() -> void: """Host a LAN game without logging in to Nakama.""" var player_name = email_input.text.strip_edges() if player_name.is_empty(): player_name = "Host" LobbyManager.local_player_name = player_name var ok = await LobbyManager.create_room_lan() if ok: _go_to_lobby() else: _show_error("Failed to create LAN server. Check firewall for port 7777.") func _on_lan_join_pressed(host_ip: String) -> void: """Join a LAN game without logging in to Nakama.""" var ip = host_ip.strip_edges() if ip.is_empty(): _show_error("Please enter the host's IP address.") return var player_name = email_input.text.strip_edges() if player_name.is_empty(): player_name = "Player" LobbyManager.local_player_name = player_name var ok = LobbyManager.join_room_lan(ip) if ok: _go_to_lobby() else: _show_error("Failed to connect to %s. Is the host running?" % ip) # ============================================================================= # Registration Handlers # ============================================================================= func _on_register_pressed() -> void: if is_loading: return var email := reg_email_input.text.strip_edges() var username := reg_username_input.text.strip_edges() var password := reg_password_input.text var confirm_password := reg_confirm_password_input.text var captcha_answer := reg_captcha_input.text.strip_edges() # Validation if email.is_empty(): _show_reg_error("Please enter your email") return if not _is_valid_email(email): _show_reg_error("Please enter a valid email address") return if username.is_empty(): _show_reg_error("Please enter a username") return if username.length() < 3: _show_reg_error("Username must be at least 3 characters") return if password.is_empty(): _show_reg_error("Please enter a password") return if password.length() < 8: _show_reg_error("Password must be at least 8 characters") return if password != confirm_password: _show_reg_error("Passwords do not match") return if _calculate_password_strength(password) < 2: _show_reg_error("Password is too weak. Add numbers or symbols.") return if captcha_answer.is_empty(): _show_reg_error("Please solve the security check.") return if not captcha_answer.is_valid_int() or int(captcha_answer) != current_captcha_answer: _show_reg_error("Incorrect security check answer.") _generate_captcha() return AuthManager.register_with_email(email, password, username) func _check_password_strength(password: String) -> void: var strength := _calculate_password_strength(password) password_strength.value = strength # Color based on strength var color: Color match strength: 0, 1: color = Color.RED password_hint.text = "Weak - add more characters" 2: color = Color.ORANGE password_hint.text = "Fair - add numbers or symbols" 3: color = Color.YELLOW_GREEN password_hint.text = "Good" 4: color = Color.GREEN password_hint.text = "Strong!" # Apply color to progress bar var style := StyleBoxFlat.new() style.bg_color = color password_strength.add_theme_stylebox_override("fill", style) func _calculate_password_strength(password: String) -> int: var strength: float = 0.0 if password.length() >= 8: strength += 1 if password.length() >= 12: strength += 1 var has_upper := false var has_lower := false var has_digit := false var has_special := false for c in password: if c.to_upper() != c.to_lower(): if c == c.to_upper(): has_upper = true else: has_lower = true elif c.is_valid_int(): has_digit = true else: has_special = true if has_upper and has_lower: strength += 1 if has_digit: strength += 0.5 if has_special: strength += 0.5 return clampi(int(strength), 0, 4) # ============================================================================= # Auth Manager Callbacks # ============================================================================= func _on_auth_started() -> void: is_loading = true loading_spinner.visible = true _set_inputs_enabled(false) status_label.text = "" reg_status_label.text = "" func _on_auth_completed(success: bool, _user_data: Dictionary) -> void: # Check if node is still in tree (may have been freed during scene transition) if not is_inside_tree(): return is_loading = false loading_spinner.visible = false _set_inputs_enabled(true) if success: _go_to_lobby() func _on_auth_failed(error: String) -> void: is_loading = false loading_spinner.visible = false _set_inputs_enabled(true) if registration_panel.visible: _show_reg_error(error) else: _show_error(error) func _on_session_restored() -> void: if not is_inside_tree(): return _go_to_lobby() # ============================================================================= # Helper Functions # ============================================================================= func _show_error(message: String) -> void: status_label.text = message status_label.add_theme_color_override("font_color", Color(1, 0.4, 0.4)) func _show_reg_error(message: String) -> void: reg_status_label.text = message reg_status_label.add_theme_color_override("font_color", Color(1, 0.4, 0.4)) func _set_inputs_enabled(enabled: bool) -> void: guest_button.disabled = not enabled login_button.disabled = not enabled register_button.disabled = not enabled google_button.disabled = not enabled apple_button.disabled = not enabled facebook_button.disabled = not enabled email_input.editable = enabled password_input.editable = enabled reg_email_input.editable = enabled reg_username_input.editable = enabled reg_password_input.editable = enabled reg_confirm_password_input.editable = enabled reg_captcha_input.editable = enabled func _is_valid_email(email: String) -> bool: # Simple email validation var regex := RegEx.new() regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$") return regex.search(email) != null func _go_to_lobby() -> void: # Navigate to lobby scene - use deferred call to avoid issues during signal callbacks if get_tree(): get_tree().change_scene_to_file("res://scenes/lobby.tscn") else: # Fallback: try deferred call call_deferred("_deferred_go_to_lobby") func _deferred_go_to_lobby() -> void: if get_tree(): get_tree().change_scene_to_file("res://scenes/lobby.tscn")