using UnityEngine; using System.Collections.Generic; using TMPro; using Unheimlich.Data.Dialogues; using Unheimlich.Gameplay.Player; using Unheimlich.Interactions.FloatingWords; using Unheimlich.Utility; 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 int _fadeCanvasTweenId = -1; private int _rotationTweenId = -1; private List _floatingTweenIds = new List(); 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) { TweenHelper.CancelTween(ref _fadeCanvasTweenId); _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) { TweenHelper.CancelTween(ref _fadeCanvasTweenId); _fadeCanvasTweenId = TweenHelper.FadeCanvasGroup( _dialogueCanvasGroup, 0f, _fadeOutDuration, () => { 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 delay = index * 0.2f; Vector3 startPos = choice.transform.localPosition; int tweenId = TweenHelper.FloatAnimation(choice, startPos, amplitude, duration, delay, Axis.Y); _floatingTweenIds.Add(tweenId); } public void RotationAnimation(float degreesPerSecond = 60f) { TweenHelper.CancelTween(ref _rotationTweenId); _rotationTweenId = TweenHelper.RotateAround(ChoiceContainer, _currentRotationAxis, degreesPerSecond); } /// /// 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(); TweenHelper.CancelTween(ref _rotationTweenId); foreach (int tweenId in _floatingTweenIds) { int id = tweenId; TweenHelper.CancelTween(ref id); } _floatingTweenIds.Clear(); foreach (Transform child in ChoiceContainer.transform) { 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); } } } }