using UnityEngine;
using System.Collections.Generic;
using TMPro;
using Unheimlich.Data.Dialogues;
using Unheimlich.Gameplay.Player;
using Unheimlich.Interactions.FloatingWords;
namespace Unheimlich.UI
{
///
/// Manages dialogue UI display and choice button spawning with configurable animations.
/// Supports multiple spawn modes, distance scaling, player following, and visual effects.
///
public class DialogueUI : MonoBehaviour
{
[Header("Panels - World Mode")]
[SerializeField] private GameObject _dialoguePanel;
public GameObject ChoiceContainer;
[Header("Text Fields - World Mode")]
[SerializeField] private TextMeshProUGUI _speakerText;
[SerializeField] private TextMeshProUGUI _contentText;
[Header("Prefabs")]
public ChoiceFocusHandler ChoiceButtonPrefab;
[Header("Default Animation Settings")]
[SerializeField] private float _defaultRotationSpeed = 36f;
[Header("Fade Settings")]
[SerializeField] private float _fadeOutDuration = 0.5f;
[SerializeField] private CanvasGroup _dialogueCanvasGroup;
private DialogueAnimationSettings _currentAnimationSettings;
private Vector3 _currentRotationAxis = Vector3.forward;
private bool _shouldFollowPlayerPosition = false;
private bool _shouldFollowPlayerRotation = false;
private Transform _currentAnchor;
private Vector3 _baseScale;
private List _activeChoices = new List();
private float _sortTimer = 0f;
private const float SORT_INTERVAL = 0.1f;
private DialogueDisplayMode _currentDisplayMode = DialogueDisplayMode.World;
private static readonly Vector3 ViewportCenter = new Vector3(0.5f, 0.5f, 0f);
private void Awake()
{
if (_dialogueCanvasGroup == null && _dialoguePanel != null)
{
_dialogueCanvasGroup = _dialoguePanel.GetComponent();
if (_dialogueCanvasGroup == null)
_dialogueCanvasGroup = _dialoguePanel.AddComponent();
}
}
private void Update()
{
if (Player.Instance == null || !ChoiceContainer.activeInHierarchy)
return;
if (_shouldFollowPlayerPosition)
ChoiceContainer.transform.position = Player.Instance.transform.position;
if (_shouldFollowPlayerRotation)
{
float playerYRotation = Player.Instance.transform.eulerAngles.y;
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
}
if (_currentAnimationSettings != null && _currentAnimationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
ApplyDistanceScaling();
SortChoicesByDistance();
}
public void ShowDialogueBox(string speaker, string content, DialogueDisplayMode displayMode = DialogueDisplayMode.World)
{
_currentDisplayMode = displayMode;
if (displayMode == DialogueDisplayMode.World)
{
if (DialogueScreenUI.Instance != null)
DialogueScreenUI.Instance.Hide();
_speakerText.text = speaker;
_contentText.text = content;
_dialoguePanel.SetActive(true);
if (_dialogueCanvasGroup != null)
{
LeanTween.cancel(_dialoguePanel);
_dialogueCanvasGroup.alpha = 1f;
}
}
else
{
if (_dialoguePanel != null)
_dialoguePanel.SetActive(false);
if (DialogueScreenUI.Instance != null)
DialogueScreenUI.Instance.Show(speaker, content);
else
Debug.LogWarning("[DialogueUI] DialogueScreenUI.Instance is null! Make sure DialogueScreenUI component exists in the scene.");
}
}
public void HideDialogueBox()
{
if (_dialoguePanel != null)
_dialoguePanel.SetActive(false);
if (DialogueScreenUI.Instance != null)
DialogueScreenUI.Instance.Hide();
ChoiceContainer.SetActive(false);
HideChoices();
}
public void FadeOutAndHide(System.Action onComplete = null)
{
if (_currentDisplayMode == DialogueDisplayMode.World)
{
if (_dialogueCanvasGroup != null && _dialoguePanel != null)
{
LeanTween.cancel(_dialoguePanel);
LeanTween.alphaCanvas(_dialogueCanvasGroup, 0f, _fadeOutDuration)
.setOnComplete(() =>
{
HideDialogueBox();
onComplete?.Invoke();
});
}
else
{
HideDialogueBox();
onComplete?.Invoke();
}
}
else
{
if (DialogueScreenUI.Instance != null)
{
DialogueScreenUI.Instance.FadeOutAndHide(() =>
{
HideChoices();
ChoiceContainer.SetActive(false);
onComplete?.Invoke();
});
}
else
{
HideDialogueBox();
onComplete?.Invoke();
}
}
}
public bool AreChoicesVisible()
{
return ChoiceContainer.transform.childCount > 0;
}
public void ShowChoices(List nodeChoices, Transform anchorTransform, DialogueAnimationSettings animationSettings = null)
{
HideChoices();
_currentAnimationSettings = animationSettings;
_currentAnchor = anchorTransform;
if (anchorTransform != null)
{
ChoiceContainer.transform.SetParent(anchorTransform);
bool followPosition = animationSettings != null && animationSettings.FollowPlayerPosition;
bool followRotation = animationSettings != null && animationSettings.FollowPlayerRotation;
_shouldFollowPlayerPosition = followPosition;
_shouldFollowPlayerRotation = followRotation;
if (followPosition && Player.Instance != null)
{
Vector3 playerWorldPos = Player.Instance.transform.position;
ChoiceContainer.transform.position = playerWorldPos;
}
else
ChoiceContainer.transform.localPosition = Vector3.zero;
if (followRotation && Player.Instance != null)
{
float playerYRotation = Player.Instance.transform.eulerAngles.y;
ChoiceContainer.transform.rotation = Quaternion.Euler(0f, playerYRotation, 0f);
}
else
ChoiceContainer.transform.localRotation = Quaternion.identity;
}
ChoiceContainer.SetActive(true);
ChoiceContainer.transform.localScale = Vector3.one;
_baseScale = Vector3.one;
int numChoices = nodeChoices.Count;
if (numChoices == 0) return;
SpawnChoices(nodeChoices, animationSettings);
if (animationSettings != null && animationSettings.EnableDistanceScaling && !_shouldFollowPlayerPosition)
ApplyDistanceScaling();
bool shouldRotate = animationSettings == null || animationSettings.EnableRotation;
if (shouldRotate)
{
float rotationSpeed = animationSettings != null ? animationSettings.NormalRotationSpeed : _defaultRotationSpeed;
RotationAnimation(rotationSpeed);
}
}
private void SpawnChoices(List nodeChoices, DialogueAnimationSettings settings)
{
_currentRotationAxis = GetRotationAxisVector(settings != null ? settings.RotationAxis : RotationAxis.Forward);
if (settings == null) return;
Vector3[] originalPositions;
List spawnedChoices = FloatingWordsSpawner.SpawnByMode(
nodeChoices,
ChoiceButtonPrefab,
ChoiceContainer.transform,
settings,
out originalPositions
);
for (int i = 0; i < spawnedChoices.Count; i++)
{
_activeChoices.Add(spawnedChoices[i]);
if (settings.EnableFloating)
StartFloatingAnimation(spawnedChoices[i].gameObject, i);
}
}
private void StartFloatingAnimation(GameObject choice, int index)
{
if (_currentAnimationSettings == null) return;
float amplitude = _currentAnimationSettings.FloatingAmplitude;
float duration = _currentAnimationSettings.FloatingDuration;
float offset = index * 0.2f;
Vector3 startPos = choice.transform.localPosition;
LeanTween.value(choice, 0f, Mathf.PI * 2f, duration)
.setDelay(offset)
.setOnUpdate((float val) => {
if (choice != null)
{
Vector3 newPos = startPos;
newPos.y += Mathf.Sin(val) * amplitude;
choice.transform.localPosition = newPos;
}
})
.setLoopClamp()
.setEase(LeanTweenType.linear);
}
public void RotationAnimation(float degreesPerSecond = 60f)
{
if (degreesPerSecond <= 0f)
return;
float duration = 360f / degreesPerSecond;
LeanTween.cancel(ChoiceContainer);
LeanTween.rotateAroundLocal(ChoiceContainer, _currentRotationAxis, 360f, duration)
.setLoopClamp()
.setEase(LeanTweenType.linear);
}
///
/// Dynamically scales the choice container based on distance from player to anchor.
/// Provides visual feedback for spatial awareness in 3D dialogue interactions.
///
private void ApplyDistanceScaling()
{
if (_currentAnchor == null || Player.Instance == null)
return;
float distance = Vector3.Distance(_currentAnchor.position, Player.Instance.transform.position);
float normalizedDistance = Mathf.InverseLerp(_currentAnimationSettings.MinDistance, _currentAnimationSettings.MaxDistance, distance);
float finalScale = Mathf.Lerp(_currentAnimationSettings.MinScaleMultiplier, _currentAnimationSettings.MaxScaleMultiplier, normalizedDistance);
ChoiceContainer.transform.localScale = _baseScale * finalScale;
}
private Vector3 GetRotationAxisVector(RotationAxis axis)
{
switch (axis)
{
case RotationAxis.Forward:
return Vector3.forward;
case RotationAxis.Up:
return Vector3.up;
case RotationAxis.Right:
return Vector3.right;
default:
return Vector3.forward;
}
}
public void HideChoices()
{
_shouldFollowPlayerPosition = false;
_shouldFollowPlayerRotation = false;
_currentAnchor = null;
_currentAnimationSettings = null;
_activeChoices.Clear();
LeanTween.cancel(ChoiceContainer);
foreach (Transform child in ChoiceContainer.transform)
{
LeanTween.cancel(child.gameObject);
Destroy(child.gameObject);
}
}
///
/// Sorts choice buttons by distance to player camera.
/// Closer choices are rendered on top by being placed last in hierarchy (higher sibling index).
/// Uses interval-based sorting to reduce performance overhead.
///
private void SortChoicesByDistance()
{
if (_activeChoices.Count <= 1 || Player.Instance == null)
return;
_sortTimer += Time.deltaTime;
if (_sortTimer < SORT_INTERVAL)
return;
_sortTimer = 0f;
Camera mainCamera = Camera.main;
if (mainCamera == null)
return;
Vector3 cameraPosition = mainCamera.transform.position;
_activeChoices.Sort((a, b) =>
{
if (a == null || b == null)
return 0;
float distanceA = Vector3.Distance(cameraPosition, a.transform.position);
float distanceB = Vector3.Distance(cameraPosition, b.transform.position);
return distanceB.CompareTo(distanceA);
});
for (int i = 0; i < _activeChoices.Count; i++)
{
if (_activeChoices[i] != null)
_activeChoices[i].transform.SetSiblingIndex(i);
}
}
}
}