287 lines
13 KiB
C#
287 lines
13 KiB
C#
using System.Reflection;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEngine.Rendering.Universal;
|
|
using Unheimlich.Features.Player;
|
|
|
|
namespace Unheimlich.World.HouseOfLeaves
|
|
{
|
|
/// <summary>
|
|
/// Orchestrates the core House of Leaves experience.
|
|
/// Script Execution Order: set to -10 so SetBaseFovOverride is written
|
|
/// before CameraEffects.Update consumes it in the same frame.
|
|
/// </summary>
|
|
public class HouseOfLeavesController : MonoBehaviour
|
|
{
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Serialized Fields
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
[Header("Corridor")]
|
|
[Tooltip("Total length of the corridor (meters).")]
|
|
[SerializeField] private float _corridorLength = 100f;
|
|
|
|
[Header("Speed Ramp")]
|
|
[SerializeField] private float _initialWalkSpeed = 10f;
|
|
[SerializeField] private float _maxWalkSpeed = 22f;
|
|
|
|
[Header("FOV Ramp")]
|
|
[SerializeField] private float _initialFov = 70f;
|
|
[SerializeField] private float _minFov = 45f;
|
|
[SerializeField] private float _fovLerpSpeed = 2f;
|
|
|
|
[Header("Darkness")]
|
|
[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;
|
|
|
|
[Header("Light Source")]
|
|
[SerializeField] private Transform _lightSourceReference;
|
|
|
|
[Header("Fracture")]
|
|
[SerializeField] private Material _fractureMaterial;
|
|
[SerializeField] private AnimationCurve _fractureCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Private Fields
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private OctoCore.Components.OctoPlayer _octoPlayer;
|
|
private Camera _playerCamera;
|
|
private CameraEffects _cameraEffects;
|
|
|
|
private FieldInfo _settingsFieldInfo;
|
|
private FieldInfo _walkSpeedFieldInfo;
|
|
private object _settingsInstance;
|
|
|
|
private Vignette _vignette;
|
|
private ColorAdjustments _colorAdjustments;
|
|
|
|
private Vector3 _lastPlayerPosition;
|
|
private float _distanceTravelled;
|
|
private float _currentFov;
|
|
private Vector3 _lightForwardDirection;
|
|
private bool _isInitialized;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Unity Messages
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private void Start()
|
|
{
|
|
if (!TryResolvePlayerReferences()) return;
|
|
|
|
InitializeSpeedReflection();
|
|
InitializeFov();
|
|
InitializeLightDirection();
|
|
InitializePostProcess();
|
|
|
|
_lastPlayerPosition = PlayerController.Instance.transform.position;
|
|
_isInitialized = true;
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!_isInitialized) return;
|
|
|
|
TrackDistance();
|
|
|
|
float t = Mathf.Clamp01(_distanceTravelled / _corridorLength);
|
|
|
|
TickSpeed(t);
|
|
TickFov(t);
|
|
TickDarkness();
|
|
TickFracture(t);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Initialization
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private bool TryResolvePlayerReferences()
|
|
{
|
|
if (PlayerController.Instance == null)
|
|
{
|
|
Debug.LogError("[HouseOfLeavesController] PlayerController.Instance not found.");
|
|
return false;
|
|
}
|
|
|
|
_octoPlayer = PlayerController.Instance.Movement;
|
|
_playerCamera = PlayerController.Instance.PlayerCamera;
|
|
_cameraEffects = PlayerController.Instance.CameraEffects;
|
|
|
|
if (_octoPlayer == null || _playerCamera == null)
|
|
{
|
|
Debug.LogError("[HouseOfLeavesController] OctoPlayer or PlayerCamera could not be resolved.");
|
|
return false;
|
|
}
|
|
|
|
if (_cameraEffects == null)
|
|
Debug.LogWarning("[HouseOfLeavesController] CameraEffects not found — FOV kick sync disabled.");
|
|
|
|
return true;
|
|
}
|
|
|
|
private void InitializeSpeedReflection()
|
|
{
|
|
System.Type playerType = typeof(OctoCore.Components.OctoPlayer);
|
|
|
|
_settingsFieldInfo = playerType.GetField(
|
|
"_settings", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
|
|
if (_settingsFieldInfo == null)
|
|
{
|
|
Debug.LogError("[HouseOfLeavesController] Could not find '_settings' on OctoPlayer via reflection.");
|
|
return;
|
|
}
|
|
|
|
_settingsInstance = _settingsFieldInfo.GetValue(_octoPlayer);
|
|
|
|
_walkSpeedFieldInfo = _settingsInstance.GetType().GetField(
|
|
"WalkSpeed", BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
if (_walkSpeedFieldInfo == null)
|
|
Debug.LogError("[HouseOfLeavesController] Could not find 'WalkSpeed' on OctoPlayer.Settings via reflection.");
|
|
}
|
|
|
|
private void InitializeFov()
|
|
{
|
|
_currentFov = _initialFov;
|
|
_playerCamera.fieldOfView = _initialFov;
|
|
}
|
|
|
|
private void InitializeLightDirection()
|
|
{
|
|
_lightForwardDirection = _lightSourceReference != null
|
|
? _lightSourceReference.forward
|
|
: PlayerController.Instance.transform.forward;
|
|
|
|
_lightForwardDirection.y = 0f;
|
|
_lightForwardDirection.Normalize();
|
|
}
|
|
|
|
private void InitializePostProcess()
|
|
{
|
|
if (_postProcessVolume == null)
|
|
{
|
|
Debug.LogWarning("[HouseOfLeavesController] No URP Volume assigned — darkness effect disabled.");
|
|
return;
|
|
}
|
|
|
|
_postProcessVolume.profile.TryGet(out _vignette);
|
|
_postProcessVolume.profile.TryGet(out _colorAdjustments);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Distance Tracking
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private void TrackDistance()
|
|
{
|
|
Vector3 currentPosition = PlayerController.Instance.transform.position;
|
|
Vector3 delta = currentPosition - _lastPlayerPosition;
|
|
delta.y = 0f;
|
|
|
|
_distanceTravelled += Vector3.Dot(delta, _lightForwardDirection);
|
|
_distanceTravelled = Mathf.Clamp(_distanceTravelled, 0f, _corridorLength);
|
|
|
|
_lastPlayerPosition = currentPosition;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Tick Methods
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
private void TickSpeed(float t)
|
|
{
|
|
if (_walkSpeedFieldInfo == null || _settingsInstance == null) return;
|
|
|
|
float newSpeed = Mathf.Lerp(_initialWalkSpeed, _maxWalkSpeed, t);
|
|
_walkSpeedFieldInfo.SetValue(_settingsInstance, newSpeed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Lerps the FOV and submits it to CameraEffects as the base for this frame's
|
|
/// sprint kick. CameraEffects.Update then does the authoritative fieldOfView write.
|
|
/// This component must run before CameraEffects (Execution Order -10).
|
|
/// </summary>
|
|
private void TickFov(float t)
|
|
{
|
|
float targetFov = Mathf.Lerp(_initialFov, _minFov, t);
|
|
_currentFov = Mathf.Lerp(_currentFov, targetFov, Time.deltaTime * _fovLerpSpeed);
|
|
|
|
// Submit to CameraEffects — it will write fieldOfView = _currentFov + kickOffset.
|
|
// If CameraEffects is absent, write directly as fallback.
|
|
if (_cameraEffects != null)
|
|
_cameraEffects.SetBaseFovOverride(_currentFov);
|
|
else
|
|
_playerCamera.fieldOfView = _currentFov;
|
|
}
|
|
|
|
private void TickDarkness()
|
|
{
|
|
if (_vignette == null && _colorAdjustments == null) return;
|
|
|
|
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;
|
|
|
|
float darknessT = Mathf.Clamp01(
|
|
(angleDegrees - _turningBackAngle) / (180f - _turningBackAngle)
|
|
);
|
|
|
|
float lerpDelta = Time.deltaTime * _darknessLerpSpeed;
|
|
|
|
if (_vignette != null)
|
|
_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);
|
|
}
|
|
|
|
private void TickFracture(float t)
|
|
{
|
|
float fractureT = _fractureCurve.Evaluate(t);
|
|
_fractureMaterial?.SetFloat("_FractureAmount", fractureT);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Public API
|
|
|
|
/// <summary>Current corridor progression in [0, 1].</summary>
|
|
public float Progression => _corridorLength > 0f
|
|
? Mathf.Clamp01(_distanceTravelled / _corridorLength)
|
|
: 0f;
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
public void ResetProgression()
|
|
{
|
|
_distanceTravelled = 0f;
|
|
_lastPlayerPosition = PlayerController.Instance.transform.position;
|
|
}
|
|
|
|
public void SetLightDirection(Vector3 worldDirection)
|
|
{
|
|
_lightForwardDirection = worldDirection;
|
|
_lightForwardDirection.y = 0f;
|
|
_lightForwardDirection.Normalize();
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Editor
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
#if UNITY_EDITOR
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawRay(transform.position + Vector3.up, _lightForwardDirection * 4f);
|
|
}
|
|
#endif
|
|
}
|
|
} |