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 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 tab_container := %TabContainer as TabContainer @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 reg_status_label := %RegStatusLabel as Label var current_captcha_answer: int = 0 var is_loading: bool = false # Server Selection Controls @onready var server_option := %ServerOption as OptionButton @onready var server_ip_input := %ServerIPInput as LineEdit @onready var lan_section := %LANSection as VBoxContainer @onready var lan_host_btn := %LANHostBtn as Button @onready var lan_ip := %LANIPInput as LineEdit @onready var lan_join_btn := %LANJoinBtn as Button func _ready() -> void: _connect_signals() _setup_ui() # Initialize connection mode view 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_ip_input.text = NakamaManager.nakama_host if NakamaManager.nakama_host != "localhost" else "127.0.0.1" _on_server_option_selected(server_option.selected) # 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) # 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) # 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) tab_container.tab_changed.connect(_on_tab_changed) server_option.item_selected.connect(_on_server_option_selected) 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)) lan_host_btn.pressed.connect(_on_lan_host_pressed) lan_join_btn.pressed.connect(func(): _on_lan_join_pressed(lan_ip.text)) # 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 tab_container.current_tab = 0 # 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 _on_tab_changed(tab: int) -> void: if tab == 0: status_label.text = "" email_input.grab_focus() elif tab == 1: 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 = "" # ============================================================================= # 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 _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 = 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 tab_container.current_tab == 1: _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")