Code/Core/CameraSystem.cs
using System.Collections.Generic;
using Sandbox;
using Sandbox.Utility;

namespace MANIFOLD.Camera {
    /// <summary>
    /// The actual brain of the camera system.
    /// </summary>
    public sealed class CameraSystem : GameObjectSystem {
        private CameraBrain mainCameraBrain;
        private bool cameraStackDirty;
        private LinkedList<VirtualCamera> cameraStack;

        private VirtualCamera transitionFrom;
        private VirtualCamera transitionTo;

        private bool inTransition;
        private bool reverseTransition;
        private float currentTransitionTimer;
        private float currentTransitionElapsed;
        private TransitionData currentTransitionData;

        public CameraBrain Brain => mainCameraBrain;
        public VirtualCamera LastCamera => transitionFrom;
        public VirtualCamera CurrentCamera => transitionTo;
        
        public CameraSystem(Scene scene) : base(scene) {
            cameraStack = new LinkedList<VirtualCamera>();
            
            Listen(Stage.FinishUpdate, 100, CameraUpdate, "camera.update");
        }
        
        private void CameraUpdate() {
            if (!FindBrain()) return;
            
            if (Scene.IsEditor) {
                if (!mainCameraBrain.UpdateInEditor) {
                    return;
                }
            }

            if (cameraStackDirty) {
                OnStackEdit();
            }

            if (!transitionTo.IsValid()) return;
            GetCameraTransform(transitionTo, out Vector3 toPos, out Rotation toRot);
            
            if (!Scene.IsEditor && inTransition) {
                float linearFactor = currentTransitionTimer / currentTransitionData.Duration;
                float easedFactor = 0f;

                {
                    float offset = currentTransitionElapsed / currentTransitionData.Duration;
                    float evalTime = linearFactor.Remap(offset, 1);

                    if (currentTransitionData.Mode == TransitionMode.Predefined) {
                        var func = Easing.GetFunction(currentTransitionData.EaseFunction.ToString());
                        easedFactor = func(evalTime) + currentTransitionElapsed;
                    } else if (currentTransitionData.Mode == TransitionMode.Curve) {
                        easedFactor = currentTransitionData.EaseCurve.Evaluate(evalTime);
                    }

                    easedFactor = easedFactor.Remap(0, 1, offset);
                }

                GetCameraTransform(transitionFrom, out Vector3 fromPos, out Rotation fromRot);
                mainCameraBrain.WorldPosition = Vector3.Lerp(fromPos, toPos, easedFactor);
                mainCameraBrain.WorldRotation = Rotation.Slerp(fromRot, toRot, easedFactor);
                mainCameraBrain.Camera.FieldOfView = MathX.Lerp(GetCameraFOV(transitionFrom), GetCameraFOV(transitionTo), easedFactor);

                currentTransitionTimer += mainCameraBrain.UseRealTime ? RealTime.Delta : Time.Delta;
                if (currentTransitionTimer >= currentTransitionData.Duration) {
                    currentTransitionTimer = currentTransitionData.Duration;
                    inTransition = false;
                }
            } else {
                mainCameraBrain.WorldPosition = toPos;
                mainCameraBrain.WorldRotation = toRot;
                mainCameraBrain.Camera.FieldOfView = GetCameraFOV(transitionTo);
            }
        }

        private void GetCameraTransform(VirtualCamera cam, out Vector3 pos, out Rotation rot) {
            cam.DoExtensionUpdate(out Vector3 localPos, out Rotation localRot);
            pos = cam.WorldPosition + (localPos * cam.WorldRotation);
            rot = cam.WorldRotation * localRot;
        }

        private float GetCameraFOV(VirtualCamera cam) {
            if (cam.FOVMode == FOVMode.Vertical) return (2 * float.Atan(float.Tan(cam.FieldOfView.DegreeToRadian() / 2) * Screen.Aspect)).RadianToDegree();
            return cam.FieldOfView;
        }
        
        public void ActivateCamera(VirtualCamera newCamera, bool updateNow = false) {
            if (!mainCameraBrain.IsValid()) {
                Log.Warning($"Tried to activate virtual camera '${newCamera.GameObject.Name}' but there is no brain in the scene.");
            }
            
            VirtualCamera highestCamera = null;
            foreach (VirtualCamera camera in cameraStack) {
                if (camera.Priority <= newCamera.Priority) {
                    highestCamera = camera;
                }
            }

            if (highestCamera != null) {
                cameraStack.AddAfter(cameraStack.Find(highestCamera), newCamera);
            } else {
                cameraStack.AddLast(newCamera);
            }

            
            cameraStackDirty = true;
            if (updateNow) {
                OnStackEdit();
            }
        }

        public void DeactivateCamera(VirtualCamera camera, bool updateNow = false) {
            cameraStack.Remove(camera);
            cameraStackDirty = true;
            if (updateNow) {
                OnStackEdit();
            }
        }

        private bool FindBrain() {
            if (mainCameraBrain.IsValid()) return true;
            mainCameraBrain = Scene.Components.GetInDescendants<CameraBrain>();
            return mainCameraBrain.IsValid();
        }
        
        private void OnStackEdit() {
            var lastNode = cameraStack.Last;
            if (lastNode == null) return;

            // var previousNode = lastNode.Previous;
            // if (previousNode != null) {
            //     transitionFrom = previousNode.Value;
            // }

            VirtualCamera newTo = lastNode.Value;
            if (newTo != transitionTo && transitionTo.IsValid()) {
                TransitionData newData = newTo.UseCustomTransition ? newTo.TransitionData : mainCameraBrain.TransitionData;

                if (newData.Mode != TransitionMode.Cut) {
                    inTransition = true;

                    if (newTo == transitionFrom) {
                        float elapsedNorm = 1 - (currentTransitionTimer / currentTransitionData.Duration);
                        currentTransitionTimer = newData.Duration * elapsedNorm;
                        currentTransitionElapsed = newData.AbsoluteEase ? 0f : newData.Duration * elapsedNorm;
                    } else {
                        currentTransitionTimer = 0;
                        currentTransitionElapsed = 0;
                    }
                }
                currentTransitionData = newData;
                transitionFrom = transitionTo;
            }
            transitionTo = newTo;
            cameraStackDirty = false;
        }
    }
}