using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using System; using System.Collections; using Unheimlich.Core; namespace Unheimlich.Features.Hallucination { /// /// 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. /// public class HallucinationManager : Singleton { // ─── 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 ──────────────────────────────────────────────────────────── /// Current logical sanity level in [0, 1]. 0 = sane, 1 = fully insane. public float SanityLevel => _currentSanity; /// /// Word shake multiplier computed from the lerped display sanity (smooth). /// public float WordShakeMultiplier => Mathf.Lerp(_shakeMultiplierAtZero, _shakeMultiplierAtOne, _displayedSanity); /// Raised whenever the logical sanity level changes. Payload is the new value in [0, 1]. public event Action OnSanityChanged; /// Raised when a discrete hallucination event is triggered (e.g. ghost word appears). public event Action 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 ──────────────────────────────────────────────────────── /// /// Increases the sanity level by (clamped to [0, 1]). /// Effects lerp smoothly to the new value. /// public void AddInsanity(float amount) { SetSanity(_currentSanity + amount); } /// /// Directly sets the logical sanity level. Effects lerp smoothly to the new value. /// 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(); } /// /// Suppresses the hallucination vignette so an external system (e.g. diapason) can /// take full control of vignette without additive blending artefacts. /// Call with false when the external system is done. /// 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); } /// Fires a discrete hallucination event (ghost word, vision flash, etc.). 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); } } } }