Files
Emberwild/Assets/GAME/Script/UI/InviteNotificationUI.cs
T
2026-06-22 16:18:34 +02:00

233 lines
7.7 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using DG.Tweening;
using Ashwild.Network;
using Ashwild.Player;
namespace Ashwild.UI
{
/// <summary>
/// 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.
/// </summary>
[DisallowMultipleComponent]
public class InviteNotificationUI : MonoBehaviour
{
#region Types
/// <summary>
/// One pending invitation: who sent it and the code needed to join their room.
/// </summary>
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<Invite> queue = new Queue<Invite>();
private Invite current;
private bool isShowing;
private Tween slideTween;
/// <summary>
/// Texture backing the currently displayed Steam avatar — destroyed when replaced to
/// avoid leaking a Texture2D per invitation.
/// </summary>
private Texture2D currentAvatarTexture;
#endregion
#region Unity Lifecycle
/// <summary>
/// Wires the buttons and parks the card off-screen; runs once.
/// </summary>
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);
}
}
/// <summary>
/// Subscribes to incoming invitations.
/// </summary>
private void OnEnable()
{
PlayerEvents.InviteReceived += HandleInviteReceived;
}
/// <summary>
/// Unsubscribes — mirrors OnEnable exactly.
/// </summary>
private void OnDisable()
{
PlayerEvents.InviteReceived -= HandleInviteReceived;
}
/// <summary>
/// Kills the tween and frees the avatar texture on teardown.
/// </summary>
private void OnDestroy()
{
slideTween?.Kill();
ReleaseAvatarTexture();
}
#endregion
#region Event Handlers
/// <summary>
/// Queues a new invitation and shows it immediately if the card is idle.
/// </summary>
private void HandleInviteReceived(ulong steamId, string inviterName, string code)
{
queue.Enqueue(new Invite(steamId, inviterName, code));
if (!isShowing) ShowNext();
}
/// <summary>
/// Join the inviter's room, then slide the card away (the scene load takes over on success).
/// </summary>
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();
}
/// <summary>
/// Declines the current invitation and moves on to the next queued one.
/// </summary>
private void HandleCancelClicked() => Dismiss();
#endregion
#region Internal Helpers
/// <summary>
/// Pops the next invitation off the queue and slides the card in; idles when empty.
/// </summary>
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);
}
/// <summary>
/// Slides the card out and shows the next queued invitation when done.
/// </summary>
private void Dismiss()
{
slideTween?.Kill();
slideTween = card.DOAnchorPosX(hiddenX, slideOutDuration)
.SetEase(slideOutEase)
.OnComplete(() =>
{
card.gameObject.SetActive(false);
ShowNext();
});
}
/// <summary>
/// Swaps the avatar sprite, releasing any previous Steam-built texture first.
/// </summary>
private void SetAvatar(Sprite sprite, Texture2D ownedTexture)
{
ReleaseAvatarTexture();
currentAvatarTexture = ownedTexture;
if (avatarImage != null) avatarImage.sprite = sprite;
}
/// <summary>
/// Destroys the previously shown Steam avatar texture, if we created one.
/// </summary>
private void ReleaseAvatarTexture()
{
if (currentAvatarTexture != null)
{
Destroy(currentAvatarTexture);
currentAvatarTexture = null;
}
}
#endregion
}
}