/// <summary>
 /// Returns a linear interpolation of two FencingCameraStates.
 /// <para>Performs Mathf.Lerp on each member of the given args and returns a new FencingCameraState with the results of those lerps as members.</para>
 /// </summary>
 /// <param name="from"></param>
 /// <param name="to"></param>
 /// <param name="lerpValue"></param>
 /// <returns></returns>
 static public FencingCameraState Lerp(FencingCameraState from, FencingCameraState to, float lerpValue)
 {
     return(new FencingCameraState
     {
         fixedAngle = Mathf.Lerp(from.fixedAngle, to.fixedAngle, lerpValue),
         positionBias = Mathf.Lerp(from.positionBias, to.positionBias, lerpValue),
         pitch = Mathf.Lerp(from.pitch, to.pitch, lerpValue),
         verticalOffset = Mathf.Lerp(from.verticalOffset, to.verticalOffset, lerpValue)
     });
 }
    /// <summary>
    /// Computes what the state of the cameraRig should be and sends the values to cameraRig.SetCameraRigState()
    /// <para>
    /// The cameraRig is positioned at a point on the line between the two fighters, with a fixed angle
    /// relative to that line.
    /// </para>
    /// <para>
    /// The distance of the camera from the rig's origin is calculated so that both fighters are always in frame.
    /// </para>
    /// </summary>
    void UpdateCamera(FencingCameraState cameraState)
    {
        // remap positionBias from (-1,1) range to (0,1) range for lerp
        float u = (cameraState.positionBias + 1) / 2;

        // set rig position, between the player and the opponent, according to the positionBias that's been remapped above
        rigPos    = Vector3.Lerp(fencingTarget.transform.position, transform.position, u);
        rigPos.y += cameraState.verticalOffset;

        // since cameraFixedAngle is relative to the line between the two fighters, we need to add the angle that line makes
        //      with the world z axis to get the angle in "world space"
        float adjustedAngle = Vector3.SignedAngle(Vector3.forward, fencingTarget.transform.position - transform.position, Vector3.up) + cameraState.fixedAngle;

        // set te rig rotation
        rigRot = new Vector3(cameraState.pitch, adjustedAngle, 0);

        // we need to first find the width we want the camera frustum to be to frame both the fighters.
        //
        // to find this we get the dot product of both their positions (relative to the camera rig position) with
        //      the cameraRig.horizontalRight vector. this gives us the distance of each fighter to the center of
        //      the screen.
        //
        // we take the biggest result of those two distances, double it, add an optional margin and we that gives
        //      us the width we want the frustum to be.
        //
        float dot1  = Mathf.Abs(Vector3.Dot(transform.position - cameraRig.transform.position, cameraRig.horizontalRight));
        float dot2  = Mathf.Abs(Vector3.Dot(fencingTarget.transform.position - cameraRig.transform.position, cameraRig.horizontalRight));
        float width = (Mathf.Max(dot1, dot2) + cameraMargin) * 2;


        // once we have the desired width, we apply this formula, found on unity's docs, to find the distance the camera
        //      must be at for the frustum to be a given width : https://docs.unity3d.com/Manual/FrustumSizeAtDistance.html
        float distance = ((width / cameraRig.cameraAspect) * 0.5f) / Mathf.Tan(cameraRig.cameraFOV * 0.5f * Mathf.Deg2Rad);

        // to ensure that no fighter is behind the camera, we check the dot product of both their relative positions
        //      with the cameraRig.horizontalForward . If one of the two is negative, that means it's behind the
        //      cameraRig center. In that case, we add the absolute value of the negative dot product to the distance.
        dot1 = Vector3.Dot(transform.position - cameraRig.transform.position, cameraRig.horizontalForward);
        dot2 = Vector3.Dot(fencingTarget.transform.position - cameraRig.transform.position, cameraRig.horizontalForward);

        if (dot1 <= 0 || dot2 <= 0)
        {
            distance += Mathf.Abs(Mathf.Min(dot1, dot2));
        }


        // the camera will be moved back by the distance we found
        cameraOffset = Vector3.back * distance;


        // pass the values to the cameraRig
        cameraRig.SetCameraRigState(rigPos, rigRot, cameraOffset);
    }
    public override void ActiveSubControllerUpdate()
    {
        if (canMove)
        {
            transform.Translate(movement * movementSpeed * Time.deltaTime, Space.World);

            if (movement.magnitude > 0.0f)
            {
                animator.SetBool("Moving", true);
                animator.SetFloat("MoveSide", movement.x);
                animator.SetFloat("MoveForward", movement.z);
            }
            else
            {
                animator.SetBool("Moving", false);
            }
        }
        else
        {
            animator.SetBool("Moving", false);
        }

        // face the enemy
        transform.LookAt(fencingTarget.transform, Vector3.up);

        #region Attacking Logic
        // we dedetermine if we're attacking based on the raw attack value, not the procesed attack value
        // (is this intentional? is this motivated? i don't remember ¯\_(ツ)_/¯ )
        if (rawAttackValue > attackStartedThreshold && !attacking)
        {
            attacking = true;
        }
        if (rawAttackValue < attackStartedThreshold && !attackRecovering)
        {
            attacking = false;
        }


        // process the attack value. the function does some mandatory processing but extra processing is determined by booleans and variables.
        processedAttackValue = ProcessAttackValue(rawAttackValue);

        // not recovering from an attack
        if (!attackRecovering && !outOfRecovery)
        {
            // if the value is passed the threshold for attacking, cache the time of attack and start a recovery "timer"
            if (processedAttackValue >= attackCompletedThreshold)
            {
                attackRecovering    = true;
                attackCompletedTime = Time.time;

                canMove = false;

                animator.SetFloat("Attack", 1);
            }
            // otherwise just assign the value
            else if (attacking)
            {
                animator.SetFloat("Attack", processedAttackValue);
            }
        }

        // ensure we're not in an attack pose when not attacking
        if (!attacking)
        {
            animator.SetFloat("Attack", 0);
        }


        // after recovery period, return to base pose
        // this should be animated ideally
        if (attackRecovering && attackCompletedTime + attackRecoveryDuration < Time.time)
        {
            animator.SetFloat("Attack", 0);
            attackRecovering = false;

            canMove = true;

            outOfRecovery = true;
        }

        // only after having released the attack trigger can we be out of recovery and attack again
        if (outOfRecovery && rawAttackValue <= attackStartedThreshold)
        {
            outOfRecovery = false;
            attacking     = false;
        }
        #endregion


        #region Camera Updating

        float fightersDistance = Vector3.Distance(transform.position, fencingTarget.transform.position);

        if (fightersDistance > farCameraStateThreshold)
        {
            UpdateCamera(fencingCameraState_far);
        }

        else if (fightersDistance < nearCameraStateThreshold)
        {
            UpdateCamera(fencingCameraState_near);
        }

        else
        if (useMiddleCameraState)
        {
            if (fightersDistance <= middleCameraStateThreshold)
            {
                float u = (fightersDistance - nearCameraStateThreshold) / (middleCameraStateThreshold - nearCameraStateThreshold);
                FencingCameraState lerpedState = FencingCameraState.Lerp(fencingCameraState_near, fencingCameraState_middle, u);
                UpdateCamera(lerpedState);
            }
            if (fightersDistance > middleCameraStateThreshold)
            {
                float u = (fightersDistance - middleCameraStateThreshold) / (farCameraStateThreshold - middleCameraStateThreshold);
                FencingCameraState lerpedState = FencingCameraState.Lerp(fencingCameraState_middle, fencingCameraState_far, u);
                UpdateCamera(lerpedState);
            }
        }
        else
        {
            float u = (fightersDistance - nearCameraStateThreshold) / (farCameraStateThreshold - nearCameraStateThreshold);
            FencingCameraState lerpedState = FencingCameraState.Lerp(fencingCameraState_near, fencingCameraState_far, u);
            UpdateCamera(lerpedState);
        }

        #endregion
    }