wip : scarecrow fix & subtitle event implementation
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unheimlich.VFX;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
@@ -8,7 +10,6 @@ public class ScarecrowFire : MonoBehaviour
|
||||
public class FireElement
|
||||
{
|
||||
public GameObject VFX;
|
||||
/// <summary>Original scale captured at initialization, used as lerp target.</summary>
|
||||
[HideInInspector] public Vector3 OriginalScale;
|
||||
}
|
||||
|
||||
@@ -23,14 +24,9 @@ public class ScarecrowFire : MonoBehaviour
|
||||
[SerializeField] private FireElement _fireBig;
|
||||
|
||||
[Header("Propagation Timings")]
|
||||
[Tooltip("Duration of the scale lerp for each fire element (seconds).")]
|
||||
[SerializeField] private float _growDuration = 3f;
|
||||
|
||||
[Tooltip("How early the next stage starts before the current one finishes (0 = no overlap, 1 = instant).")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float _overlapFactor = 0.4f;
|
||||
|
||||
[Tooltip("Curve driving the scale growth — tweak directly in the Inspector.")]
|
||||
[SerializeField] private AnimationCurve _growCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 0f),
|
||||
new Keyframe(0.3f, 0.1f, 0f, 2f),
|
||||
@@ -39,10 +35,7 @@ public class ScarecrowFire : MonoBehaviour
|
||||
);
|
||||
|
||||
[Header("Post Process")]
|
||||
[Tooltip("Volume driving the heat haze effect — weight is driven up as fire spreads.")]
|
||||
[SerializeField] private Volume _heatVolume;
|
||||
|
||||
[Tooltip("Curve driving all heat effects over the full sequence duration.")]
|
||||
[SerializeField] private AnimationCurve _heatCurve = new AnimationCurve(
|
||||
new Keyframe(0f, 0f, 0f, 0.5f),
|
||||
new Keyframe(0.5f, 0.4f, 1f, 1f),
|
||||
@@ -50,17 +43,18 @@ public class ScarecrowFire : MonoBehaviour
|
||||
);
|
||||
|
||||
[Header("Heat Shader")]
|
||||
[Tooltip("Material using the HeatHaze shader — _Weight property is driven from 0 to 1.")]
|
||||
[SerializeField] private Material _heatMaterial;
|
||||
|
||||
[Header("Light")]
|
||||
[Tooltip("LightFlicker component to drive — baseIntensity is ramped up with the fire.")]
|
||||
[SerializeField] private MonoLimboStudio.LightFlicker _lightFlicker;
|
||||
[Header("Lights")]
|
||||
[SerializeField] private List<LightEntry> _lights = new List<LightEntry>();
|
||||
|
||||
[Tooltip("Final light intensity reached when the fire is fully ablaze.")]
|
||||
[SerializeField] private float _targetLightIntensity = 3f;
|
||||
[System.Serializable]
|
||||
public class LightEntry
|
||||
{
|
||||
public LightFlicker Flicker;
|
||||
public float TargetIntensity = 3f;
|
||||
}
|
||||
|
||||
// Flag set when the last GrowFire completes — DriveHeatEffects waits on this
|
||||
private bool _fireSequenceComplete = false;
|
||||
private float _totalSequenceDuration = 0f;
|
||||
|
||||
@@ -77,16 +71,13 @@ public class ScarecrowFire : MonoBehaviour
|
||||
InitElement(_fireHead);
|
||||
InitElement(_fireBig);
|
||||
|
||||
// Used as a time reference for _heatCurve mapping — doesn't need to be exact
|
||||
// since DriveHeatEffects now syncs to _fireSequenceComplete rather than this value
|
||||
float overlapDelay = _growDuration * (1f - _overlapFactor);
|
||||
_totalSequenceDuration = _growDuration + (7f * overlapDelay);
|
||||
|
||||
// Ensure everything starts at zero
|
||||
SetHeatShaderWeight(0f);
|
||||
|
||||
if (_lightFlicker)
|
||||
_lightFlicker.SetBaseIntensity(0f);
|
||||
foreach (var entry in _lights)
|
||||
if (entry.Flicker) entry.Flicker.SetBaseIntensity(0f);
|
||||
}
|
||||
|
||||
void Start()
|
||||
@@ -94,10 +85,6 @@ public class ScarecrowFire : MonoBehaviour
|
||||
TriggerFire();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the original Inspector scale, then sets the starting scale to
|
||||
/// (x * 0.2, y * 0.1, z * 0.2) so each element grows from near-nothing.
|
||||
/// </summary>
|
||||
private void InitElement(FireElement el)
|
||||
{
|
||||
if (!el.VFX) return;
|
||||
@@ -113,51 +100,36 @@ public class ScarecrowFire : MonoBehaviour
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Starts the full fire propagation sequence.</summary>
|
||||
public void TriggerFire()
|
||||
{
|
||||
_fireSequenceComplete = false;
|
||||
StartCoroutine(FireSequence());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Propagates fire bottom-up with overlapping stages for an organic feel.
|
||||
/// Sets _fireSequenceComplete when the last element finishes growing.
|
||||
/// </summary>
|
||||
private IEnumerator FireSequence()
|
||||
{
|
||||
float overlapDelay = _growDuration * (1f - _overlapFactor);
|
||||
|
||||
// Drive all heat effects independently — syncs via _fireSequenceComplete
|
||||
StartCoroutine(DriveHeatEffects());
|
||||
|
||||
// Feet
|
||||
StartCoroutine(GrowFire(_fireBase));
|
||||
yield return new WaitForSeconds(overlapDelay);
|
||||
|
||||
// Torso overlaps with feet still growing
|
||||
StartCoroutine(GrowFire(_fireTorso));
|
||||
yield return new WaitForSeconds(overlapDelay);
|
||||
|
||||
// Both arms ignite simultaneously, each propagating from shoulder to tip
|
||||
StartCoroutine(PropagateArm(_fireArmLeft, _fireArmLeftEnd, overlapDelay));
|
||||
StartCoroutine(PropagateArm(_fireArmRight, _fireArmRightEnd, overlapDelay));
|
||||
yield return new WaitForSeconds(overlapDelay * 2f);
|
||||
|
||||
// Head
|
||||
StartCoroutine(GrowFire(_fireHead));
|
||||
yield return new WaitForSeconds(overlapDelay);
|
||||
|
||||
// Full blaze — wait for it to fully complete before flagging done
|
||||
yield return StartCoroutine(GrowFire(_fireBig));
|
||||
|
||||
_fireSequenceComplete = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Propagates fire along one arm: shoulder ignites first,
|
||||
/// then tip starts after overlapDelay rather than waiting for shoulder to finish.
|
||||
/// </summary>
|
||||
private IEnumerator PropagateArm(FireElement shoulder, FireElement tip, float overlapDelay)
|
||||
{
|
||||
StartCoroutine(GrowFire(shoulder));
|
||||
@@ -165,10 +137,6 @@ public class ScarecrowFire : MonoBehaviour
|
||||
yield return StartCoroutine(GrowFire(tip));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates a fire element and lerps its scale from near-zero to its original
|
||||
/// Inspector scale, driven by _growCurve for an organic progression.
|
||||
/// </summary>
|
||||
private IEnumerator GrowFire(FireElement el)
|
||||
{
|
||||
if (!el.VFX) yield break;
|
||||
@@ -195,57 +163,54 @@ public class ScarecrowFire : MonoBehaviour
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Drives all heat effects (post-process volume, heat shader, light intensity)
|
||||
/// using _heatCurve until _fireSequenceComplete is set, then snaps cleanly to 1.
|
||||
/// Decoupled from individual fire element timing for smoother blending.
|
||||
/// </summary>
|
||||
private IEnumerator DriveHeatEffects()
|
||||
{
|
||||
float elapsed = 0f;
|
||||
float elapsed = 0f;
|
||||
float lastWeight = 0f;
|
||||
|
||||
// Phase 1 : monte avec la heatCurve jusqu'à ce que la séquence feu soit finie.
|
||||
// On snapshote lastWeight à chaque frame pour partir de la vraie valeur courante
|
||||
// en phase 2, évitant tout creux si la courbe redescend après t=1.
|
||||
while (!_fireSequenceComplete)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = elapsed / _totalSequenceDuration;
|
||||
float weight = _heatCurve.Evaluate(Mathf.Min(t, 0.99f));
|
||||
|
||||
if (_heatVolume)
|
||||
_heatVolume.weight = weight;
|
||||
|
||||
SetHeatShaderWeight(weight);
|
||||
|
||||
if (_lightFlicker)
|
||||
_lightFlicker.SetBaseIntensity(_targetLightIntensity * weight);
|
||||
float t = elapsed / _totalSequenceDuration;
|
||||
lastWeight = _heatCurve.Evaluate(Mathf.Clamp01(t));
|
||||
|
||||
ApplyHeatWeight(lastWeight);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
float lerpElapsed = 0f;
|
||||
// Phase 2 : lerp propre de la valeur courante vers 1, sans jamais redescendre.
|
||||
float lerpElapsed = 0f;
|
||||
float lerpDuration = 8f;
|
||||
float startWeight = _heatCurve.Evaluate(0.99f);
|
||||
float startWeight = lastWeight; // ← valeur réelle au moment du flag, pas heatCurve(0.99)
|
||||
|
||||
while (lerpElapsed < lerpDuration)
|
||||
{
|
||||
lerpElapsed += Time.deltaTime;
|
||||
float weight = Mathf.Lerp(startWeight, 1f, lerpElapsed / lerpDuration);
|
||||
|
||||
if (_heatVolume) _heatVolume.weight = weight;
|
||||
SetHeatShaderWeight(weight);
|
||||
if (_lightFlicker) _lightFlicker.SetBaseIntensity(_targetLightIntensity * weight);
|
||||
|
||||
ApplyHeatWeight(weight);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (_heatVolume) _heatVolume.weight = 1f;
|
||||
SetHeatShaderWeight(1f);
|
||||
if (_lightFlicker) _lightFlicker.SetBaseIntensity(_targetLightIntensity);
|
||||
// Phase 3 : maintien à 1 à l'infini.
|
||||
ApplyHeatWeight(1f);
|
||||
}
|
||||
|
||||
/// <summary>Applique un weight normalisé à tous les effets de chaleur + lumière.</summary>
|
||||
private void ApplyHeatWeight(float weight)
|
||||
{
|
||||
if (_heatVolume) _heatVolume.weight = weight;
|
||||
SetHeatShaderWeight(weight);
|
||||
|
||||
// ScarecrowFire pilote la base — LightFlicker applique le flicker par-dessus
|
||||
// sans écrire directement sur targetLight.intensity.
|
||||
foreach (var entry in _lights)
|
||||
if (entry.Flicker) entry.Flicker.SetBaseIntensity(entry.TargetIntensity * weight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drives the heat shader via a single normalized weight (0-1).
|
||||
/// All visual parameters are tweaked directly on the material in the Inspector.
|
||||
/// </summary>
|
||||
private void SetHeatShaderWeight(float weight)
|
||||
{
|
||||
if (!_heatMaterial) return;
|
||||
|
||||
@@ -7,12 +7,15 @@ namespace Unheimlich.World.CameraSystem
|
||||
/// Overlays a "lost footage / handheld camcorder" shake on a dedicated pivot
|
||||
/// sitting between CameraPivot and Camera Player.
|
||||
///
|
||||
/// Layered in three passes:
|
||||
/// Layered in four passes:
|
||||
/// 1. Continuous Perlin drift — organic idle tremor, scales with movement and stress.
|
||||
/// 2. Micro-jolts — randomised snap/recoil spikes, only fire when the camera
|
||||
/// is steady (attenuated during active mouse look).
|
||||
/// 2. Micro-jolts — randomised snap/recoil spikes, only fire when the
|
||||
/// camera is steady (attenuated during active mouse look).
|
||||
/// 3. Inertia swinging — the camera "lags" behind head rotation on all axes,
|
||||
/// simulating the physical weight of a handheld camcorder.
|
||||
/// 4. Spinning/dizzy drift — a slow, persistent rotational pull on Y (and a subtle
|
||||
/// rolling tilt on Z) that builds with stress, simulating
|
||||
/// a confused/disoriented head.
|
||||
///
|
||||
/// Hierarchy expected:
|
||||
/// OctoPlayerFP (mouse-look root)
|
||||
@@ -20,13 +23,32 @@ namespace Unheimlich.World.CameraSystem
|
||||
/// └── LostFootagePivot ← this component
|
||||
/// └── Camera Player
|
||||
///
|
||||
/// Mouse-look speed is sampled on OctoPlayerFP (grandparent) to avoid being
|
||||
/// polluted by the continuous rotations CameraHeadBob writes on CameraPivot.
|
||||
/// Movement blend (moveT) is smoothed to avoid noise amplitude spikes on
|
||||
/// direction changes.
|
||||
/// Quick setup:
|
||||
/// • Toggle "Is Active" in the inspector (or call SetActive()) to enable/disable with fade.
|
||||
/// • "Master Intensity" scales everything: noise, jolts, inertia and spin.
|
||||
/// • "Stress Level" can be driven externally (e.g. by a StressController) or left at 0.
|
||||
/// </summary>
|
||||
public class LostFootageRig : MonoBehaviour
|
||||
{
|
||||
// ── Inspector — Global ────────────────────────────────────────────────────
|
||||
|
||||
[Header("Global")]
|
||||
[Tooltip("Enable or disable the whole rig (with fade). Can also be toggled at runtime via SetActive().")]
|
||||
[SerializeField] private bool _isActive = true;
|
||||
|
||||
[Tooltip("Master scale applied to ALL effects (noise amplitude, jolt amplitude, inertia strength, spin strength). 0 = off, 1 = nominal.")]
|
||||
[SerializeField] [Range(0f, 3f)] private float _masterIntensity = 1f;
|
||||
|
||||
// ── Inspector — Stress ────────────────────────────────────────────────────
|
||||
|
||||
[Header("Stress")]
|
||||
[Tooltip("Stress level in [0, 1]. Can be driven externally (StressController.Instance → rig.StressLevel = ...) or set manually here for testing.")]
|
||||
[SerializeField] [Range(0f, 1f)] private float _stressLevel = 0f;
|
||||
|
||||
[Header("Stress — noise scale")]
|
||||
[SerializeField] private float _stressAmplitudeMul = 2.5f;
|
||||
[SerializeField] private float _stressSpeedMul = 1f;
|
||||
|
||||
// ── Inspector — Noise ─────────────────────────────────────────────────────
|
||||
|
||||
[Header("Noise — idle")]
|
||||
@@ -41,12 +63,8 @@ namespace Unheimlich.World.CameraSystem
|
||||
[SerializeField] private float _runAmplitude = 5f;
|
||||
[SerializeField] private float _runNoiseSpeed = 0.6f;
|
||||
|
||||
[Header("Noise — stress scale")]
|
||||
[SerializeField] private float _stressAmplitudeMul = 2.5f;
|
||||
[SerializeField] private float _stressSpeedMul = 1f;
|
||||
|
||||
[Header("Noise — smoothing")]
|
||||
[Tooltip("How fast the noise amplitude blends between idle/walk/run. Lower = smoother transitions.")]
|
||||
[Tooltip("How fast the noise amplitude blends between idle/walk/run.")]
|
||||
[SerializeField] private float _moveTSmoothSpeed = 4f;
|
||||
|
||||
// ── Inspector — Micro-jolts ───────────────────────────────────────────────
|
||||
@@ -64,56 +82,73 @@ namespace Unheimlich.World.CameraSystem
|
||||
[SerializeField] private Vector2 _runJoltIntervalRange = new Vector2(1f, 2f);
|
||||
|
||||
[Header("Micro-jolts — shared")]
|
||||
[Tooltip("How fast the jolt snaps to its target (fraction of interval).")]
|
||||
[SerializeField] private float _joltSnapFraction = 0.1f;
|
||||
[Tooltip("Jolt interval is divided by this value when stress = 1.")]
|
||||
[SerializeField] private float _stressJoltFreqMul = 2f;
|
||||
[Tooltip("Jolt amplitude is multiplied by this value when stress = 1.")]
|
||||
[SerializeField] private float _stressJoltAmpMul = 1.4f;
|
||||
[Tooltip("Look speed (deg/s) above which jolts are fully suppressed. Sampled on OctoPlayerFP to bypass CameraHeadBob noise.")]
|
||||
[SerializeField] private float _joltLookSuppressSpeed = 5f;
|
||||
[SerializeField] private float _joltSnapFraction = 0.1f;
|
||||
[SerializeField] private float _stressJoltFreqMul = 2f;
|
||||
[SerializeField] private float _stressJoltAmpMul = 1.4f;
|
||||
[Tooltip("Look speed (deg/s) above which jolts are fully suppressed.")]
|
||||
[SerializeField] private float _joltLookSuppressSpeed = 5f;
|
||||
|
||||
// ── Inspector — Inertia swing ─────────────────────────────────────────────
|
||||
|
||||
[Header("Inertia swing")]
|
||||
[Tooltip("How strongly the camera lags behind movement. Higher = more swing.")]
|
||||
[SerializeField] private float _inertiaStrength = 4f;
|
||||
[Tooltip("How fast the inertia offset returns to zero. Lower = longer swing tail.")]
|
||||
[SerializeField] private float _inertiaDamping = 6f;
|
||||
[Tooltip("Maximum swing angle (degrees) on each axis to avoid excessive drift.")]
|
||||
[SerializeField] private float _inertiaMaxAngle = 8f;
|
||||
|
||||
// ── Inspector — Spinning / dizzy head ─────────────────────────────────────
|
||||
|
||||
[Header("Spinning / dizzy head")]
|
||||
[Tooltip("Maximum Y-axis spin angle (degrees) at stress = 1 and masterIntensity = 1.")]
|
||||
[SerializeField] private float _spinMaxYAngle = 6f;
|
||||
|
||||
[Tooltip("Maximum Z-axis roll angle (degrees) at stress = 1 — the subtle listing tilt.")]
|
||||
[SerializeField] private float _spinMaxZAngle = 2.5f;
|
||||
|
||||
[Tooltip("Speed of the Perlin noise driving the spin drift. Lower = slower, more nauseating.")]
|
||||
[SerializeField] private float _spinNoiseSpeed = 0.08f;
|
||||
|
||||
[Tooltip("How fast the spin offset interpolates toward its target. Lower = more sluggish/drunk.")]
|
||||
[SerializeField] private float _spinDamping = 2f;
|
||||
|
||||
// ── Inspector — Fade ──────────────────────────────────────────────────────
|
||||
|
||||
[Header("Fade")]
|
||||
[Tooltip("Duration (seconds) of the fade-in / fade-out when toggling the rig.")]
|
||||
[SerializeField] private float _fadeDuration = 0.6f;
|
||||
|
||||
// ── Inspector — Debug ─────────────────────────────────────────────────────
|
||||
|
||||
[Header("Debug")]
|
||||
[SerializeField] private bool _debugSustainStress;
|
||||
[SerializeField] [Range(0f, 1f)] private float _debugStressValue;
|
||||
[SerializeField] private bool _debugOverrideStress;
|
||||
[SerializeField] [Range(0f, 1f)] private float _debugStressOverride;
|
||||
|
||||
// ── Runtime state ─────────────────────────────────────────────────────────
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Current stress level in [0, 1]. Driven by <see cref="StressController"/>.</summary>
|
||||
public float StressLevel { get; set; }
|
||||
/// <summary>
|
||||
/// Current stress level in [0, 1].
|
||||
/// Set this from an external controller (e.g. StressController) each frame,
|
||||
/// or leave it at the inspector default.
|
||||
/// </summary>
|
||||
public float StressLevel
|
||||
{
|
||||
get => _stressLevel;
|
||||
set => _stressLevel = Mathf.Clamp01(value);
|
||||
}
|
||||
|
||||
/// <summary>Whether the rig is currently active (or fading in/out).</summary>
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
// ── Runtime state ─────────────────────────────────────────────────────────
|
||||
|
||||
private float _weight;
|
||||
private float _noiseTimer;
|
||||
private float _initialMasterIntensity; // value set in inspector, restored on Enable()
|
||||
private float _noiseTimer;
|
||||
private float _spinTimer;
|
||||
private Vector3 _noiseOffset;
|
||||
private Vector3 _joltRotation;
|
||||
private Vector3 _inertiaOffset;
|
||||
private Vector3 _spinOffset; // current smoothed spin (Y, Z) rotation
|
||||
private float _smoothMoveT;
|
||||
|
||||
// Smoothed movement blend — avoids amplitude spikes on direction changes.
|
||||
private float _smoothMoveT;
|
||||
|
||||
// Mouse-look speed sampled on OctoPlayerFP (grandparent) — used to gate
|
||||
// micro-jolts so they only fire when the player is not actively looking around.
|
||||
private Quaternion _lastLookRootRotation;
|
||||
private float _lookSpeed;
|
||||
|
||||
@@ -129,65 +164,87 @@ namespace Unheimlich.World.CameraSystem
|
||||
Random.Range(0f, 100f),
|
||||
Random.Range(0f, 100f)
|
||||
);
|
||||
|
||||
// Spin uses its own offset to decouple from the main noise.
|
||||
_spinTimer = Random.Range(0f, 100f);
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Store the inspector value so Enable() can restore it even if the rig
|
||||
// started disabled (and _masterIntensity was faded to 0).
|
||||
_initialMasterIntensity = _masterIntensity;
|
||||
|
||||
// Sync inspector toggle on first frame.
|
||||
IsActive = _isActive;
|
||||
_weight = _isActive ? 1f : 0f;
|
||||
|
||||
Transform lookRoot = transform.parent?.parent;
|
||||
_lastLookRootRotation = lookRoot != null
|
||||
? lookRoot.localRotation
|
||||
: Quaternion.identity;
|
||||
|
||||
if (_isActive)
|
||||
_joltCoroutine = StartCoroutine(JoltCoroutine());
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
// Allow toggling _isActive from the inspector during Play mode.
|
||||
if (!Application.isPlaying) return;
|
||||
SetActive(_isActive);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_debugSustainStress)
|
||||
StressLevel = _debugStressValue;
|
||||
if (_debugOverrideStress)
|
||||
_stressLevel = _debugStressOverride;
|
||||
|
||||
float stress = Mathf.Clamp01(_stressLevel);
|
||||
float intensity = _masterIntensity;
|
||||
|
||||
UpdateInertia();
|
||||
|
||||
if (_weight <= 0f)
|
||||
{
|
||||
UpdateInertia();
|
||||
transform.localRotation = Quaternion.identity;
|
||||
return;
|
||||
}
|
||||
|
||||
float stress = Mathf.Clamp01(StressLevel);
|
||||
|
||||
// ── 1. Resolve movement state ─────────────────────────────────────────
|
||||
// ── 1. Movement state ─────────────────────────────────────────────────
|
||||
|
||||
float horizontalSpeed = 0f;
|
||||
bool isRunning = false;
|
||||
bool running = IsRunning();
|
||||
|
||||
if (Unheimlich.Features.Player.PlayerController.Instance?.CharController != null)
|
||||
{
|
||||
var vel = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
|
||||
var vel = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
|
||||
horizontalSpeed = new Vector3(vel.x, 0f, vel.z).magnitude;
|
||||
isRunning = OctoCore.Components.OctoPlayer.Instance != null
|
||||
&& OctoCore.Components.OctoPlayer.Instance.State.IsRunning;
|
||||
}
|
||||
|
||||
const float WalkSpeedMs = 10f / 3.6f;
|
||||
const float RunSpeedMs = 20f / 3.6f;
|
||||
|
||||
float maxSpeed = isRunning ? RunSpeedMs : WalkSpeedMs;
|
||||
float rawMoveT = Mathf.Clamp01(horizontalSpeed / maxSpeed);
|
||||
float maxSpeed = running ? RunSpeedMs : WalkSpeedMs;
|
||||
float rawMoveT = Mathf.Clamp01(horizontalSpeed / maxSpeed);
|
||||
_smoothMoveT = Mathf.Lerp(_smoothMoveT, rawMoveT, Time.deltaTime * _moveTSmoothSpeed);
|
||||
|
||||
// Smooth moveT to avoid abrupt amplitude jumps on direction changes.
|
||||
_smoothMoveT = Mathf.Lerp(_smoothMoveT, rawMoveT, Time.deltaTime * _moveTSmoothSpeed);
|
||||
// ── 2. Noise parameters ───────────────────────────────────────────────
|
||||
|
||||
// ── 2. Interpolate noise parameters from movement state ───────────────
|
||||
|
||||
float targetAmp = isRunning
|
||||
float targetAmp = running
|
||||
? Mathf.Lerp(_walkAmplitude, _runAmplitude, _smoothMoveT)
|
||||
: Mathf.Lerp(_idleAmplitude, _walkAmplitude, _smoothMoveT);
|
||||
|
||||
float targetSpeed = isRunning
|
||||
float targetSpeed = running
|
||||
? Mathf.Lerp(_walkNoiseSpeed, _runNoiseSpeed, _smoothMoveT)
|
||||
: Mathf.Lerp(_idleNoiseSpeed, _walkNoiseSpeed, _smoothMoveT);
|
||||
|
||||
float amp = (targetAmp + stress * _stressAmplitudeMul) * _weight;
|
||||
float spd = (targetSpeed + stress * _stressSpeedMul);
|
||||
float amp = (targetAmp + stress * _stressAmplitudeMul) * _weight * intensity;
|
||||
float spd = targetSpeed + stress * _stressSpeedMul;
|
||||
|
||||
_noiseTimer += Time.deltaTime * spd;
|
||||
if (_noiseTimer > 1000f) _noiseTimer -= 1000f; // wrap to avoid float precision loss
|
||||
if (_noiseTimer > 1000f) _noiseTimer -= 1000f;
|
||||
|
||||
float t = _noiseTimer;
|
||||
|
||||
// ── 3. Perlin drift ───────────────────────────────────────────────────
|
||||
@@ -198,74 +255,101 @@ namespace Unheimlich.World.CameraSystem
|
||||
|
||||
// ── 4. Inertia swing ──────────────────────────────────────────────────
|
||||
|
||||
UpdateInertia();
|
||||
Vector3 swing = _inertiaOffset * _weight;
|
||||
Vector3 swing = _inertiaOffset * _weight * intensity;
|
||||
|
||||
// ── 5. Compose and apply ──────────────────────────────────────────────
|
||||
// ── 5. Spinning / dizzy drift ─────────────────────────────────────────
|
||||
//
|
||||
// Two independent Perlin channels drive a slow yaw drift (Y) and a
|
||||
// listing roll (Z). Both scale with stress and masterIntensity.
|
||||
// The offset is smoothed via _spinDamping so it feels heavy and sluggish
|
||||
// rather than jittery — like a head that's struggling to stay level.
|
||||
|
||||
_spinTimer += Time.deltaTime * _spinNoiseSpeed;
|
||||
if (_spinTimer > 1000f) _spinTimer -= 1000f;
|
||||
|
||||
float spinScale = stress * _weight * intensity;
|
||||
|
||||
float targetSpinY = (Mathf.PerlinNoise(_noiseOffset.x * 0.5f + _spinTimer, 10f) - 0.5f)
|
||||
* _spinMaxYAngle * 2f * spinScale;
|
||||
|
||||
float targetSpinZ = (Mathf.PerlinNoise(_noiseOffset.z * 0.5f + _spinTimer, 20f) - 0.5f)
|
||||
* _spinMaxZAngle * 2f * spinScale;
|
||||
|
||||
Vector3 targetSpin = new Vector3(0f, targetSpinY, targetSpinZ);
|
||||
_spinOffset = Vector3.Lerp(_spinOffset, targetSpin, Time.deltaTime * _spinDamping);
|
||||
|
||||
// ── 6. Compose and apply ──────────────────────────────────────────────
|
||||
|
||||
transform.localRotation = Quaternion.Euler(
|
||||
rx + _joltRotation.x + swing.x,
|
||||
ry + _joltRotation.y + swing.y,
|
||||
rz + _joltRotation.z + swing.z
|
||||
ry + _joltRotation.y + swing.y + _spinOffset.y,
|
||||
rz + _joltRotation.z + swing.z + _spinOffset.z
|
||||
);
|
||||
}
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>UnityEvent-friendly wrapper — enables with the inspector master intensity.</summary>
|
||||
public void Enable() => Enable(_initialMasterIntensity, _stressLevel);
|
||||
|
||||
/// <summary>UnityEvent-friendly wrapper — disables with a fade to 0.</summary>
|
||||
public void Disable() => Disable(0f);
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the lost-footage effect with a smooth fade.
|
||||
/// Safe to call multiple times — an in-progress fade is cancelled and reversed.
|
||||
/// Enables the rig and lerps <see cref="_masterIntensity"/> toward
|
||||
/// <paramref name="targetIntensity"/> and <see cref="_stressLevel"/> toward
|
||||
/// <paramref name="targetStress"/> over <see cref="_fadeDuration"/> seconds.
|
||||
/// Pass the values you want to reach — the current inspector values are left
|
||||
/// untouched if you don't call this.
|
||||
/// </summary>
|
||||
public void SetActive(bool active)
|
||||
public void Enable(float targetIntensity, float targetStress = 0f)
|
||||
{
|
||||
if (IsActive == active)
|
||||
return;
|
||||
_isActive = true;
|
||||
IsActive = true;
|
||||
|
||||
IsActive = active;
|
||||
if (_fadeCoroutine != null) StopCoroutine(_fadeCoroutine);
|
||||
_fadeCoroutine = StartCoroutine(FadeCoroutine(true, targetIntensity, targetStress));
|
||||
|
||||
if (_fadeCoroutine != null)
|
||||
StopCoroutine(_fadeCoroutine);
|
||||
|
||||
_fadeCoroutine = StartCoroutine(FadeCoroutine(active));
|
||||
|
||||
if (active)
|
||||
{
|
||||
if (_joltCoroutine != null)
|
||||
StopCoroutine(_joltCoroutine);
|
||||
|
||||
_joltCoroutine = StartCoroutine(JoltCoroutine());
|
||||
}
|
||||
if (_joltCoroutine != null) StopCoroutine(_joltCoroutine);
|
||||
_joltCoroutine = StartCoroutine(JoltCoroutine());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the rig, lerping <see cref="_masterIntensity"/> toward
|
||||
/// <paramref name="targetIntensity"/> (default 0) over <see cref="_fadeDuration"/> seconds.
|
||||
/// </summary>
|
||||
public void Disable(float targetIntensity = 0f)
|
||||
{
|
||||
_isActive = false;
|
||||
IsActive = false;
|
||||
|
||||
if (_fadeCoroutine != null) StopCoroutine(_fadeCoroutine);
|
||||
_fadeCoroutine = StartCoroutine(FadeCoroutine(false, targetIntensity, _stressLevel));
|
||||
}
|
||||
|
||||
/// <summary>Backwards-compatible toggle — calls Enable() or Disable().</summary>
|
||||
public void SetActive(bool active) { if (active) Enable(); else Disable(); }
|
||||
|
||||
// ── Private helpers ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Drives inertia swing from CharController velocity projected into local camera space,
|
||||
/// avoiding the rotation-delta approach which caused spikes whenever CameraHeadBob
|
||||
/// changed phase. Also samples OctoPlayerFP (grandparent) rotation delta to measure
|
||||
/// true mouse-look speed for jolt gating.
|
||||
/// </summary>
|
||||
private void UpdateInertia()
|
||||
{
|
||||
// ── Inertia from CharController velocity ──────────────────────────────
|
||||
|
||||
Vector3 velocity = Vector3.zero;
|
||||
|
||||
if (Unheimlich.Features.Player.PlayerController.Instance?.CharController != null)
|
||||
velocity = Unheimlich.Features.Player.PlayerController.Instance.CharController.velocity;
|
||||
|
||||
// Project velocity into local camera space so the swing feels attached to the view.
|
||||
Vector3 localVel = transform.parent != null
|
||||
Vector3 localVel = transform.parent != null
|
||||
? transform.parent.InverseTransformDirection(velocity)
|
||||
: velocity;
|
||||
|
||||
Vector3 targetInertia = new Vector3(-localVel.z, 0f, localVel.x) * (_inertiaStrength * 0.01f);
|
||||
targetInertia = Vector3.ClampMagnitude(targetInertia, _inertiaMaxAngle);
|
||||
Vector3 targetInertia = new Vector3(-localVel.z, 0f, localVel.x)
|
||||
* (_inertiaStrength * _masterIntensity * 0.01f);
|
||||
targetInertia = Vector3.ClampMagnitude(targetInertia, _inertiaMaxAngle);
|
||||
|
||||
_inertiaOffset = Vector3.Lerp(_inertiaOffset, targetInertia, _inertiaDamping * Time.deltaTime);
|
||||
|
||||
// ── Mouse-look speed from OctoPlayerFP (grandparent) ─────────────────
|
||||
|
||||
// Mouse-look speed from OctoPlayerFP (grandparent) — gates micro-jolts.
|
||||
Transform lookRoot = transform.parent?.parent;
|
||||
Quaternion lookRot = lookRoot != null ? lookRoot.localRotation : Quaternion.identity;
|
||||
Quaternion lookDelta = lookRot * Quaternion.Inverse(_lastLookRootRotation);
|
||||
@@ -277,21 +361,28 @@ namespace Unheimlich.World.CameraSystem
|
||||
_lastLookRootRotation = lookRot;
|
||||
}
|
||||
|
||||
private IEnumerator FadeCoroutine(bool fadeIn)
|
||||
private IEnumerator FadeCoroutine(bool fadeIn, float targetIntensity, float targetStress)
|
||||
{
|
||||
float start = _weight;
|
||||
float target = fadeIn ? 1f : 0f;
|
||||
float elapsed = 0f;
|
||||
float startIntensity = _masterIntensity;
|
||||
float startStress = _stressLevel;
|
||||
float elapsed = 0f;
|
||||
|
||||
if (fadeIn && _weight <= 0f) _weight = 0.001f;
|
||||
|
||||
while (elapsed < _fadeDuration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
_weight = Mathf.Lerp(start, target, elapsed / _fadeDuration);
|
||||
elapsed += Time.deltaTime;
|
||||
float t = elapsed / _fadeDuration;
|
||||
_masterIntensity = Mathf.Lerp(startIntensity, targetIntensity, t);
|
||||
_stressLevel = Mathf.Lerp(startStress, targetStress, t);
|
||||
_weight = _masterIntensity > 0f ? 1f : 0f;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_weight = target;
|
||||
_fadeCoroutine = null;
|
||||
_masterIntensity = targetIntensity;
|
||||
_stressLevel = targetStress;
|
||||
_weight = targetIntensity > 0f ? 1f : 0f;
|
||||
_fadeCoroutine = null;
|
||||
|
||||
if (!fadeIn && _joltCoroutine != null)
|
||||
{
|
||||
@@ -304,14 +395,14 @@ namespace Unheimlich.World.CameraSystem
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
float stress = Mathf.Clamp01(StressLevel);
|
||||
float stress = Mathf.Clamp01(_stressLevel);
|
||||
bool running = IsRunning();
|
||||
|
||||
// Use the same smoothed moveT as Update to keep jolt parameters consistent.
|
||||
float joltAmp = isRunning()
|
||||
float joltAmp = running
|
||||
? Mathf.Lerp(_walkJoltAmplitude, _runJoltAmplitude, _smoothMoveT)
|
||||
: Mathf.Lerp(_idleJoltAmplitude, _walkJoltAmplitude, _smoothMoveT);
|
||||
|
||||
Vector2 intervalRange = isRunning()
|
||||
Vector2 intervalRange = running
|
||||
? Vector2.Lerp(_walkJoltIntervalRange, _runJoltIntervalRange, _smoothMoveT)
|
||||
: Vector2.Lerp(_idleJoltIntervalRange, _walkJoltIntervalRange, _smoothMoveT);
|
||||
|
||||
@@ -320,10 +411,8 @@ namespace Unheimlich.World.CameraSystem
|
||||
|
||||
yield return new WaitForSeconds(interval);
|
||||
|
||||
// Jolts only fire when the camera is steady — steadyFactor is 1 when still,
|
||||
// 0 when the player is actively looking around.
|
||||
float steadyFactor = 1f - Mathf.Clamp01(_lookSpeed / _joltLookSuppressSpeed);
|
||||
float ampScale = (1f + stress * _stressJoltAmpMul) * _weight * steadyFactor;
|
||||
float ampScale = (1f + stress * _stressJoltAmpMul) * _weight * _masterIntensity * steadyFactor;
|
||||
|
||||
Vector3 target = new Vector3(
|
||||
Random.Range(-1f, 1f) * joltAmp * ampScale,
|
||||
@@ -335,17 +424,12 @@ namespace Unheimlich.World.CameraSystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns true if the player is currently running.</summary>
|
||||
private bool isRunning()
|
||||
private bool IsRunning()
|
||||
{
|
||||
return OctoCore.Components.OctoPlayer.Instance != null
|
||||
&& OctoCore.Components.OctoPlayer.Instance.State.IsRunning;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snaps to <paramref name="target"/> in the first fraction of the interval,
|
||||
/// then eases back to zero for the remainder — simulating a hand recoil.
|
||||
/// </summary>
|
||||
private IEnumerator ApplyJolt(Vector3 target, float totalDuration)
|
||||
{
|
||||
float snapDur = totalDuration * _joltSnapFraction;
|
||||
|
||||
@@ -1,74 +1,39 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
|
||||
namespace MonoLimboStudio
|
||||
namespace Unheimlich.VFX
|
||||
{
|
||||
public class LightFlicker : MonoBehaviour
|
||||
{
|
||||
[Header("Light Settings")]
|
||||
public Light targetLight;
|
||||
|
||||
[Tooltip("Base intensity set externally (e.g. by ScarecrowFire). Flicker oscillates around this value.")]
|
||||
public float baseIntensity = 1f;
|
||||
[Tooltip("Flicker oscillates around this value. Driven each frame by ScarecrowFire.")]
|
||||
public float baseIntensity = 0f;
|
||||
|
||||
[Tooltip("Flicker range applied relative to baseIntensity (e.g. 0.3 = ±30%).")]
|
||||
[Range(0f, 1f)]
|
||||
public float flickerRange = 0.3f;
|
||||
|
||||
[Header("Perlin Flicker (smooth)")]
|
||||
[Range(0f, 1f)] public float flickerRange = 0.3f;
|
||||
public float flickerSpeed = 5f;
|
||||
|
||||
[Header("Glitch Flicker (bursts)")]
|
||||
[Range(0f, 1f)] public float glitchChance = 0.15f;
|
||||
public float glitchMinDuration = 0.02f;
|
||||
public float glitchMaxDuration = 0.12f;
|
||||
[Range(0f, 1f)] public float glitchChance = 0.15f;
|
||||
public float glitchMinDuration = 0.02f;
|
||||
public float glitchMaxDuration = 0.12f;
|
||||
|
||||
[Header("Material Settings")]
|
||||
public Renderer targetRenderer;
|
||||
public string opacityProperty = "_Opacity";
|
||||
[Range(0f, 1f)] public float minOpacity = 0.2f;
|
||||
[Range(0f, 1f)] public float maxOpacity = 1.0f;
|
||||
|
||||
private Material _materialInstance;
|
||||
private float _randomOffset;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
private float _randomOffset;
|
||||
|
||||
void Start()
|
||||
{
|
||||
_randomOffset = Random.Range(0f, 1000f);
|
||||
|
||||
if (targetRenderer != null)
|
||||
{
|
||||
_materialInstance = targetRenderer.material;
|
||||
|
||||
if (!_materialInstance.HasProperty(opacityProperty))
|
||||
Debug.LogWarning($"Property '{opacityProperty}' not found on material.");
|
||||
}
|
||||
|
||||
StartCoroutine(FlickerRoutine());
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Sets the base intensity that the flicker oscillates around.
|
||||
/// Called each frame by ScarecrowFire as the fire grows.
|
||||
/// </summary>
|
||||
public void SetBaseIntensity(float value)
|
||||
{
|
||||
baseIntensity = value;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// <summary>Called each frame by ScarecrowFire to update the base the flicker oscillates around.</summary>
|
||||
public void SetBaseIntensity(float value) => baseIntensity = value;
|
||||
|
||||
private IEnumerator FlickerRoutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
float perlinValue = Mathf.PerlinNoise(Time.time * flickerSpeed + _randomOffset, 0f);
|
||||
ApplyFlicker(perlinValue);
|
||||
float t = Mathf.PerlinNoise(Time.time * flickerSpeed + _randomOffset, 0f);
|
||||
ApplyFlicker(t);
|
||||
|
||||
if (Random.value < glitchChance)
|
||||
yield return StartCoroutine(GlitchBurst());
|
||||
@@ -79,8 +44,8 @@ namespace MonoLimboStudio
|
||||
|
||||
private IEnumerator GlitchBurst()
|
||||
{
|
||||
float duration = Random.Range(glitchMinDuration, glitchMaxDuration);
|
||||
float elapsed = 0f;
|
||||
float duration = Random.Range(glitchMinDuration, glitchMaxDuration);
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
@@ -90,26 +55,12 @@ namespace MonoLimboStudio
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies flicker as an offset around baseIntensity rather than
|
||||
/// absolute min/max, so external intensity control stays intact.
|
||||
/// </summary>
|
||||
private void ApplyFlicker(float t)
|
||||
{
|
||||
if (targetLight != null)
|
||||
{
|
||||
float offset = Mathf.Lerp(-flickerRange, flickerRange, t);
|
||||
targetLight.intensity = Mathf.Max(0f, baseIntensity + offset);
|
||||
}
|
||||
|
||||
if (_materialInstance != null && _materialInstance.HasProperty(opacityProperty))
|
||||
_materialInstance.SetFloat(opacityProperty, Mathf.Lerp(minOpacity, maxOpacity, t));
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (_materialInstance != null)
|
||||
Destroy(_materialInstance);
|
||||
if (targetLight == null) return;
|
||||
float offset = Mathf.Lerp(-baseIntensity * flickerRange,
|
||||
baseIntensity * flickerRange, t);
|
||||
targetLight.intensity = Mathf.Max(0f, baseIntensity + offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user