Files
Ipseity-Demo/Assets/Unheimlich_POC/Scripts/Features/Hallucination/Runtime/HallucinationManager.cs
T
2026-03-16 17:06:47 +01:00

273 lines
13 KiB
C#

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using System;
using System.Collections;
using Unheimlich.Core;
namespace Unheimlich.Features.Hallucination
{
/// <summary>
/// Central manager for the hallucination system.
/// Tracks the player's sanity level (0 = sane, 1 = fully insane) and drives all
/// hallucination effects: individual URP post-process overrides (with lerp), word shake
/// intensity, and ghost-word visibility via the HallucinationWordSpawner.
///
/// Effects are applied directly on VolumeProfile overrides rather than on volume.weight,
/// so multiple volumes coexist without parameter conflicts.
/// </summary>
public class HallucinationManager : Singleton<HallucinationManager>
{
// ─── Constants ─────────────────────────────────────────────────────────────
private const float MinSanity = 0f;
private const float MaxSanity = 1f;
// ─── Inspector ─────────────────────────────────────────────────────────────
[Header("Sanity")]
[Tooltip("Starting sanity level (0 = sane, 1 = fully insane)")]
[SerializeField, Range(0f, 1f)] private float _initialSanity = 0f;
[Tooltip("Sanity naturally recovers over time at this rate per second (0 = no recovery)")]
[SerializeField, Range(0f, 0.1f)] private float _sanityRecoveryRate = 0f;
[Header("Effect Lerp")]
[Tooltip("Speed at which visual effects lerp toward the target sanity value")]
[SerializeField] private float _effectLerpSpeed = 2f;
[Header("URP Post-Process Volume")]
[Tooltip("Volume containing the hallucination overrides (Chromatic Aberration, Lens Distortion, etc.)")]
[SerializeField] private Volume _hallucinationVolume;
[Header("Chromatic Aberration")]
[SerializeField] private bool _driveChromaticAberration = true;
[SerializeField, Range(0f, 1f)] private float _chromaticAberrationMin = 0f;
[SerializeField, Range(0f, 1f)] private float _chromaticAberrationMax = 0.8f;
[Header("Lens Distortion")]
[SerializeField] private bool _driveLensDistortion = true;
[SerializeField, Range(-1f, 1f)] private float _lensDistortionMin = 0f;
[SerializeField, Range(-1f, 1f)] private float _lensDistortionMax = -0.4f;
[Header("Vignette")]
[SerializeField] private bool _driveVignette = true;
[SerializeField, Range(0f, 1f)] private float _vignetteMin = 0.3f;
[SerializeField, Range(0f, 1f)] private float _vignetteMax = 0.7f;
[Header("Film Grain")]
[SerializeField] private bool _driveFilmGrain = true;
[SerializeField, Range(0f, 1f)] private float _filmGrainMin = 0f;
[SerializeField, Range(0f, 1f)] private float _filmGrainMax = 0.6f;
[Header("Bloom")]
[SerializeField] private bool _driveBloom = true;
[Tooltip("Bloom intensity at sanity = 0")]
[SerializeField, Min(0f)] private float _bloomIntensityMin = 0f;
[Tooltip("Bloom intensity at sanity = 1")]
[SerializeField, Min(0f)] private float _bloomIntensityMax = 1f;
[Tooltip("Bloom threshold at sanity = 0 — raise this to suppress any bleed at zero sanity")]
[SerializeField, Min(0f)] private float _bloomThresholdMin = 1f;
[Tooltip("Bloom threshold at sanity = 1")]
[SerializeField, Min(0f)] private float _bloomThresholdMax = 0.5f;
[Tooltip("Bloom scatter (spread) at sanity = 0")]
[SerializeField, Range(0f, 1f)] private float _bloomScatterMin = 0f;
[Tooltip("Bloom scatter (spread) at sanity = 1")]
[SerializeField, Range(0f, 1f)] private float _bloomScatterMax = 0.7f;
[Tooltip("Bloom dirt texture intensity at sanity = 0")]
[SerializeField, Min(0f)] private float _bloomDirtIntensityMin = 0f;
[Tooltip("Bloom dirt texture intensity at sanity = 1")]
[SerializeField, Min(0f)] private float _bloomDirtIntensityMax = 0f;
[Header("Word Shake")]
[Tooltip("Baseline shake multiplier at sanity = 0")]
[SerializeField] private float _shakeMultiplierAtZero = 1f;
[Tooltip("Max shake multiplier at sanity = 1")]
[SerializeField] private float _shakeMultiplierAtOne = 3f;
[Header("Debug")]
[SerializeField] private bool _debugMode;
// ─── State ─────────────────────────────────────────────────────────────────
private float _currentSanity;
private float _displayedSanity; // lerped value used for visual effects
private Coroutine _lerpCoroutine;
// External systems can suppress the hallucination vignette (e.g. diapason listening mode).
private bool _vignetteExternallyOverridden;
// Cached volume overrides
private ChromaticAberration _chromaticAberration;
private LensDistortion _lensDistortion;
private Vignette _vignette;
private FilmGrain _filmGrain;
private Bloom _bloom;
// ─── Public API ────────────────────────────────────────────────────────────
/// <summary>Current logical sanity level in [0, 1]. 0 = sane, 1 = fully insane.</summary>
public float SanityLevel => _currentSanity;
/// <summary>
/// Word shake multiplier computed from the lerped display sanity (smooth).
/// </summary>
public float WordShakeMultiplier =>
Mathf.Lerp(_shakeMultiplierAtZero, _shakeMultiplierAtOne, _displayedSanity);
/// <summary>Raised whenever the logical sanity level changes. Payload is the new value in [0, 1].</summary>
public event Action<float> OnSanityChanged;
/// <summary>Raised when a discrete hallucination event is triggered (e.g. ghost word appears).</summary>
public event Action<HallucinationEvent> OnHallucinationTriggered;
// ─── Lifecycle ─────────────────────────────────────────────────────────────
protected override void OnAwake()
{
_currentSanity = Mathf.Clamp(_initialSanity, MinSanity, MaxSanity);
_displayedSanity = _currentSanity;
CacheVolumeOverrides();
ApplyEffectsImmediate(_currentSanity);
}
private void Update()
{
if (_sanityRecoveryRate > 0f && _currentSanity > MinSanity)
SetSanity(_currentSanity - _sanityRecoveryRate * Time.deltaTime);
}
// ─── Public Methods ────────────────────────────────────────────────────────
/// <summary>
/// Increases the sanity level by <paramref name="amount"/> (clamped to [0, 1]).
/// Effects lerp smoothly to the new value.
/// </summary>
public void AddInsanity(float amount)
{
SetSanity(_currentSanity + amount);
}
/// <summary>
/// Directly sets the logical sanity level. Effects lerp smoothly to the new value.
/// </summary>
public void SetSanity(float value)
{
float clamped = Mathf.Clamp(value, MinSanity, MaxSanity);
if (Mathf.Approximately(clamped, _currentSanity))
return;
_currentSanity = clamped;
OnSanityChanged?.Invoke(_currentSanity);
if (_debugMode)
Debug.Log($"[HallucinationManager] Sanity → {_currentSanity:F2}", this);
RestartLerp();
}
/// <summary>
/// Suppresses the hallucination vignette so an external system (e.g. diapason) can
/// take full control of vignette without additive blending artefacts.
/// Call with <c>false</c> when the external system is done.
/// </summary>
public void SetVignetteOverriddenExternally(bool overridden)
{
_vignetteExternallyOverridden = overridden;
// Apply immediately so the vignette drops to zero without waiting for the lerp coroutine
if (_vignette != null)
_vignette.intensity.value = overridden ? 0f : Mathf.Lerp(_vignetteMin, _vignetteMax, _displayedSanity);
}
/// <summary>Fires a discrete hallucination event (ghost word, vision flash, etc.).</summary>
public void TriggerHallucinationEvent(HallucinationEvent hallucinationEvent)
{
if (hallucinationEvent == null)
{
Debug.LogError("[HallucinationManager] Attempted to trigger a null HallucinationEvent!", this);
return;
}
if (hallucinationEvent.SanityCost > 0f)
AddInsanity(hallucinationEvent.SanityCost);
if (_debugMode)
Debug.Log($"[HallucinationManager] Event: {hallucinationEvent.name}", this);
OnHallucinationTriggered?.Invoke(hallucinationEvent);
}
// ─── Lerp ──────────────────────────────────────────────────────────────────
private void RestartLerp()
{
if (_lerpCoroutine != null)
StopCoroutine(_lerpCoroutine);
_lerpCoroutine = StartCoroutine(LerpEffects());
}
private IEnumerator LerpEffects()
{
while (!Mathf.Approximately(_displayedSanity, _currentSanity))
{
_displayedSanity = Mathf.MoveTowards(_displayedSanity, _currentSanity, _effectLerpSpeed * Time.deltaTime);
ApplyEffectsImmediate(_displayedSanity);
yield return null;
}
_displayedSanity = _currentSanity;
ApplyEffectsImmediate(_displayedSanity);
_lerpCoroutine = null;
}
// ─── Effects ───────────────────────────────────────────────────────────────
private void CacheVolumeOverrides()
{
if (_hallucinationVolume == null || _hallucinationVolume.profile == null)
return;
_hallucinationVolume.profile.TryGet(out _chromaticAberration);
_hallucinationVolume.profile.TryGet(out _lensDistortion);
_hallucinationVolume.profile.TryGet(out _vignette);
_hallucinationVolume.profile.TryGet(out _filmGrain);
_hallucinationVolume.profile.TryGet(out _bloom);
// Keep volume weight at 1 permanently — we write individual override values directly.
// The hallucination volume must be on its OWN dedicated VolumeProfile, separate from
// the dialogue blur volume, to avoid weight conflicts between the two systems.
_hallucinationVolume.weight = 1f;
_hallucinationVolume.enabled = true;
}
private void ApplyEffectsImmediate(float t)
{
if (_driveChromaticAberration && _chromaticAberration != null)
_chromaticAberration.intensity.value = Mathf.Lerp(_chromaticAberrationMin, _chromaticAberrationMax, t);
if (_driveLensDistortion && _lensDistortion != null)
_lensDistortion.intensity.value = Mathf.Lerp(_lensDistortionMin, _lensDistortionMax, t);
if (_driveVignette && _vignette != null && !_vignetteExternallyOverridden)
_vignette.intensity.value = Mathf.Lerp(_vignetteMin, _vignetteMax, t);
if (_driveFilmGrain && _filmGrain != null)
_filmGrain.intensity.value = Mathf.Lerp(_filmGrainMin, _filmGrainMax, t);
if (_driveBloom && _bloom != null)
{
// Drive all bloom parameters that can independently cause it to appear.
// intensity alone is not enough — threshold=0 or scatter>0 can bleed through.
_bloom.intensity.value = Mathf.Lerp(_bloomIntensityMin, _bloomIntensityMax, t);
_bloom.threshold.value = Mathf.Lerp(_bloomThresholdMin, _bloomThresholdMax, t);
_bloom.scatter.value = Mathf.Lerp(_bloomScatterMin, _bloomScatterMax, t);
_bloom.dirtIntensity.value = Mathf.Lerp(_bloomDirtIntensityMin, _bloomDirtIntensityMax, t);
}
}
}
}