273 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|