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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+