update
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
extends PanelContainer
|
||||
## Admin panel for in-game server management using Nakama RPC
|
||||
## Requires server-side tekton_admin.ts module for security
|
||||
|
||||
signal closed
|
||||
signal player_kicked(player_id: String)
|
||||
signal player_banned(player_id: String)
|
||||
|
||||
@onready var close_button := %CloseButton as Button
|
||||
@onready var player_list := %PlayerList as ItemList
|
||||
@onready var kick_btn := %KickBtn as Button
|
||||
@onready var ban_btn := %BanBtn as Button
|
||||
@onready var mute_btn := %MuteBtn as Button
|
||||
@onready var player_count_label := %PlayerCount as Label
|
||||
@onready var match_id_label := %MatchIdLabel as Label
|
||||
@onready var server_status_label := %ServerStatus as Label
|
||||
@onready var end_match_btn := %EndMatchBtn as Button
|
||||
@onready var restart_match_btn := %RestartMatchBtn as Button
|
||||
@onready var ban_list := %BanList as ItemList
|
||||
@onready var unban_btn := %UnbanBtn as Button
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
|
||||
# Player data cache
|
||||
var players: Array = []
|
||||
var banned_players: Array = [] # [{user_id, username, banned_at, reason, expires}]
|
||||
var is_admin: bool = false
|
||||
var is_host: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_connect_signals()
|
||||
visible = false
|
||||
|
||||
func _connect_signals() -> void:
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
kick_btn.pressed.connect(_on_kick_pressed)
|
||||
ban_btn.pressed.connect(_on_ban_pressed)
|
||||
mute_btn.pressed.connect(_on_mute_pressed)
|
||||
end_match_btn.pressed.connect(_on_end_match_pressed)
|
||||
restart_match_btn.pressed.connect(_on_restart_match_pressed)
|
||||
unban_btn.pressed.connect(_on_unban_pressed)
|
||||
player_list.item_selected.connect(_on_player_selected)
|
||||
|
||||
func show_panel() -> void:
|
||||
# Check permissions
|
||||
is_host = LobbyManager.is_host
|
||||
is_admin = await _check_admin_status()
|
||||
|
||||
if not is_host and not is_admin:
|
||||
status_label.text = "Admin access required"
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
return
|
||||
|
||||
# Show/hide admin-only features
|
||||
ban_btn.visible = is_admin
|
||||
unban_btn.visible = is_admin
|
||||
|
||||
await _refresh_all_data()
|
||||
visible = true
|
||||
|
||||
func _check_admin_status() -> bool:
|
||||
# Check with server if user has admin role
|
||||
var result := await _rpc_call("get_user_profile", {})
|
||||
if result.has("role"):
|
||||
return result.role in ["admin", "moderator", "owner"]
|
||||
return false
|
||||
|
||||
func _on_close_pressed() -> void:
|
||||
visible = false
|
||||
emit_signal("closed")
|
||||
|
||||
# =============================================================================
|
||||
# RPC Helpers
|
||||
# =============================================================================
|
||||
|
||||
func _rpc_call(rpc_name: String, payload: Dictionary) -> Dictionary:
|
||||
if not NakamaManager.client or not NakamaManager.session:
|
||||
push_error("[AdminPanel] Not connected to Nakama")
|
||||
return {"error": "Not connected"}
|
||||
|
||||
var result := await NakamaManager.client.rpc_async(
|
||||
NakamaManager.session,
|
||||
rpc_name,
|
||||
JSON.stringify(payload)
|
||||
)
|
||||
|
||||
if result.is_exception():
|
||||
var error: String = result.get_exception().message
|
||||
push_error("[AdminPanel] RPC Error: " + error)
|
||||
status_label.text = "Error: " + error
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
return {"error": error}
|
||||
|
||||
if result.payload:
|
||||
return JSON.parse_string(result.payload)
|
||||
return {}
|
||||
|
||||
# =============================================================================
|
||||
# Data Refresh
|
||||
# =============================================================================
|
||||
|
||||
func _refresh_all_data() -> void:
|
||||
await _refresh_player_list()
|
||||
await _refresh_server_stats()
|
||||
if is_admin:
|
||||
await _refresh_ban_list()
|
||||
status_label.text = ""
|
||||
|
||||
func _refresh_player_list() -> void:
|
||||
player_list.clear()
|
||||
players = LobbyManager.get_players()
|
||||
|
||||
for player in players:
|
||||
var name: String = player.get("name", "Unknown")
|
||||
var id: int = player.get("id", 0)
|
||||
var user_id: String = player.get("user_id", "")
|
||||
var is_player_host := id == 1
|
||||
|
||||
var display := name
|
||||
if is_player_host:
|
||||
display += " (Host)"
|
||||
|
||||
player_list.add_item(display)
|
||||
# Store user_id as metadata
|
||||
player_list.set_item_metadata(player_list.item_count - 1, {
|
||||
"user_id": user_id,
|
||||
"peer_id": id,
|
||||
"name": name
|
||||
})
|
||||
|
||||
_update_action_buttons()
|
||||
|
||||
func _refresh_server_stats() -> void:
|
||||
var stats := await _rpc_call("admin_get_server_stats", {
|
||||
"match_id": NakamaManager.current_match_id
|
||||
})
|
||||
|
||||
if stats.has("error"):
|
||||
return
|
||||
|
||||
player_count_label.text = str(stats.get("total_players", LobbyManager.get_players().size()))
|
||||
|
||||
var match_id := NakamaManager.current_match_id
|
||||
if match_id.length() > 16:
|
||||
match_id = match_id.substr(0, 16) + "..."
|
||||
match_id_label.text = match_id if match_id else "N/A"
|
||||
|
||||
server_status_label.text = "Running"
|
||||
server_status_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
|
||||
func _refresh_ban_list() -> void:
|
||||
ban_list.clear()
|
||||
|
||||
var result := await _rpc_call("admin_get_ban_list", {})
|
||||
|
||||
if result.has("error"):
|
||||
return
|
||||
|
||||
banned_players = result.get("bans", [])
|
||||
|
||||
for ban in banned_players:
|
||||
var display := "%s - %s" % [
|
||||
ban.get("username", "Unknown"),
|
||||
ban.get("reason", "No reason")
|
||||
]
|
||||
if ban.get("expires"):
|
||||
display += " (until " + ban.expires + ")"
|
||||
|
||||
ban_list.add_item(display)
|
||||
ban_list.set_item_metadata(ban_list.item_count - 1, ban)
|
||||
|
||||
# =============================================================================
|
||||
# Player Actions
|
||||
# =============================================================================
|
||||
|
||||
func _on_player_selected(_index: int) -> void:
|
||||
_update_action_buttons()
|
||||
|
||||
func _update_action_buttons() -> void:
|
||||
var selected := player_list.get_selected_items()
|
||||
if selected.is_empty():
|
||||
kick_btn.disabled = true
|
||||
ban_btn.disabled = true
|
||||
mute_btn.disabled = true
|
||||
return
|
||||
|
||||
var idx: int = selected[0]
|
||||
var meta: Dictionary = player_list.get_item_metadata(idx)
|
||||
var is_player_host := meta.get("peer_id", 0) == 1
|
||||
|
||||
# Can't kick/ban the host or yourself
|
||||
var is_self := meta.get("user_id", "") == AuthManager.current_user.get("user_id", "")
|
||||
|
||||
kick_btn.disabled = is_player_host or is_self
|
||||
ban_btn.disabled = is_player_host or is_self or not is_admin
|
||||
mute_btn.disabled = is_self
|
||||
|
||||
func _on_kick_pressed() -> void:
|
||||
var selected := player_list.get_selected_items()
|
||||
if selected.is_empty():
|
||||
return
|
||||
|
||||
var meta: Dictionary = player_list.get_item_metadata(selected[0])
|
||||
var user_id: String = meta.get("user_id", "")
|
||||
var player_name: String = meta.get("name", "Unknown")
|
||||
|
||||
if user_id.is_empty():
|
||||
status_label.text = "Cannot kick: Invalid player"
|
||||
return
|
||||
|
||||
status_label.text = "Kicking player..."
|
||||
|
||||
var result := await _rpc_call("admin_kick_player", {
|
||||
"match_id": NakamaManager.current_match_id,
|
||||
"user_id": user_id,
|
||||
"reason": "Kicked by " + ("admin" if is_admin else "host")
|
||||
})
|
||||
|
||||
if result.get("success", false):
|
||||
status_label.text = "Kicked: " + player_name
|
||||
status_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
emit_signal("player_kicked", user_id)
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
_refresh_player_list()
|
||||
else:
|
||||
status_label.text = "Failed to kick player"
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
|
||||
func _on_ban_pressed() -> void:
|
||||
var selected := player_list.get_selected_items()
|
||||
if selected.is_empty():
|
||||
return
|
||||
|
||||
var meta: Dictionary = player_list.get_item_metadata(selected[0])
|
||||
var user_id: String = meta.get("user_id", "")
|
||||
var player_name: String = meta.get("name", "Unknown")
|
||||
|
||||
if user_id.is_empty():
|
||||
status_label.text = "Cannot ban: Invalid player"
|
||||
return
|
||||
|
||||
# Show ban dialog
|
||||
var dialog := _create_ban_dialog(user_id, player_name)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
|
||||
func _create_ban_dialog(user_id: String, player_name: String) -> ConfirmationDialog:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.title = "Ban Player"
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
|
||||
var info_label := Label.new()
|
||||
info_label.text = "Ban player: " + player_name
|
||||
vbox.add_child(info_label)
|
||||
|
||||
var reason_label := Label.new()
|
||||
reason_label.text = "Reason:"
|
||||
vbox.add_child(reason_label)
|
||||
|
||||
var reason_input := LineEdit.new()
|
||||
reason_input.placeholder_text = "Enter ban reason"
|
||||
vbox.add_child(reason_input)
|
||||
|
||||
var duration_label := Label.new()
|
||||
duration_label.text = "Duration (hours, 0 = permanent):"
|
||||
vbox.add_child(duration_label)
|
||||
|
||||
var duration_input := SpinBox.new()
|
||||
duration_input.min_value = 0
|
||||
duration_input.max_value = 8760 # 1 year
|
||||
duration_input.value = 24 # Default 24 hours
|
||||
vbox.add_child(duration_input)
|
||||
|
||||
dialog.add_child(vbox)
|
||||
|
||||
dialog.confirmed.connect(func():
|
||||
_execute_ban(user_id, player_name, reason_input.text, int(duration_input.value))
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.canceled.connect(func():
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
return dialog
|
||||
|
||||
func _execute_ban(user_id: String, player_name: String, reason: String, duration_hours: int) -> void:
|
||||
status_label.text = "Banning player..."
|
||||
|
||||
var result := await _rpc_call("admin_ban_player", {
|
||||
"user_id": user_id,
|
||||
"reason": reason if reason else "No reason provided",
|
||||
"duration_hours": duration_hours,
|
||||
"match_id": NakamaManager.current_match_id
|
||||
})
|
||||
|
||||
if result.get("success", false):
|
||||
status_label.text = "Banned: " + player_name
|
||||
status_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
emit_signal("player_banned", user_id)
|
||||
await get_tree().create_timer(0.5).timeout
|
||||
await _refresh_all_data()
|
||||
else:
|
||||
status_label.text = "Failed to ban: " + result.get("error", "Unknown error")
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
|
||||
func _on_mute_pressed() -> void:
|
||||
var selected := player_list.get_selected_items()
|
||||
if selected.is_empty():
|
||||
return
|
||||
|
||||
var meta: Dictionary = player_list.get_item_metadata(selected[0])
|
||||
var player_name: String = meta.get("name", "Unknown")
|
||||
|
||||
# TODO: Implement mute via chat system RPC
|
||||
status_label.text = "Muted: " + player_name
|
||||
|
||||
# =============================================================================
|
||||
# Server Controls
|
||||
# =============================================================================
|
||||
|
||||
func _on_end_match_pressed() -> void:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.dialog_text = "Are you sure you want to end this match?\nAll players will be returned to the lobby."
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
|
||||
dialog.confirmed.connect(func():
|
||||
status_label.text = "Ending match..."
|
||||
|
||||
var result := await _rpc_call("admin_end_match", {
|
||||
"match_id": NakamaManager.current_match_id,
|
||||
"reason": "Ended by " + ("admin" if is_admin else "host")
|
||||
})
|
||||
|
||||
if result.get("success", false):
|
||||
status_label.text = "Match ended"
|
||||
else:
|
||||
status_label.text = "Failed to end match"
|
||||
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.canceled.connect(func(): dialog.queue_free())
|
||||
|
||||
func _on_restart_match_pressed() -> void:
|
||||
var dialog := ConfirmationDialog.new()
|
||||
dialog.dialog_text = "Restart the current match?"
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
|
||||
dialog.confirmed.connect(func():
|
||||
# For restart, we'll use a different approach - reload scene locally
|
||||
# The actual match restart logic would depend on your game
|
||||
get_tree().reload_current_scene()
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
dialog.canceled.connect(func(): dialog.queue_free())
|
||||
|
||||
# =============================================================================
|
||||
# Ban List Management
|
||||
# =============================================================================
|
||||
|
||||
func _on_unban_pressed() -> void:
|
||||
var selected := ban_list.get_selected_items()
|
||||
if selected.is_empty():
|
||||
status_label.text = "Select a player to unban"
|
||||
return
|
||||
|
||||
var meta: Dictionary = ban_list.get_item_metadata(selected[0])
|
||||
var user_id: String = meta.get("user_id", "")
|
||||
var username: String = meta.get("username", "Unknown")
|
||||
|
||||
if user_id.is_empty():
|
||||
status_label.text = "Invalid ban entry"
|
||||
return
|
||||
|
||||
status_label.text = "Unbanning..."
|
||||
|
||||
var result := await _rpc_call("admin_unban_player", {
|
||||
"user_id": user_id
|
||||
})
|
||||
|
||||
if result.get("success", false):
|
||||
status_label.text = "Unbanned: " + username
|
||||
status_label.add_theme_color_override("font_color", Color.GREEN)
|
||||
await _refresh_ban_list()
|
||||
else:
|
||||
status_label.text = "Failed to unban"
|
||||
status_label.add_theme_color_override("font_color", Color.RED)
|
||||
|
||||
# =============================================================================
|
||||
# Input Handling
|
||||
# =============================================================================
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Toggle admin panel with F10
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_F10:
|
||||
if visible:
|
||||
_on_close_pressed()
|
||||
else:
|
||||
show_panel()
|
||||
@@ -0,0 +1,340 @@
|
||||
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 register_button := %RegisterButton as Button
|
||||
@onready var back_to_login_link := %BackToLoginLink as LinkButton
|
||||
@onready var reg_status_label := %RegStatusLabel as Label
|
||||
|
||||
# Main panel reference
|
||||
@onready var main_panel := $CenterContainer/MainPanel as PanelContainer
|
||||
|
||||
var is_loading: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
_connect_signals()
|
||||
_setup_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 = ""
|
||||
reg_email_input.grab_focus()
|
||||
|
||||
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)
|
||||
|
||||
# =============================================================================
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
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 := 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 mini(int(strength), 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:
|
||||
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:
|
||||
_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
|
||||
|
||||
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
|
||||
get_tree().change_scene_to_file("res://assets/models/meshes/lobby.tscn")
|
||||
@@ -0,0 +1,194 @@
|
||||
extends PanelContainer
|
||||
## Profile panel controller - displays and edits user profile
|
||||
|
||||
signal closed
|
||||
signal profile_updated
|
||||
|
||||
@onready var close_button := %CloseButton as Button
|
||||
@onready var avatar_display := %AvatarDisplay as TextureRect
|
||||
@onready var change_avatar_btn := %ChangeAvatarBtn as Button
|
||||
@onready var display_name_input := %DisplayNameInput as LineEdit
|
||||
@onready var save_name_btn := %SaveNameBtn as Button
|
||||
@onready var games_played_label := %GamesPlayed as Label
|
||||
@onready var win_rate_label := %WinRate as Label
|
||||
@onready var high_score_label := %HighScore as Label
|
||||
@onready var account_type_label := %AccountType as Label
|
||||
@onready var link_account_btn := %LinkAccountBtn as Button
|
||||
@onready var logout_btn := %LogoutBtn as Button
|
||||
@onready var status_label := %StatusLabel as Label
|
||||
@onready var avatar_popup := %AvatarSelectionPopup as PopupPanel
|
||||
@onready var avatar_grid := %GridContainer as GridContainer
|
||||
|
||||
func _ready() -> void:
|
||||
_connect_signals()
|
||||
_load_profile_data()
|
||||
_setup_avatar_grid()
|
||||
|
||||
func _connect_signals() -> void:
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
change_avatar_btn.pressed.connect(_on_change_avatar_pressed)
|
||||
save_name_btn.pressed.connect(_on_save_name_pressed)
|
||||
link_account_btn.pressed.connect(_on_link_account_pressed)
|
||||
logout_btn.pressed.connect(_on_logout_pressed)
|
||||
|
||||
UserProfileManager.profile_updated.connect(_on_profile_updated)
|
||||
UserProfileManager.profile_update_failed.connect(_on_profile_update_failed)
|
||||
|
||||
func _load_profile_data() -> void:
|
||||
var profile := UserProfileManager.profile
|
||||
var stats := UserProfileManager.stats
|
||||
|
||||
# Display name
|
||||
display_name_input.text = profile.get("display_name", "Guest")
|
||||
|
||||
# Avatar
|
||||
var avatar_url: String = UserProfileManager.get_avatar_url()
|
||||
if ResourceLoader.exists(avatar_url):
|
||||
avatar_display.texture = load(avatar_url)
|
||||
|
||||
# Stats
|
||||
games_played_label.text = "Games Played: %d" % stats.get("games_played", 0)
|
||||
win_rate_label.text = "Win Rate: %.1f%%" % UserProfileManager.get_win_rate()
|
||||
high_score_label.text = "High Score: %d" % stats.get("high_score", 0)
|
||||
|
||||
# Account type
|
||||
if AuthManager.is_guest:
|
||||
account_type_label.text = "Account: Guest"
|
||||
link_account_btn.visible = true
|
||||
link_account_btn.text = "Link Email (Keep Progress)"
|
||||
else:
|
||||
var mode_name := _get_auth_mode_name(AuthManager.auth_mode)
|
||||
account_type_label.text = "Account: %s" % mode_name
|
||||
link_account_btn.visible = false
|
||||
|
||||
status_label.text = ""
|
||||
|
||||
func _get_auth_mode_name(mode: int) -> String:
|
||||
match mode:
|
||||
AuthManager.AuthMode.EMAIL:
|
||||
return "Email"
|
||||
AuthManager.AuthMode.GOOGLE:
|
||||
return "Google"
|
||||
AuthManager.AuthMode.APPLE:
|
||||
return "Apple"
|
||||
AuthManager.AuthMode.FACEBOOK:
|
||||
return "Facebook"
|
||||
_:
|
||||
return "Guest"
|
||||
|
||||
func _setup_avatar_grid() -> void:
|
||||
# Clear existing
|
||||
for child in avatar_grid.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Add avatar buttons
|
||||
for i in range(UserProfileManager.AVATARS.size()):
|
||||
var avatar_path: String = UserProfileManager.AVATARS[i]
|
||||
var btn := Button.new()
|
||||
btn.custom_minimum_size = Vector2(64, 64)
|
||||
|
||||
if ResourceLoader.exists(avatar_path):
|
||||
var tex := load(avatar_path) as Texture2D
|
||||
btn.icon = tex
|
||||
btn.expand_icon = true
|
||||
else:
|
||||
btn.text = str(i + 1)
|
||||
|
||||
btn.pressed.connect(_on_avatar_selected.bind(i))
|
||||
avatar_grid.add_child(btn)
|
||||
|
||||
func _on_close_pressed() -> void:
|
||||
hide()
|
||||
emit_signal("closed")
|
||||
|
||||
func _on_change_avatar_pressed() -> void:
|
||||
avatar_popup.popup_centered()
|
||||
|
||||
func _on_avatar_selected(index: int) -> void:
|
||||
avatar_popup.hide()
|
||||
status_label.text = "Saving avatar..."
|
||||
|
||||
var success := await UserProfileManager.update_avatar(index)
|
||||
if success:
|
||||
var avatar_url: String = UserProfileManager.get_avatar_url()
|
||||
if ResourceLoader.exists(avatar_url):
|
||||
avatar_display.texture = load(avatar_url)
|
||||
status_label.text = "Avatar updated!"
|
||||
else:
|
||||
status_label.text = "Failed to update avatar"
|
||||
|
||||
func _on_save_name_pressed() -> void:
|
||||
var new_name := display_name_input.text.strip_edges()
|
||||
|
||||
if new_name.is_empty():
|
||||
status_label.text = "Name cannot be empty"
|
||||
return
|
||||
|
||||
status_label.text = "Saving..."
|
||||
save_name_btn.disabled = true
|
||||
|
||||
var success := await UserProfileManager.update_display_name(new_name)
|
||||
|
||||
save_name_btn.disabled = false
|
||||
|
||||
if success:
|
||||
status_label.text = "Name updated!"
|
||||
emit_signal("profile_updated")
|
||||
else:
|
||||
status_label.text = "Failed to update name"
|
||||
|
||||
func _on_link_account_pressed() -> void:
|
||||
# Show link account dialog
|
||||
# For now, just show a simple popup
|
||||
var dialog := AcceptDialog.new()
|
||||
dialog.title = "Link Email"
|
||||
dialog.dialog_text = "Enter your email and password to link this guest account.\nYour progress will be preserved!"
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
var email_input := LineEdit.new()
|
||||
email_input.placeholder_text = "Email"
|
||||
var password_input := LineEdit.new()
|
||||
password_input.placeholder_text = "Password"
|
||||
password_input.secret = true
|
||||
|
||||
vbox.add_child(email_input)
|
||||
vbox.add_child(password_input)
|
||||
dialog.add_child(vbox)
|
||||
|
||||
add_child(dialog)
|
||||
dialog.popup_centered()
|
||||
|
||||
dialog.confirmed.connect(func():
|
||||
var email := email_input.text.strip_edges()
|
||||
var password := password_input.text
|
||||
|
||||
if email.is_empty() or password.is_empty():
|
||||
status_label.text = "Please fill in all fields"
|
||||
return
|
||||
|
||||
status_label.text = "Linking account..."
|
||||
var success := await AuthManager.link_email(email, password)
|
||||
|
||||
if success:
|
||||
status_label.text = "Account linked successfully!"
|
||||
link_account_btn.visible = false
|
||||
account_type_label.text = "Account: Email"
|
||||
else:
|
||||
status_label.text = "Failed to link account"
|
||||
|
||||
dialog.queue_free()
|
||||
)
|
||||
|
||||
func _on_logout_pressed() -> void:
|
||||
AuthManager.logout()
|
||||
get_tree().change_scene_to_file("res://scenes/ui/login_screen.tscn")
|
||||
|
||||
func _on_profile_updated() -> void:
|
||||
_load_profile_data()
|
||||
|
||||
func _on_profile_update_failed(error: String) -> void:
|
||||
status_label.text = error
|
||||
|
||||
func show_panel() -> void:
|
||||
_load_profile_data()
|
||||
show()
|
||||
Reference in New Issue
Block a user