using System.Reflection; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using Unheimlich.Features.Player; namespace Unheimlich.World.HouseOfLeaves { /// /// 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. /// 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); } /// /// 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). /// 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 /// Current corridor progression in [0, 1]. 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 } }