439 lines
18 KiB
C#
439 lines
18 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Internal configuration struct to unify settings from different sources.
|
|
/// </summary>
|
|
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<ChoiceFocusHandler> SpawnByMode(List<DialogueChoiceData> nodeChoices, ChoiceFocusHandler prefab, Transform container, DialogueAnimationSettings settings, out Vector3[] originalPositions)
|
|
{
|
|
return SpawnByMode(nodeChoices, prefab, container, GetConfig(settings), out originalPositions);
|
|
}
|
|
|
|
public static List<ChoiceFocusHandler> SpawnByMode(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform container, ObjectDescriptionSettings settings, out Vector3[] originalPositions)
|
|
{
|
|
return SpawnByMode(choices, prefab, container, GetConfig(settings), out originalPositions);
|
|
}
|
|
|
|
private static List<ChoiceFocusHandler> SpawnByMode(List<DialogueChoiceData> 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<ChoiceFocusHandler> SpawnWordsInCircle(List<DialogueChoiceData> choices, ChoiceFocusHandler wordPrefab, Transform parent, SpawnConfig config, out Vector3[] originalPositions)
|
|
{
|
|
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
|
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<CanvasGroup>();
|
|
if (canvasGroup == null)
|
|
canvasGroup = word.gameObject.AddComponent<CanvasGroup>();
|
|
|
|
canvasGroup.alpha = 1f;
|
|
|
|
word.Setup(choices[i], config, config.DisableFloatingEffects);
|
|
|
|
if (config.EnableOrganicShake) word.StartOrganicShake();
|
|
|
|
words.Add(word);
|
|
}
|
|
|
|
return words;
|
|
}
|
|
|
|
private static List<ChoiceFocusHandler> SpawnAtAnchor(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
|
|
{
|
|
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
|
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<ChoiceFocusHandler> SpawnInFrontOfPlayer(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config)
|
|
{
|
|
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
|
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<ChoiceFocusHandler> SpawnAroundPlayer(List<DialogueChoiceData> choices, ChoiceFocusHandler prefab, Transform parent, SpawnConfig config, out Vector3[] originalPositions)
|
|
{
|
|
List<ChoiceFocusHandler> words = new List<ChoiceFocusHandler>();
|
|
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<ChoiceFocusHandler> words, ObjectDescriptionSettings settings, System.Action onMergeComplete = null)
|
|
{
|
|
MergeWords(words, GetConfig(settings), onMergeComplete);
|
|
}
|
|
|
|
private static void MergeWords(List<ChoiceFocusHandler> 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<CanvasGroup>();
|
|
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<ChoiceFocusHandler> words, ObjectDescriptionSettings settings)
|
|
{
|
|
UnmergeWords(words, GetConfig(settings));
|
|
}
|
|
|
|
private static void UnmergeWords(List<ChoiceFocusHandler> 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<CanvasGroup>();
|
|
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<Canvas>();
|
|
containerCanvas.renderMode = RenderMode.WorldSpace;
|
|
|
|
return rotator;
|
|
}
|
|
|
|
|
|
public static void UpdateContainerRotation(GameObject container, GameObject rotator, Transform playerTransform, ObjectDescriptionSettings settings)
|
|
{
|
|
if (container == null || playerTransform == null) return;
|
|
|
|
Vector3 lookAtPosition = playerTransform.position;
|
|
lookAtPosition.y = container.transform.position.y;
|
|
container.transform.LookAt(lookAtPosition);
|
|
|
|
if (!settings.EnableRotation || rotator == null) return;
|
|
|
|
Vector3 rotationAxis = GetRotationAxisVector(settings.RotationAxis);
|
|
float rotationAmount = (360f / settings.RotationSpeed) * Time.deltaTime;
|
|
rotator.transform.Rotate(rotationAxis, rotationAmount, Space.Self);
|
|
}
|
|
#endregion
|
|
|
|
#region Depth Sorting
|
|
/// <summary>
|
|
/// Sorts words by distance to camera so closer words render on top via sibling index.
|
|
/// Ghost trails are synced to always sit just below their main word.
|
|
/// Shared by ObjectDescriptionTrigger and DialogueUI to avoid duplication.
|
|
/// Sorts a temporary copy to preserve the original list order, which may be index-coupled
|
|
/// to spawn positions (e.g. _originalSpawnPositions in ObjectDescriptionTrigger).
|
|
/// </summary>
|
|
public static void SortByDistance(List<ChoiceFocusHandler> words, Camera camera)
|
|
{
|
|
if (words.Count <= 1 || camera == null) return;
|
|
|
|
Vector3 cameraPosition = camera.transform.position;
|
|
|
|
List<ChoiceFocusHandler> sorted = new List<ChoiceFocusHandler>(words);
|
|
sorted.Sort((a, b) =>
|
|
{
|
|
if (a == null || b == null) return 0;
|
|
float dA = Vector3.Distance(cameraPosition, a.transform.position);
|
|
float dB = Vector3.Distance(cameraPosition, b.transform.position);
|
|
return dA.CompareTo(dB);
|
|
});
|
|
|
|
for (int i = 0; i < sorted.Count; i++)
|
|
{
|
|
if (sorted[i] != null)
|
|
{
|
|
sorted[i].transform.SetSiblingIndex(i);
|
|
sorted[i].SyncGhostSiblingIndex(i);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Tools
|
|
private static Vector3 CalculateCircularPosition(float angleRad, float radius, RotationAxis axis, float heightOffset)
|
|
{
|
|
switch (axis)
|
|
{
|
|
case RotationAxis.Forward:
|
|
return new Vector3(Mathf.Cos(angleRad) * radius, Mathf.Sin(angleRad) * radius + heightOffset, 0f);
|
|
case RotationAxis.Up:
|
|
return new Vector3(Mathf.Cos(angleRad) * radius, heightOffset, Mathf.Sin(angleRad) * radius);
|
|
case RotationAxis.Right:
|
|
return new Vector3(0f, Mathf.Cos(angleRad) * radius + heightOffset, Mathf.Sin(angleRad) * radius);
|
|
default:
|
|
return new Vector3(Mathf.Cos(angleRad) * radius, heightOffset, Mathf.Sin(angleRad) * radius);
|
|
}
|
|
}
|
|
|
|
private static 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.up;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
} |