diff --git a/Assets/_Content/Scripts/Features/LiminalSpaces/HouseOfLeaves/Runtime/HouseOfLeavesController.cs b/Assets/_Content/Scripts/Features/LiminalSpaces/HouseOfLeaves/Runtime/HouseOfLeavesController.cs index 07994da1..6792073c 100644 --- a/Assets/_Content/Scripts/Features/LiminalSpaces/HouseOfLeaves/Runtime/HouseOfLeavesController.cs +++ b/Assets/_Content/Scripts/Features/LiminalSpaces/HouseOfLeaves/Runtime/HouseOfLeavesController.cs @@ -8,11 +8,13 @@ namespace Unheimlich.World.HouseOfLeaves /// /// Orchestrates the House of Leaves "endless corridor" illusion: /// - The exit is slowly pushed away as the player walks, so the corridor - /// never seems to actually run out (progress feels real, but is deliberately - /// much slower than the player's own walking speed - no speed/FOV trickery involved). - /// - A darkness/vignette effect kicks in if the player turns back towards - /// where they came from, discouraging backtracking. - /// No fixed corridor length is required - this works indefinitely. + /// never seems to actually run out. + /// - A darkness/vignette effect kicks in if the player turns back. + /// - Exposes a normalized Progression value [0,1] for visual effects + /// such as sand, walls, shaders, etc. + /// + /// The corridor itself remains infinite. Progression only represents + /// visual advancement over a configurable distance. /// public class HouseOfLeavesController : MonoBehaviour { @@ -22,38 +24,69 @@ namespace Unheimlich.World.HouseOfLeaves [Header("Endless Corridor (Receding Exit)")] - [Tooltip("The transform representing the visual end of the corridor (exit door, light at the end, etc). " + - "It will be slowly pushed further away as the player walks, to create the illusion of an endless corridor.")] - [SerializeField] private Transform _exit; + [Tooltip( + "The transform representing the visual end of the corridor " + + "(exit door, light at the end, etc).")] + [SerializeField] + private Transform _exit; - [Tooltip("Fraction of each meter walked that pushes the exit further away. " + - "0 = exit never recedes (normal corridor). Close to 1 = exit recedes almost as fast as the player walks " + - "(barely any felt progress). Keep this comfortably below 1 so the player still senses real progress, just a lot slower than expected.")] - [SerializeField, Range(0f, 0.95f)] private float _recedeFactor = 0.4f; + [Tooltip( + "Fraction of each meter walked that pushes the exit further away. " + + "0 = exit never recedes. Close to 1 = exit recedes almost as fast " + + "as the player walks.")] + [SerializeField, Range(0f, 0.95f)] + private float _recedeFactor = 0.4f; + + [Tooltip( + "How slowly the exit catches up to its receded target, in seconds.")] + [SerializeField, Min(0.01f)] + private float _recedeSmoothTime = 2f; + + + [Header("Visual Progression")] + + [Tooltip( + "Distance in meters required for visual effects to reach " + + "Progression = 1. The corridor itself remains infinite.")] + [SerializeField, Min(0.01f)] + private float _progressionDistance = 50f; - [Tooltip("How slowly the exit's position catches up to its receded target, in seconds. " + - "Higher values make the shift smoother and harder to consciously notice.")] - [SerializeField] private float _recedeSmoothTime = 2f; [Header("Darkness (Turning Back)")] - [SerializeField] private Volume _postProcessVolume; - [SerializeField] private float _turningBackAngle = 100f; - [SerializeField] [Range(0f, 1f)] private float _maxVignetteIntensity = 0.6f; - [SerializeField] private float _maxExposureReduction = -3f; - [SerializeField] private float _darknessLerpSpeed = 2f; + + [SerializeField] + private Volume _postProcessVolume; + + [SerializeField] + private float _turningBackAngle = 100f; + + [SerializeField, Range(0f, 1f)] + private float _maxVignetteIntensity = 0.6f; + + [SerializeField] + private float _maxExposureReduction = -3f; + + [SerializeField] + private float _darknessLerpSpeed = 2f; + // ───────────────────────────────────────────────────────────────────────── // Private Fields // ───────────────────────────────────────────────────────────────────────── - private Vignette _vignette; + private Vignette _vignette; private ColorAdjustments _colorAdjustments; private Vector3 _lastPlayerPosition; - private float _distanceTravelled; // Unbounded - there is no fixed corridor length anymore. + + // Unbounded distance travelled through the corridor. + private float _distanceTravelled; + private Vector3 _lightForwardDirection; - private Vector3 _exitRestPosition; // The exit's original position, used as the recede baseline. - private bool _isInitialized; + private Vector3 _exitRestPosition; + + private bool _isInitialized; + // ───────────────────────────────────────────────────────────────────────── // Unity Messages @@ -61,25 +94,29 @@ namespace Unheimlich.World.HouseOfLeaves private void Start() { - if (!TryResolvePlayerReferences()) return; + if (!TryResolvePlayerReferences()) + return; InitializeLightDirection(); InitializePostProcess(); InitializeExit(); _lastPlayerPosition = PlayerController.Instance.transform.position; - _isInitialized = true; + + _isInitialized = true; } private void Update() { - if (!_isInitialized) return; + if (!_isInitialized) + return; TrackDistance(); TickExitRecede(); TickDarkness(); } + // ───────────────────────────────────────────────────────────────────────── // Initialization // ───────────────────────────────────────────────────────────────────────── @@ -88,7 +125,10 @@ namespace Unheimlich.World.HouseOfLeaves { if (PlayerController.Instance == null) { - Debug.LogError("[HouseOfLeavesController] PlayerController.Instance not found."); + Debug.LogError( + "[HouseOfLeavesController] PlayerController.Instance not found." + ); + return false; } @@ -96,22 +136,28 @@ namespace Unheimlich.World.HouseOfLeaves } /// - /// The corridor's reference direction is simply wherever the player is facing when this - /// controller starts - no manual light/reference transform needed. This becomes both the - /// "forward" axis used to detect turning back, and the axis the exit recedes along. + /// Uses the player's initial facing direction as the corridor's forward axis. /// private void InitializeLightDirection() { - _lightForwardDirection = PlayerController.Instance.transform.forward; + _lightForwardDirection = + PlayerController.Instance.transform.forward; + _lightForwardDirection.y = 0f; - _lightForwardDirection.Normalize(); + + if (_lightForwardDirection.sqrMagnitude > 0f) + _lightForwardDirection.Normalize(); } private void InitializePostProcess() { if (_postProcessVolume == null) { - Debug.LogWarning("[HouseOfLeavesController] No URP Volume assigned — darkness effect disabled."); + Debug.LogWarning( + "[HouseOfLeavesController] No URP Volume assigned — " + + "darkness effect disabled." + ); + return; } @@ -123,116 +169,220 @@ namespace Unheimlich.World.HouseOfLeaves { if (_exit == null) { - Debug.LogWarning("[HouseOfLeavesController] No Exit transform assigned — endless-corridor recede disabled."); + Debug.LogWarning( + "[HouseOfLeavesController] No Exit transform assigned — " + + "endless-corridor recede disabled." + ); + return; } _exitRestPosition = _exit.position; } + // ───────────────────────────────────────────────────────────────────────── // Distance Tracking // ───────────────────────────────────────────────────────────────────────── private void TrackDistance() { - Vector3 currentPosition = PlayerController.Instance.transform.position; - Vector3 delta = currentPosition - _lastPlayerPosition; - delta.y = 0f; + Vector3 currentPosition = + PlayerController.Instance.transform.position; - // No clamp here on purpose: the corridor has no defined length, so this can grow forever. - _distanceTravelled += Vector3.Dot(delta, _lightForwardDirection); + Vector3 delta = + currentPosition - _lastPlayerPosition; + + delta.y = 0f; + + // Signed distance along the corridor direction. + _distanceTravelled += + Vector3.Dot(delta, _lightForwardDirection); + + // Prevent the visual progression from becoming negative if + // the player walks backwards past the starting point. + _distanceTravelled = + Mathf.Max(0f, _distanceTravelled); _lastPlayerPosition = currentPosition; } + // ───────────────────────────────────────────────────────────────────────── // Tick Methods // ───────────────────────────────────────────────────────────────────────── - /// - /// Pushes the exit further away along the corridor direction, proportionally to how far - /// the player has walked. Because _recedeFactor is less than 1, the exit still slowly - /// gets closer overall - just much slower than the player's actual walking speed - which - /// is what sells the "this corridor never seems to end" feeling without ever touching - /// the player's movement speed or FOV. - /// private void TickExitRecede() { - if (_exit == null) return; + if (_exit == null) + return; - float recedeDistance = _distanceTravelled * _recedeFactor; - Vector3 targetPosition = _exitRestPosition + _lightForwardDirection * recedeDistance; + float recedeDistance = + _distanceTravelled * _recedeFactor; - // Smoothly drift towards the target rather than snapping, so the shift is never perceptible - // as a sudden jump - it should read as "the corridor is just long", not as a visible trick. - float t = Time.deltaTime / Mathf.Max(_recedeSmoothTime, 0.01f); - _exit.position = Vector3.Lerp(_exit.position, targetPosition, t); + Vector3 targetPosition = + _exitRestPosition + + _lightForwardDirection * recedeDistance; + + float t = + Time.deltaTime / + Mathf.Max(_recedeSmoothTime, 0.01f); + + _exit.position = + Vector3.Lerp( + _exit.position, + targetPosition, + t + ); } private void TickDarkness() { - if (_vignette == null && _colorAdjustments == null) return; + if (_vignette == null && + _colorAdjustments == null) + { + return; + } + + Vector3 playerForward = + PlayerController.Instance.transform.forward; - Vector3 playerForward = PlayerController.Instance.transform.forward; playerForward.y = 0f; - playerForward.Normalize(); - float dot = Vector3.Dot(playerForward, _lightForwardDirection); - float angleDegrees = Mathf.Acos(Mathf.Clamp(dot, -1f, 1f)) * Mathf.Rad2Deg; + if (playerForward.sqrMagnitude > 0f) + playerForward.Normalize(); - float darknessT = Mathf.Clamp01( - (angleDegrees - _turningBackAngle) / (180f - _turningBackAngle) - ); + float dot = + Vector3.Dot( + playerForward, + _lightForwardDirection + ); - float lerpDelta = Time.deltaTime * _darknessLerpSpeed; + float angleDegrees = + Mathf.Acos( + Mathf.Clamp(dot, -1f, 1f) + ) * Mathf.Rad2Deg; + + float darknessT = + Mathf.Clamp01( + (angleDegrees - _turningBackAngle) / + Mathf.Max( + 180f - _turningBackAngle, + 0.01f + ) + ); + + float lerpDelta = + Time.deltaTime * _darknessLerpSpeed; if (_vignette != null) - _vignette.intensity.value = Mathf.Lerp(_vignette.intensity.value, _maxVignetteIntensity * darknessT, lerpDelta); + { + _vignette.intensity.value = + Mathf.Lerp( + _vignette.intensity.value, + _maxVignetteIntensity * darknessT, + lerpDelta + ); + } if (_colorAdjustments != null) - _colorAdjustments.postExposure.value = Mathf.Lerp(_colorAdjustments.postExposure.value, _maxExposureReduction * darknessT, lerpDelta); + { + _colorAdjustments.postExposure.value = + Mathf.Lerp( + _colorAdjustments.postExposure.value, + _maxExposureReduction * darknessT, + lerpDelta + ); + } } + // ───────────────────────────────────────────────────────────────────────── // Public API // ───────────────────────────────────────────────────────────────────────── - /// Total distance the player has walked along the corridor direction (unbounded). - public float DistanceTravelled => _distanceTravelled; + /// + /// Total distance travelled along the corridor direction. + /// + public float DistanceTravelled => + _distanceTravelled; + /// + /// Normalized visual progression. + /// + /// 0 = start of the corridor. + /// 1 = progression distance has been reached. + /// + /// This value is clamped to [0,1] even though DistanceTravelled + /// can continue increasing indefinitely. + /// + public float Progression => + Mathf.Clamp01( + _distanceTravelled / + Mathf.Max(_progressionDistance, 0.01f) + ); + + /// + /// Resets corridor progression and the receding exit. + /// public void ResetProgression() { - _distanceTravelled = 0f; - _lastPlayerPosition = PlayerController.Instance.transform.position; + _distanceTravelled = 0f; + + if (PlayerController.Instance != null) + { + _lastPlayerPosition = + PlayerController.Instance.transform.position; + } if (_exit != null) - _exit.position = _exitRestPosition; + { + _exit.position = + _exitRestPosition; + } } + /// + /// Overrides the corridor forward direction. + /// public void SetLightDirection(Vector3 worldDirection) { - _lightForwardDirection = worldDirection; - _lightForwardDirection.y = 0f; - _lightForwardDirection.Normalize(); + worldDirection.y = 0f; + + if (worldDirection.sqrMagnitude <= 0f) + return; + + _lightForwardDirection = + worldDirection.normalized; } + // ───────────────────────────────────────────────────────────────────────── // Editor // ───────────────────────────────────────────────────────────────────────── #if UNITY_EDITOR + private void OnDrawGizmosSelected() { Gizmos.color = Color.yellow; - Gizmos.DrawRay(transform.position + Vector3.up, _lightForwardDirection * 4f); + + Gizmos.DrawRay( + transform.position + Vector3.up, + _lightForwardDirection * 4f + ); if (_exit != null) { Gizmos.color = Color.cyan; - Gizmos.DrawWireSphere(_exit.position, 0.5f); + + Gizmos.DrawWireSphere( + _exit.position, + 0.5f + ); } } + #endif } -} \ No newline at end of file +} diff --git a/Residency_VisualDemo_TEST.slnx b/Residency_VisualDemo_TEST.slnx new file mode 100644 index 00000000..d304bbaa --- /dev/null +++ b/Residency_VisualDemo_TEST.slnx @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + +