using UnityEngine;
using System.Collections.Generic;
using Unheimlich.Features.Dialogue.Data;
using Unheimlich.Features.Player;
using Unheimlich.Core.Utils;
namespace Unheimlich.Features.Dialogue.UI
{
public static class FloatingWordsSpawner
{
private const float ROTATOR_SCALE = 0.01f;
///
/// Internal configuration struct to unify settings from different sources.
///
public struct SpawnConfig
{
public float SpawnRadius;
public float SpawnHeightOffset;
public RotationAxis RotationAxis;
public bool EnableOrganicShake;
public float ForwardOffset;
public float TiltAngle;
public float RotationSpeed;
public bool EnableRotation;
public ChoiceSpawnMode SpawnMode;
public bool EnableFloating;
public float MergeDuration;
public float MergeScalePulse;
public bool EnableGhostTrail;
public float GhostTrailAlpha;
public float GhostTrailDelay;
public float ShakeRadius;
public float ShakeSpeed;
public float ShakeSmoothness;
public bool MoveTowardPlayerOnFocus;
public float FocusDistanceFromPlayer;
public bool DisableFloatingEffects;
public float DefaultScaleMultiplier;
public bool EnableScaleVariation;
public float MinScaleVariation;
public float MaxScaleVariation;
}
private static SpawnConfig GetConfig(ObjectDescriptionSettings settings)
{
return new SpawnConfig
{
SpawnRadius = settings.SpawnRadius,
SpawnHeightOffset = settings.SpawnHeightOffset,
RotationAxis = settings.RotationAxis,
EnableOrganicShake = settings.EnableOrganicShake,
ForwardOffset = settings.ForwardOffset,
TiltAngle = settings.TiltAngle,
RotationSpeed = settings.RotationSpeed,
EnableRotation = settings.EnableRotation,
SpawnMode = settings.SpawnMode,
EnableFloating = settings.EnableFloating,
MergeDuration = settings.MergeDuration,
MergeScalePulse = settings.MergeScalePulse,
EnableGhostTrail = settings.EnableGhostTrail,
GhostTrailAlpha = settings.GhostTrailAlpha,
GhostTrailDelay = settings.GhostTrailDelay,
ShakeRadius = settings.ShakeRadius,
ShakeSpeed = settings.ShakeSpeed,
ShakeSmoothness = settings.ShakeSmoothness,
MoveTowardPlayerOnFocus = false,
FocusDistanceFromPlayer = 1f,
DisableFloatingEffects = true,
DefaultScaleMultiplier = 1f
};
}
private static SpawnConfig GetConfig(DialogueAnimationSettings settings)
{
return new SpawnConfig
{
SpawnRadius = settings.SpawnRadius,
SpawnHeightOffset = settings.SpawnHeightOffset,
RotationAxis = settings.RotationAxis,
EnableOrganicShake = settings.EnableOrganicShake,
ForwardOffset = 0f,
TiltAngle = 0f,
RotationSpeed = settings.NormalRotationSpeed,
EnableRotation = settings.EnableRotation,
SpawnMode = settings.SpawnMode,
EnableFloating = settings.EnableFloating,
MergeDuration = 0.3f,
MergeScalePulse = 1.2f,
EnableGhostTrail = settings.EnableGhostTrail,
GhostTrailAlpha = settings.GhostTrailAlpha,
GhostTrailDelay = settings.GhostTrailDelay,
ShakeRadius = settings.ShakeRadius,
ShakeSpeed = settings.ShakeSpeed,
ShakeSmoothness = settings.ShakeSmoothness,
MoveTowardPlayerOnFocus = settings.MoveTowardPlayerOnFocus,
FocusDistanceFromPlayer = settings.FocusDistanceFromPlayer,
DisableFloatingEffects = false,
DefaultScaleMultiplier = settings.DefaultScaleMultiplier,
EnableScaleVariation = settings.EnableScaleVariation,
MinScaleVariation = settings.MinScaleVariation,
MaxScaleVariation = settings.MaxScaleVariation
};
}
public static List SpawnByMode(List nodeChoices, ChoiceFocusHandler prefab, Transform container, DialogueAnimationSettings settings, out Vector3[] originalPositions)
{
return SpawnByMode(nodeChoices, prefab, container, GetConfig(settings), out originalPositions);
}
public static List SpawnByMode(List choices, ChoiceFocusHandler prefab, Transform container, ObjectDescriptionSettings settings, out Vector3[] originalPositions)
{
return SpawnByMode(choices, prefab, container, GetConfig(settings), out originalPositions);
}
private static List SpawnByMode(List nodeChoices, ChoiceFocusHandler prefab, Transform container, SpawnConfig config, out Vector3[] originalPositions)
{
switch (config.SpawnMode)
{
case ChoiceSpawnMode.AroundAnchor:
return SpawnWordsInCircle(nodeChoices, prefab, container, config, out originalPositions);
case ChoiceSpawnMode.AtAnchorPosition:
originalPositions = new Vector3[0];
return SpawnAtAnchor(nodeChoices, prefab, container, config);
case ChoiceSpawnMode.InFrontOfPlayer:
originalPositions = new Vector3[0];
return SpawnInFrontOfPlayer(nodeChoices, prefab, container, config);
case ChoiceSpawnMode.CircleAroundPlayer:
return SpawnAroundPlayer(nodeChoices, prefab, container, config, out originalPositions);
default:
return SpawnWordsInCircle(nodeChoices, prefab, container, config, out originalPositions);
}
}
private static List SpawnWordsInCircle(List choices, ChoiceFocusHandler wordPrefab, Transform parent, SpawnConfig config, out Vector3[] originalPositions)
{
List words = new List();
originalPositions = new Vector3[choices.Count];
float angleStep = 360f / choices.Count;
for (int i = 0; i < choices.Count; i++)
{
float angle = i * angleStep * Mathf.Deg2Rad;
Vector3 localOffset = CalculateCircularPosition(angle, config.SpawnRadius, config.RotationAxis, config.SpawnHeightOffset);
originalPositions[i] = localOffset;
ChoiceFocusHandler word = Object.Instantiate(wordPrefab, parent);
word.transform.localPosition = localOffset;
word.transform.localRotation = Quaternion.identity;
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
CanvasGroup canvasGroup = word.GetComponent();
if (canvasGroup == null)
canvasGroup = word.gameObject.AddComponent();
canvasGroup.alpha = 1f;
word.Setup(choices[i], config, config.DisableFloatingEffects);
if (config.EnableOrganicShake) word.StartOrganicShake();
words.Add(word);
}
return words;
}
private static List SpawnAtAnchor(List choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
{
List words = new List();
for (int i = 0; i < choices.Count; i++)
{
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
word.transform.localPosition = Vector3.up * i * 50f;
word.transform.localRotation = Quaternion.identity;
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
word.Setup(choices[i], config);
words.Add(word);
}
return words;
}
private static List SpawnInFrontOfPlayer(List choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
{
List words = new List();
if (PlayerController.Instance == null) return words;
Vector3 playerForward = PlayerController.Instance.transform.forward;
for (int i = 0; i < choices.Count; i++)
{
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
Vector3 offset = playerForward * 2f + Vector3.up * (i - choices.Count / 2f) * (config.SpawnRadius / choices.Count / 100f);
word.transform.position = PlayerController.Instance.transform.position + offset;
word.transform.LookAt(PlayerController.Instance.transform);
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
word.Setup(choices[i], config);
words.Add(word);
}
return words;
}
private static List SpawnAroundPlayer(List choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config, out Vector3[] originalPositions)
{
List words = new List();
originalPositions = new Vector3[choices.Count];
if (PlayerController.Instance == null) return words;
parent.position = PlayerController.Instance.transform.position;
parent.rotation = Quaternion.identity;
float angleStep = 360f / choices.Count;
for (int i = 0; i < choices.Count; i++)
{
float angle = i * angleStep * Mathf.Deg2Rad;
Vector3 localOffset = new Vector3(Mathf.Cos(angle) * config.SpawnRadius, config.SpawnHeightOffset, Mathf.Sin(angle) * config.SpawnRadius);
originalPositions[i] = localOffset;
ChoiceFocusHandler word = Object.Instantiate(prefab, parent);
word.transform.localPosition = localOffset;
word.transform.LookAt(PlayerController.Instance.transform);
float scaleMultiplier = config.DefaultScaleMultiplier;
if (config.EnableScaleVariation)
{
scaleMultiplier *= Random.Range(config.MinScaleVariation, config.MaxScaleVariation);
}
word.transform.localScale = Vector3.one * scaleMultiplier;
word.Setup(choices[i], config);
words.Add(word);
}
return words;
}
public static void MergeWords(List words, ObjectDescriptionSettings settings, System.Action onMergeComplete = null)
{
MergeWords(words, GetConfig(settings), onMergeComplete);
}
private static void MergeWords(List words, SpawnConfig config, System.Action onMergeComplete)
{
for (int i = 0; i < words.Count; i++)
{
if (words[i] != null)
{
words[i].StopOrganicShake();
words[i].HideGhostTrail();
}
}
for (int i = 1; i < words.Count; i++)
{
if (words[i] != null)
{
CanvasGroup canvasGroup = words[i].GetComponent();
if (canvasGroup != null)
{
TweenHelper.FadeCanvasGroup(canvasGroup, 0f, config.MergeDuration, null, LeanTweenType.easeInQuad);
}
}
}
if (words.Count > 0 && words[0] != null)
{
TweenHelper.ScaleLocal(
words[0].gameObject,
Vector3.one * config.MergeScalePulse,
config.MergeDuration / 2f,
() =>
{
if (words.Count > 0 && words[0] != null)
{
TweenHelper.ScaleLocal(
words[0].gameObject,
Vector3.one,
config.MergeDuration / 2f,
() => onMergeComplete.SafeInvoke(),
LeanTweenType.easeInBack
);
}
},
LeanTweenType.easeOutBack
);
}
}
public static void UnmergeWords(List words, ObjectDescriptionSettings settings)
{
UnmergeWords(words, GetConfig(settings));
}
private static void UnmergeWords(List words, SpawnConfig config)
{
for (int i = 0; i < words.Count; i++)
{
if (words[i] != null)
{
words[i].StartOrganicShake();
words[i].ShowGhostTrail();
}
}
for (int i = 1; i < words.Count; i++)
{
if (words[i] != null)
{
words[i].gameObject.CancelAllTweens();
CanvasGroup canvasGroup = words[i].GetComponent();
if (canvasGroup != null)
{
TweenHelper.FadeCanvasGroup(canvasGroup, 1f, config.MergeDuration, null, LeanTweenType.easeOutQuad);
}
words[i].transform.localScale = Vector3.one;
}
}
if (words.Count > 0 && words[0] != null)
{
words[0].gameObject.CancelAllTweens();
words[0].transform.localScale = Vector3.one;
}
}
#region For obj description
public static GameObject CreateWordsContainer(string containerName, Transform anchor, ObjectDescriptionSettings settings)
{
SpawnConfig config = GetConfig(settings);
GameObject rotator = new GameObject($"{containerName}_Rotator");
rotator.transform.localPosition = Vector3.forward * config.ForwardOffset;
rotator.transform.localRotation = Quaternion.Euler(config.TiltAngle, 0f, 0f);
rotator.transform.localScale = Vector3.one * ROTATOR_SCALE;
Canvas containerCanvas = rotator.AddComponent