Hallucination system
This commit is contained in:
+248
@@ -0,0 +1,248 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user