Hallucination system setup & implementation with the infinite staircase + Fake volumetric lights test
This commit is contained in:
@@ -1,42 +1,86 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
namespace Unheimlich.Features.Hallucination
|
||||
{
|
||||
// ─── Per-label override data ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Describes how a specific FloatingLabel should behave when a HallucinationEvent fires.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class FloatingLabelOverride
|
||||
{
|
||||
[Tooltip("The FloatingLabel to override when this event fires.")]
|
||||
public FloatingLabel Target;
|
||||
|
||||
[Header("Shake Override")]
|
||||
[Tooltip("Absolute shake radius applied during the override (world units). 0 = no shake.")]
|
||||
[Min(0f)] public float ShakeRadius = 0.15f;
|
||||
[Tooltip("Shake speed during the override.")]
|
||||
[Min(0f)] public float ShakeSpeed = 3f;
|
||||
[Tooltip("Shake smoothness during the override.")]
|
||||
[Min(0f)] public float ShakeSmoothness = 4f;
|
||||
|
||||
[Header("Ghost Trail Override")]
|
||||
public bool GhostEnabled = true;
|
||||
[Range(0f, 1f)] public float GhostAlpha = 0.5f;
|
||||
|
||||
[Header("Duration")]
|
||||
[Tooltip("How long the override lasts (seconds) before reverting to the sanity baseline.")]
|
||||
[Min(0f)] public float Duration = 2f;
|
||||
}
|
||||
|
||||
// ─── ScriptableObject ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// ScriptableObject describing a discrete hallucination event.
|
||||
/// Configure in the inspector and pass to HallucinationManager.TriggerHallucinationEvent().
|
||||
///
|
||||
/// Ghost word spawn removed. Words live on FloatingLabel components in the scene.
|
||||
/// LabelOverrides targets specific labels with timed shake/ghost overrides.
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "HallucinationEvent", menuName = "Unheimlich/Hallucination/Hallucination Event")]
|
||||
[CreateAssetMenu(
|
||||
fileName = "HallucinationEvent",
|
||||
menuName = "Unheimlich/Hallucination/Hallucination Event")]
|
||||
public class HallucinationEvent : ScriptableObject
|
||||
{
|
||||
[Header("Identity")]
|
||||
[Tooltip("Human-readable label for this event (used in logs and the inspector)")]
|
||||
[SerializeField] private string _label;
|
||||
|
||||
[Header("Ghost Word")]
|
||||
[Tooltip("Spawn a ghost word that moves toward the player")]
|
||||
[SerializeField] private bool _spawnGhostWord;
|
||||
|
||||
[Tooltip("Text displayed by the ghost word")]
|
||||
[SerializeField] private string _ghostWordText;
|
||||
|
||||
[Tooltip("World-space spawn position offset from the anchor point")]
|
||||
[SerializeField] private Vector3 _ghostWordSpawnOffset = new Vector3(0f, 1.5f, 2f);
|
||||
|
||||
[Tooltip("Duration before the ghost word disappears (seconds)")]
|
||||
[SerializeField] private float _ghostWordLifetime = 3f;
|
||||
|
||||
[Header("Sanity Cost")]
|
||||
[Tooltip("Sanity added to the player when this event fires (0 = no change)")]
|
||||
[SerializeField, Range(0f, 1f)] private float _sanityCost = 0f;
|
||||
|
||||
// ─── Public Accessors ──────────────────────────────────────────────────────
|
||||
[Header("Floating Label Overrides")]
|
||||
[Tooltip("One entry per FloatingLabel to affect when this event fires.")]
|
||||
[SerializeField] private List<FloatingLabelOverride> _labelOverrides = new();
|
||||
|
||||
public string Label => _label;
|
||||
public bool SpawnGhostWord => _spawnGhostWord;
|
||||
public string GhostWordText => _ghostWordText;
|
||||
public Vector3 GhostWordSpawnOffset => _ghostWordSpawnOffset;
|
||||
public float GhostWordLifetime => _ghostWordLifetime;
|
||||
public float SanityCost => _sanityCost;
|
||||
// ─── Public Accessors ──────────────────────────────────────────────────
|
||||
|
||||
public string Label => _label;
|
||||
public float SanityCost => _sanityCost;
|
||||
public IReadOnlyList<FloatingLabelOverride> LabelOverrides => _labelOverrides;
|
||||
|
||||
// ─── Internal ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Pushes all label overrides onto their target FloatingLabels.
|
||||
/// Called by HallucinationManager.TriggerHallucinationEvent().
|
||||
/// </summary>
|
||||
internal void ApplyLabelOverrides()
|
||||
{
|
||||
foreach (FloatingLabelOverride entry in _labelOverrides)
|
||||
{
|
||||
if (entry.Target == null) continue;
|
||||
|
||||
entry.Target.ApplyOverride(
|
||||
entry.ShakeRadius,
|
||||
entry.ShakeSpeed,
|
||||
entry.ShakeSmoothness,
|
||||
entry.GhostEnabled,
|
||||
entry.GhostAlpha,
|
||||
entry.Duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
namespace Unheimlich.Features.Hallucination
|
||||
{
|
||||
/// <summary>
|
||||
/// Static registry that FloatingLabel instances self-register into on OnEnable/OnDisable.
|
||||
/// Lets HallucinationManager iterate active labels in a radius without any FindObjectsOfType.
|
||||
/// </summary>
|
||||
public static class HallucinationLabelRegistry
|
||||
{
|
||||
private static readonly HashSet<FloatingLabel> _labels = new();
|
||||
|
||||
public static void Register(FloatingLabel label) => _labels.Add(label);
|
||||
public static void Unregister(FloatingLabel label) => _labels.Remove(label);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all registered labels whose transform is within <paramref name="radius"/>
|
||||
/// of <paramref name="center"/>. Results are written into <paramref name="results"/>
|
||||
/// (which is cleared first) to avoid allocations per call.
|
||||
/// </summary>
|
||||
public static void GetLabelsInRadius(Vector3 center, float radius, List<FloatingLabel> results)
|
||||
{
|
||||
results.Clear();
|
||||
float sqrRadius = radius * radius;
|
||||
foreach (FloatingLabel label in _labels)
|
||||
{
|
||||
if (label == null) continue;
|
||||
if ((label.transform.position - center).sqrMagnitude <= sqrRadius)
|
||||
results.Add(label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63e30fb837ed3ab4182c89a053c11a6a
|
||||
+117
-111
@@ -3,41 +3,27 @@ using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.Core;
|
||||
using Unheimlich.Features.Dialogue.UI;
|
||||
|
||||
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")]
|
||||
@@ -62,151 +48,175 @@ namespace Unheimlich.Features.Hallucination
|
||||
|
||||
[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, Min(0f)] private float _bloomIntensityMin = 0f;
|
||||
[SerializeField, Min(0f)] private float _bloomIntensityMax = 1f;
|
||||
[SerializeField, Min(0f)] private float _bloomThresholdMin = 1f;
|
||||
[SerializeField, Min(0f)] private float _bloomThresholdMax = 0.5f;
|
||||
[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;
|
||||
[Header("Floating Label — Baseline Influence")]
|
||||
[Tooltip("Labels within this radius of the player receive the sanity-driven baseline.")]
|
||||
[SerializeField, Min(0f)] private float _labelInfluenceRadius = 12f;
|
||||
|
||||
[Tooltip("Max shake multiplier at sanity = 1")]
|
||||
[SerializeField] private float _shakeMultiplierAtOne = 3f;
|
||||
[Header("Floating Label — Shake")]
|
||||
[Tooltip("Shake radius applied to labels at sanity = 0.")]
|
||||
[SerializeField, Min(0f)] private float _labelShakeRadiusAtZero = 0f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSpeedAtZero = 1.5f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSmoothnessAtZero = 2f;
|
||||
|
||||
[Tooltip("Shake radius applied to labels at sanity = 1.")]
|
||||
[SerializeField, Min(0f)] private float _labelShakeRadiusAtOne = 0.2f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSpeedAtOne = 4f;
|
||||
[SerializeField, Min(0f)] private float _labelShakeSmoothnessAtOne = 5f;
|
||||
|
||||
[Header("Floating Label — Ghost Trail")]
|
||||
[SerializeField] private bool _ghostEnabledAtZero = false;
|
||||
[SerializeField] private bool _ghostEnabledAtOne = true;
|
||||
[SerializeField, Range(0f, 1f)] private float _ghostAlphaMin = 0.1f;
|
||||
[SerializeField, Range(0f, 1f)] private float _ghostAlphaMax = 0.5f;
|
||||
|
||||
[Header("Word Shake (legacy — for other systems)")]
|
||||
[SerializeField] private float _shakeMultiplierLegacyAtZero = 1f;
|
||||
[SerializeField] private float _shakeMultiplierLegacyAtOne = 3f;
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool _debugMode;
|
||||
|
||||
// ─── State ─────────────────────────────────────────────────────────────────
|
||||
// ─── State ─────────────────────────────────────────────────────────────
|
||||
|
||||
private float _currentSanity;
|
||||
private float _displayedSanity; // lerped value used for visual effects
|
||||
private float _currentSanity;
|
||||
private float _displayedSanity;
|
||||
private Coroutine _lerpCoroutine;
|
||||
private bool _vignetteExternallyOverridden;
|
||||
|
||||
// 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;
|
||||
private LensDistortion _lensDistortion;
|
||||
private Vignette _vignette;
|
||||
private FilmGrain _filmGrain;
|
||||
private Bloom _bloom;
|
||||
|
||||
// ─── Public API ────────────────────────────────────────────────────────────
|
||||
private Transform _playerTransform;
|
||||
private readonly List<FloatingLabel> _labelsInRange = new();
|
||||
|
||||
// ─── 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);
|
||||
Mathf.Lerp(_shakeMultiplierLegacyAtZero, _shakeMultiplierLegacyAtOne, _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<float> OnSanityChanged;
|
||||
public event Action<HallucinationEvent> OnHallucinationTriggered;
|
||||
|
||||
// ─── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
// ─── Lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
_currentSanity = Mathf.Clamp(_initialSanity, MinSanity, MaxSanity);
|
||||
_currentSanity = Mathf.Clamp(_initialSanity, MinSanity, MaxSanity);
|
||||
_displayedSanity = _currentSanity;
|
||||
CacheVolumeOverrides();
|
||||
ApplyEffectsImmediate(_currentSanity);
|
||||
ApplyPostProcessImmediate(_currentSanity);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_sanityRecoveryRate > 0f && _currentSanity > MinSanity)
|
||||
SetSanity(_currentSanity - _sanityRecoveryRate * Time.deltaTime);
|
||||
|
||||
DriveFloatingLabelBaseline();
|
||||
}
|
||||
|
||||
// ─── Public Methods ────────────────────────────────────────────────────────
|
||||
// ─── 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);
|
||||
}
|
||||
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;
|
||||
if (Mathf.Approximately(clamped, _currentSanity)) return;
|
||||
|
||||
_currentSanity = clamped;
|
||||
OnSanityChanged?.Invoke(_currentSanity);
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[HallucinationManager] Sanity → {_currentSanity:F2}", this);
|
||||
|
||||
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);
|
||||
_vignette.intensity.value = overridden
|
||||
? 0f
|
||||
: Mathf.Lerp(_vignetteMin, _vignetteMax, _displayedSanity);
|
||||
}
|
||||
|
||||
/// <summary>Fires a discrete hallucination event (ghost word, vision flash, etc.).</summary>
|
||||
/// <summary>
|
||||
/// Fires a discrete hallucination event:
|
||||
/// 1. Applies SanityCost.
|
||||
/// 2. Dispatches per-label overrides defined in the event asset.
|
||||
/// 3. Raises OnHallucinationTriggered.
|
||||
/// </summary>
|
||||
public void TriggerHallucinationEvent(HallucinationEvent hallucinationEvent)
|
||||
{
|
||||
if (hallucinationEvent == null)
|
||||
{
|
||||
Debug.LogError("[HallucinationManager] Attempted to trigger a null HallucinationEvent!", this);
|
||||
Debug.LogError("[HallucinationManager] Null HallucinationEvent!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hallucinationEvent.SanityCost > 0f)
|
||||
AddInsanity(hallucinationEvent.SanityCost);
|
||||
|
||||
hallucinationEvent.ApplyLabelOverrides();
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[HallucinationManager] Event: {hallucinationEvent.name}", this);
|
||||
Debug.Log($"[HallucinationManager] Event: {hallucinationEvent.Label} " +
|
||||
$"({hallucinationEvent.LabelOverrides.Count} label overrides)", this);
|
||||
|
||||
OnHallucinationTriggered?.Invoke(hallucinationEvent);
|
||||
}
|
||||
|
||||
// ─── Lerp ──────────────────────────────────────────────────────────────────
|
||||
// ─── Floating Label Baseline ───────────────────────────────────────────
|
||||
|
||||
private void DriveFloatingLabelBaseline()
|
||||
{
|
||||
EnsurePlayerTransform();
|
||||
if (_playerTransform == null) return;
|
||||
|
||||
HallucinationLabelRegistry.GetLabelsInRadius(
|
||||
_playerTransform.position, _labelInfluenceRadius, _labelsInRange);
|
||||
|
||||
float t = _displayedSanity;
|
||||
|
||||
float shakeRadius = Mathf.Lerp(_labelShakeRadiusAtZero, _labelShakeRadiusAtOne, t);
|
||||
float shakeSpeed = Mathf.Lerp(_labelShakeSpeedAtZero, _labelShakeSpeedAtOne, t);
|
||||
float shakeSmoothness = Mathf.Lerp(_labelShakeSmoothnessAtZero, _labelShakeSmoothnessAtOne, t);
|
||||
bool ghostEnabled = t >= 0.5f ? _ghostEnabledAtOne : _ghostEnabledAtZero;
|
||||
float ghostAlpha = Mathf.Lerp(_ghostAlphaMin, _ghostAlphaMax, t);
|
||||
|
||||
foreach (FloatingLabel label in _labelsInRange)
|
||||
{
|
||||
label.ApplyBaseline(shakeRadius, shakeSpeed, shakeSmoothness, ghostEnabled, ghostAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsurePlayerTransform()
|
||||
{
|
||||
if (_playerTransform != null) return;
|
||||
if (Unheimlich.Features.Player.PlayerController.Instance != null)
|
||||
_playerTransform = Unheimlich.Features.Player.PlayerController.Instance.transform;
|
||||
else if (Camera.main != null)
|
||||
_playerTransform = Camera.main.transform;
|
||||
}
|
||||
|
||||
// ─── Lerp ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void RestartLerp()
|
||||
{
|
||||
if (_lerpCoroutine != null)
|
||||
StopCoroutine(_lerpCoroutine);
|
||||
|
||||
if (_lerpCoroutine != null) StopCoroutine(_lerpCoroutine);
|
||||
_lerpCoroutine = StartCoroutine(LerpEffects());
|
||||
}
|
||||
|
||||
@@ -214,22 +224,21 @@ namespace Unheimlich.Features.Hallucination
|
||||
{
|
||||
while (!Mathf.Approximately(_displayedSanity, _currentSanity))
|
||||
{
|
||||
_displayedSanity = Mathf.MoveTowards(_displayedSanity, _currentSanity, _effectLerpSpeed * Time.deltaTime);
|
||||
ApplyEffectsImmediate(_displayedSanity);
|
||||
_displayedSanity = Mathf.MoveTowards(
|
||||
_displayedSanity, _currentSanity, _effectLerpSpeed * Time.deltaTime);
|
||||
ApplyPostProcessImmediate(_displayedSanity);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_displayedSanity = _currentSanity;
|
||||
ApplyEffectsImmediate(_displayedSanity);
|
||||
ApplyPostProcessImmediate(_displayedSanity);
|
||||
_lerpCoroutine = null;
|
||||
}
|
||||
|
||||
// ─── Effects ───────────────────────────────────────────────────────────────
|
||||
// ─── Post-Process Effects ──────────────────────────────────────────────
|
||||
|
||||
private void CacheVolumeOverrides()
|
||||
{
|
||||
if (_hallucinationVolume == null || _hallucinationVolume.profile == null)
|
||||
return;
|
||||
if (_hallucinationVolume == null || _hallucinationVolume.profile == null) return;
|
||||
|
||||
_hallucinationVolume.profile.TryGet(out _chromaticAberration);
|
||||
_hallucinationVolume.profile.TryGet(out _lensDistortion);
|
||||
@@ -237,20 +246,19 @@ namespace Unheimlich.Features.Hallucination
|
||||
_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.weight = 1f;
|
||||
_hallucinationVolume.enabled = true;
|
||||
}
|
||||
|
||||
private void ApplyEffectsImmediate(float t)
|
||||
private void ApplyPostProcessImmediate(float t)
|
||||
{
|
||||
if (_driveChromaticAberration && _chromaticAberration != null)
|
||||
_chromaticAberration.intensity.value = Mathf.Lerp(_chromaticAberrationMin, _chromaticAberrationMax, t);
|
||||
_chromaticAberration.intensity.value =
|
||||
Mathf.Lerp(_chromaticAberrationMin, _chromaticAberrationMax, t);
|
||||
|
||||
if (_driveLensDistortion && _lensDistortion != null)
|
||||
_lensDistortion.intensity.value = Mathf.Lerp(_lensDistortionMin, _lensDistortionMax, t);
|
||||
_lensDistortion.intensity.value =
|
||||
Mathf.Lerp(_lensDistortionMin, _lensDistortionMax, t);
|
||||
|
||||
if (_driveVignette && _vignette != null && !_vignetteExternallyOverridden)
|
||||
_vignette.intensity.value = Mathf.Lerp(_vignetteMin, _vignetteMax, t);
|
||||
@@ -260,13 +268,11 @@ namespace Unheimlich.Features.Hallucination
|
||||
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-25
@@ -5,24 +5,21 @@ namespace Unheimlich.Features.Hallucination
|
||||
{
|
||||
/// <summary>
|
||||
/// Self-contained zone that triggers a hallucination effect when the player enters.
|
||||
/// Handles the once-flag, optional delay, insanity increase, and a discrete HallucinationEvent
|
||||
/// — no GameEventTrigger or HallucinationTrigger required alongside it.
|
||||
/// Ghost-word spawn removed — words live on FloatingLabel components in the scene.
|
||||
/// SetEvent() lets InfiniteStaircaseController inject the right event per slot at runtime.
|
||||
/// </summary>
|
||||
public class HallucinationTriggerZone : TriggerZoneBase
|
||||
{
|
||||
[Header("Zone Behaviour")]
|
||||
[Tooltip("Allow the zone to fire again after the player exits and re-enters")]
|
||||
[SerializeField] private bool _retriggerable = false;
|
||||
|
||||
[Tooltip("Delay in seconds before the hallucination fires after the player enters")]
|
||||
[SerializeField] private float _delay = 0f;
|
||||
[SerializeField] private bool _retriggerable = false;
|
||||
[SerializeField] private float _delay = 0f;
|
||||
|
||||
[Header("Sanity")]
|
||||
[Tooltip("Amount of insanity added when the zone fires (0 = no change)")]
|
||||
[Tooltip("Insanity added when the zone fires, on top of any SanityCost on the event.")]
|
||||
[SerializeField, Range(0f, 1f)] private float _insanityAmount = 0f;
|
||||
|
||||
[Header("Hallucination Event")]
|
||||
[Tooltip("Optional discrete hallucination event (ghost word, vision flash, etc.)")]
|
||||
[Tooltip("Overridable at runtime via SetEvent().")]
|
||||
[SerializeField] private HallucinationEvent _hallucinationEvent;
|
||||
|
||||
[Header("Debug")]
|
||||
@@ -32,39 +29,38 @@ namespace Unheimlich.Features.Hallucination
|
||||
|
||||
protected override void OnPlayerEntered(Collider playerCollider)
|
||||
{
|
||||
if (_delay > 0f)
|
||||
Invoke(nameof(Fire), _delay);
|
||||
else
|
||||
Fire();
|
||||
if (_delay > 0f) Invoke(nameof(Fire), _delay);
|
||||
else Fire();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires the hallucination effect. Can also be called from external scripts or UnityEvents.
|
||||
/// </summary>
|
||||
/// <summary>Overrides the event at runtime. Pass null to disable.</summary>
|
||||
public void SetEvent(HallucinationEvent hallucinationEvent) =>
|
||||
_hallucinationEvent = hallucinationEvent;
|
||||
|
||||
/// <summary>Fires the hallucination. Can be called from external scripts or UnityEvents.</summary>
|
||||
public void Fire()
|
||||
{
|
||||
if (HallucinationManager.Instance == null)
|
||||
{
|
||||
Debug.LogError("[HallucinationTriggerZone] HallucinationManager not found in scene!", this);
|
||||
Debug.LogError("[HallucinationTriggerZone] HallucinationManager not found!", this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_insanityAmount > 0f)
|
||||
{
|
||||
HallucinationManager.Instance.AddInsanity(_insanityAmount);
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[HallucinationTriggerZone] Added {_insanityAmount:F2} insanity on '{gameObject.name}'.", this);
|
||||
if (_debugMode) Debug.Log($"[HallucinationTriggerZone] +{_insanityAmount:F2} insanity.", this);
|
||||
}
|
||||
|
||||
if (_hallucinationEvent != null)
|
||||
{
|
||||
HallucinationManager.Instance.TriggerHallucinationEvent(_hallucinationEvent);
|
||||
|
||||
if (_debugMode)
|
||||
Debug.Log($"[HallucinationTriggerZone] Fired event: {_hallucinationEvent.Label}", this);
|
||||
if (_debugMode) Debug.Log($"[HallucinationTriggerZone] Event: {_hallucinationEvent.Label}", this);
|
||||
}
|
||||
else if (_debugMode)
|
||||
{
|
||||
Debug.Log($"[HallucinationTriggerZone] No event on '{gameObject.name}'.", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+15
-237
@@ -1,248 +1,26 @@
|
||||
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.
|
||||
/// DEPRECATED — Ghost word spawning has been removed from the hallucination system.
|
||||
///
|
||||
/// Words are now FloatingLabel components placed directly in the scene (on stair prefabs
|
||||
/// or world props). Their behaviour (shake, ghost trail) is driven at runtime by
|
||||
/// HallucinationManager (baseline, sanity-lerped) and HallucinationEvent (timed overrides).
|
||||
///
|
||||
/// This script is kept as an empty stub so existing scene references don't break.
|
||||
/// It can safely be removed from any GameObject it was attached to.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
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()
|
||||
private void Awake()
|
||||
{
|
||||
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;
|
||||
Debug.LogWarning(
|
||||
"[HallucinationWordSpawner] This component is deprecated and does nothing. " +
|
||||
"Remove it from the scene and use FloatingLabel + HallucinationEvent overrides instead.",
|
||||
this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user