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 { /// /// 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. /// 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 _activeWords = new List(); // ─── 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.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(); if (canvasGroup == null) canvasGroup = handler.gameObject.AddComponent(); 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; } } }