using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; using DG.Tweening; using Ashwild.Network; using Ashwild.Player; namespace Ashwild.UI { /// /// The invitation card that slides in from the right when a friend invites the local player /// to their session. Shows the inviter's Steam avatar and name, with Join (connect to their /// room) and Cancel (dismiss) actions. Invites are queued and shown one at a time; each is /// animated entirely with DOTween. Drop this on a card in the menu canvas. /// [DisallowMultipleComponent] public class InviteNotificationUI : MonoBehaviour { #region Types /// /// One pending invitation: who sent it and the code needed to join their room. /// private readonly struct Invite { public readonly ulong SteamId; public readonly string Name; public readonly string Code; public Invite(ulong steamId, string name, string code) { SteamId = steamId; Name = name; Code = code; } } #endregion #region Serialized Fields [Header("References")] [Tooltip("The card RectTransform that slides in/out (anchored to the left edge).")] [SerializeField] private RectTransform card; [SerializeField] private Image avatarImage; [Tooltip("Receives the inviter's display name.")] [SerializeField] private TMP_Text nameText; [SerializeField] private Button joinButton; [SerializeField] private Button cancelButton; [Header("Slide Animation")] [Tooltip("Off-screen anchoredPosition.x (card hidden to the right).")] [SerializeField] private float hiddenX = 560f; [Tooltip("On-screen anchoredPosition.x (card resting position).")] [SerializeField] private float shownX = -24f; [SerializeField] private float slideInDuration = 0.4f; [SerializeField] private Ease slideInEase = Ease.OutBack; [SerializeField] private float slideOutDuration = 0.25f; [SerializeField] private Ease slideOutEase = Ease.InBack; #endregion #region State private readonly Queue queue = new Queue(); private Invite current; private bool isShowing; private Tween slideTween; /// /// Texture backing the currently displayed Steam avatar — destroyed when replaced to /// avoid leaking a Texture2D per invitation. /// private Texture2D currentAvatarTexture; #endregion #region Unity Lifecycle /// /// Wires the buttons and parks the card off-screen; runs once. /// private void Awake() { if (joinButton != null) joinButton.onClick.AddListener(HandleJoinClicked); if (cancelButton != null) cancelButton.onClick.AddListener(HandleCancelClicked); if (card != null) { card.anchoredPosition = new Vector2(hiddenX, card.anchoredPosition.y); card.gameObject.SetActive(false); } } /// /// Subscribes to incoming invitations. /// private void OnEnable() { PlayerEvents.InviteReceived += HandleInviteReceived; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.InviteReceived -= HandleInviteReceived; } /// /// Kills the tween and frees the avatar texture on teardown. /// private void OnDestroy() { slideTween?.Kill(); ReleaseAvatarTexture(); } #endregion #region Event Handlers /// /// Queues a new invitation and shows it immediately if the card is idle. /// private void HandleInviteReceived(ulong steamId, string inviterName, string code) { queue.Enqueue(new Invite(steamId, inviterName, code)); if (!isShowing) ShowNext(); } /// /// Join the inviter's room, then slide the card away (the scene load takes over on success). /// private void HandleJoinClicked() { if (NetworkSessionManager.Instance != null && !string.IsNullOrEmpty(current.Code)) NetworkSessionManager.Instance.JoinSession(current.Code); else Debug.LogError("[InviteNotification] Cannot join — NetworkSessionManager missing or code empty.", this); Dismiss(); } /// /// Declines the current invitation and moves on to the next queued one. /// private void HandleCancelClicked() => Dismiss(); #endregion #region Internal Helpers /// /// Pops the next invitation off the queue and slides the card in; idles when empty. /// private void ShowNext() { if (queue.Count == 0) { isShowing = false; return; } current = queue.Dequeue(); isShowing = true; if (nameText != null) nameText.text = current.Name; // Clear the previous invite's avatar; Steam fills it back in (almost always instantly, // since a friend's picture is normally already cached). SetAvatar(null, null); // Ask Steam for the real avatar; it may arrive now or a moment later. if (SteamInviteService.Instance != null) { ulong requestedId = current.SteamId; SteamInviteService.Instance.RequestAvatar(requestedId, sprite => { // Ignore a late avatar for an invitation we are no longer showing. if (!isShowing || current.SteamId != requestedId || sprite == null) return; SetAvatar(sprite, sprite.texture); }); } slideTween?.Kill(); card.gameObject.SetActive(true); card.anchoredPosition = new Vector2(hiddenX, card.anchoredPosition.y); slideTween = card.DOAnchorPosX(shownX, slideInDuration).SetEase(slideInEase); } /// /// Slides the card out and shows the next queued invitation when done. /// private void Dismiss() { slideTween?.Kill(); slideTween = card.DOAnchorPosX(hiddenX, slideOutDuration) .SetEase(slideOutEase) .OnComplete(() => { card.gameObject.SetActive(false); ShowNext(); }); } /// /// Swaps the avatar sprite, releasing any previous Steam-built texture first. /// private void SetAvatar(Sprite sprite, Texture2D ownedTexture) { ReleaseAvatarTexture(); currentAvatarTexture = ownedTexture; if (avatarImage != null) avatarImage.sprite = sprite; } /// /// Destroys the previously shown Steam avatar texture, if we created one. /// private void ReleaseAvatarTexture() { if (currentAvatarTexture != null) { Destroy(currentAvatarTexture); currentAvatarTexture = null; } } #endregion } }