249 lines
10 KiB
C#
249 lines
10 KiB
C#
using UnityEngine;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Unheimlich.Features.Dialogue.Data;
|
|
using Unheimlich.Features.Dialogue.UI;
|
|
using Unheimlich.Features.Player;
|
|
using Unheimlich.Core.Utils;
|
|
using RotationAxis = Unheimlich.Features.Dialogue.Data.RotationAxis;
|
|
using ChoiceSpawnMode = Unheimlich.Features.Dialogue.Data.ChoiceSpawnMode;
|
|
|
|
namespace Unheimlich.Features.Hallucination
|
|
{
|
|
/// <summary>
|
|
/// Listens to HallucinationManager.OnHallucinationTriggered and spawns ghost words
|
|
/// that float in world space and drift toward the player.
|
|
/// Reuses the existing ChoiceFocusHandler prefab and its organic shake system.
|
|
/// Add this component to the HallucinationManager GameObject or any persistent scene object.
|
|
/// </summary>
|
|
public class HallucinationWordSpawner : MonoBehaviour
|
|
{
|
|
private const float CanvasLocalScale = 0.01f;
|
|
|
|
[Header("References")]
|
|
[Tooltip("Prefab used to render each ghost word (ChoiceFocusHandler)")]
|
|
[SerializeField] private ChoiceFocusHandler _wordPrefab;
|
|
|
|
[Header("Ghost Word Behaviour")]
|
|
[Tooltip("Speed at which the ghost word drifts toward the player")]
|
|
[SerializeField] private float _driftSpeed = 0.4f;
|
|
|
|
[Tooltip("Distance from the player at which the ghost word stops drifting")]
|
|
[SerializeField] private float _stopDistance = 1.5f;
|
|
|
|
[Tooltip("Fade-in duration when a ghost word appears")]
|
|
[SerializeField] private float _fadeInDuration = 0.5f;
|
|
|
|
[Tooltip("Fade-out duration before the ghost word is destroyed")]
|
|
[SerializeField] private float _fadeOutDuration = 0.8f;
|
|
|
|
[Header("Shake Settings")]
|
|
[Tooltip("Shake radius applied to ghost words (world units)")]
|
|
[SerializeField] private float _shakeRadius = 15f;
|
|
|
|
[Tooltip("Shake speed")]
|
|
[SerializeField] private float _shakeSpeed = 2f;
|
|
|
|
[Tooltip("Shake smoothness")]
|
|
[SerializeField] private float _shakeSmoothness = 5f;
|
|
|
|
private readonly List<GhostWordInstance> _activeWords = new List<GhostWordInstance>();
|
|
|
|
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (HallucinationManager.Instance != null)
|
|
HallucinationManager.Instance.OnHallucinationTriggered += OnHallucinationTriggered;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (HallucinationManager.Instance != null)
|
|
HallucinationManager.Instance.OnHallucinationTriggered -= OnHallucinationTriggered;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
for (int i = _activeWords.Count - 1; i >= 0; i--)
|
|
{
|
|
GhostWordInstance instance = _activeWords[i];
|
|
|
|
if (instance.Handler == null)
|
|
{
|
|
_activeWords.RemoveAt(i);
|
|
continue;
|
|
}
|
|
|
|
DriftTowardPlayer(instance);
|
|
}
|
|
}
|
|
|
|
// ─── Event Handler ─────────────────────────────────────────────────────────
|
|
|
|
private void OnHallucinationTriggered(HallucinationEvent hallucinationEvent)
|
|
{
|
|
if (!hallucinationEvent.SpawnGhostWord || _wordPrefab == null)
|
|
return;
|
|
|
|
SpawnGhostWord(hallucinationEvent);
|
|
}
|
|
|
|
// ─── Spawn ─────────────────────────────────────────────────────────────────
|
|
|
|
private void SpawnGhostWord(HallucinationEvent hallucinationEvent)
|
|
{
|
|
if (PlayerController.Instance == null)
|
|
{
|
|
Debug.LogWarning("[HallucinationWordSpawner] PlayerController not found, cannot spawn ghost word.", this);
|
|
return;
|
|
}
|
|
|
|
// World-space canvas container — mirrors the pattern in FloatingWordsSpawner
|
|
GameObject container = new GameObject($"GhostWord_{hallucinationEvent.Label}");
|
|
Canvas canvas = container.AddComponent<Canvas>();
|
|
canvas.renderMode = RenderMode.WorldSpace;
|
|
container.transform.localScale = Vector3.one * CanvasLocalScale;
|
|
|
|
// Position relative to player + event offset
|
|
Vector3 spawnPosition = PlayerController.Instance.transform.position
|
|
+ PlayerController.Instance.transform.TransformDirection(hallucinationEvent.GhostWordSpawnOffset);
|
|
container.transform.position = spawnPosition;
|
|
container.transform.LookAt(PlayerController.Instance.transform.position);
|
|
|
|
// Spawn the word handler
|
|
ChoiceFocusHandler handler = Instantiate(_wordPrefab, container.transform);
|
|
handler.transform.localPosition = Vector3.zero;
|
|
handler.transform.localRotation = Quaternion.identity;
|
|
handler.transform.localScale = Vector3.one;
|
|
|
|
// Build a ghost-only DialogueChoiceData (no NextNode, no localization key)
|
|
DialogueChoiceData ghostData = new DialogueChoiceData
|
|
{
|
|
ChoiceTextOverride = hallucinationEvent.GhostWordText,
|
|
NextNode = null
|
|
};
|
|
|
|
FloatingWordsSpawner.SpawnConfig config = BuildGhostConfig();
|
|
handler.Setup(ghostData, config, disableFloatingEffects: false);
|
|
handler.StartOrganicShake();
|
|
|
|
// Fade-in via CanvasGroup
|
|
CanvasGroup canvasGroup = handler.GetComponent<CanvasGroup>();
|
|
if (canvasGroup == null)
|
|
canvasGroup = handler.gameObject.AddComponent<CanvasGroup>();
|
|
|
|
canvasGroup.alpha = 0f;
|
|
TweenHelper.FadeCanvasGroup(canvasGroup, 1f, _fadeInDuration, null, LeanTweenType.easeOutQuad);
|
|
|
|
GhostWordInstance instance = new GhostWordInstance
|
|
{
|
|
Handler = handler,
|
|
Container = container,
|
|
CanvasGroup = canvasGroup,
|
|
Lifetime = hallucinationEvent.GhostWordLifetime,
|
|
ElapsedTime = 0f,
|
|
IsFadingOut = false
|
|
};
|
|
|
|
_activeWords.Add(instance);
|
|
StartCoroutine(LifetimeRoutine(instance));
|
|
}
|
|
|
|
// ─── Drift ─────────────────────────────────────────────────────────────────
|
|
|
|
private void DriftTowardPlayer(GhostWordInstance instance)
|
|
{
|
|
if (PlayerController.Instance == null || instance.IsFadingOut)
|
|
return;
|
|
|
|
Transform container = instance.Container.transform;
|
|
Vector3 playerPos = PlayerController.Instance.transform.position;
|
|
float distance = Vector3.Distance(container.position, playerPos);
|
|
|
|
if (distance > _stopDistance)
|
|
{
|
|
Vector3 direction = (playerPos - container.position).normalized;
|
|
container.position += direction * _driftSpeed * Time.deltaTime;
|
|
}
|
|
|
|
// Always face the player
|
|
container.LookAt(playerPos);
|
|
}
|
|
|
|
// ─── Lifetime ──────────────────────────────────────────────────────────────
|
|
|
|
private IEnumerator LifetimeRoutine(GhostWordInstance instance)
|
|
{
|
|
float aliveTime = Mathf.Max(0f, instance.Lifetime - _fadeOutDuration);
|
|
yield return new WaitForSeconds(aliveTime);
|
|
|
|
if (instance.Handler == null)
|
|
yield break;
|
|
|
|
instance.IsFadingOut = true;
|
|
instance.Handler.StopOrganicShake();
|
|
|
|
if (instance.CanvasGroup != null)
|
|
TweenHelper.FadeCanvasGroup(instance.CanvasGroup, 0f, _fadeOutDuration, null, LeanTweenType.easeInQuad);
|
|
|
|
yield return new WaitForSeconds(_fadeOutDuration);
|
|
|
|
_activeWords.Remove(instance);
|
|
|
|
if (instance.Container != null)
|
|
Destroy(instance.Container);
|
|
}
|
|
|
|
// ─── Config ────────────────────────────────────────────────────────────────
|
|
|
|
private FloatingWordsSpawner.SpawnConfig BuildGhostConfig()
|
|
{
|
|
float sanityMultiplier = HallucinationManager.Instance != null
|
|
? HallucinationManager.Instance.WordShakeMultiplier
|
|
: 1f;
|
|
|
|
return new FloatingWordsSpawner.SpawnConfig
|
|
{
|
|
SpawnRadius = 0f,
|
|
SpawnHeightOffset = 0f,
|
|
RotationAxis = RotationAxis.Up,
|
|
EnableOrganicShake = true,
|
|
ForwardOffset = 0f,
|
|
TiltAngle = 0f,
|
|
RotationSpeed = 0f,
|
|
EnableRotation = false,
|
|
SpawnMode = ChoiceSpawnMode.AtAnchorPosition,
|
|
EnableFloating = false,
|
|
MergeDuration = 0.3f,
|
|
MergeScalePulse = 1f,
|
|
EnableGhostTrail = true,
|
|
GhostTrailAlpha = 0.25f,
|
|
GhostTrailDelay = 0.08f,
|
|
ShakeRadius = _shakeRadius * sanityMultiplier,
|
|
ShakeSpeed = _shakeSpeed,
|
|
ShakeSmoothness = _shakeSmoothness,
|
|
MoveTowardPlayerOnFocus = false,
|
|
FocusDistanceFromPlayer = _stopDistance,
|
|
DisableFloatingEffects = false,
|
|
DefaultScaleMultiplier = 1f,
|
|
EnableScaleVariation = false,
|
|
MinScaleVariation = 1f,
|
|
MaxScaleVariation = 1f
|
|
};
|
|
}
|
|
|
|
// ─── Internal ──────────────────────────────────────────────────────────────
|
|
|
|
private class GhostWordInstance
|
|
{
|
|
public ChoiceFocusHandler Handler;
|
|
public GameObject Container;
|
|
public CanvasGroup CanvasGroup;
|
|
public float Lifetime;
|
|
public float ElapsedTime;
|
|
public bool IsFadingOut;
|
|
}
|
|
}
|
|
}
|