Files
tekton/scripts/ui/login_screen.gd
T
2026-04-08 03:12:55 +08:00

449 lines
13 KiB
GDScript

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")