547 lines
17 KiB
GDScript
547 lines
17 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 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")
|