#define _SSC_SHIP_DEBUG
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// For Math functions instead of Mathf
using System;

// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
    /// <summary>
    /// Class containing data for a ship.
    /// </summary>
    [System.Serializable]
    public class Ship
    {
        #region Enumerations

        public enum ShipPhysicsModel
        {
            PhysicsBased = 10,
            Arcade = 20
        }

        public enum GroundNormalCalculationMode
        {
            SingleNormal = 10,
            SmoothedNormal = 20
            //AverageHeight = 30
        }

        public enum RollControlMode
        {
            YawInput = 10,
            StrafeInput = 20
        }

        public enum ShipDamageModel
        {
            Simple = 10,
            Progressive = 20,
            Localised = 30
        }

        public enum RespawningMode
        {
            DontRespawn = 10,
            RespawnAtOriginalPosition = 20,
            RespawnAtLastPosition = 30,
            RespawnAtSpecifiedPosition = 40,
            RespawnOnPath = 50
        }

        public enum StuckAction
        {
            DoNothing = 10,
            InvokeCallback = 20,
            RespawnOnPath = 30,
            SameAsRespawningMode = 50
        }

        public enum InputControlAxis
        {
            None = 0,
            X = 1,
            Y = 2
        }

        #endregion

        #region Public Variables and Properties

        // IMPORTANT - when changing this section also update SetClassDefault()
        // Also update ClassName(ClassName className) Clone Constructor (if there is one)

        /// <summary>
        /// If enabled, the InitialiseShip() will be called as soon as Awake() runs for the ship. This should be disabled if you are
        /// instantiating the ship through code.
        /// </summary>
        public bool initialiseOnAwake;

        /// <summary>
        /// What physics model the ship uses. If you modify this, call ReinitialiseShipPhysicsModel().
        /// Physics Based: Physics-based model. Physically realistic thrusters and aerodynamic devices are used to control the ship's motion.
        /// Arcade: Simplified physics model. Some physical effects are simplified and unrealistic effects can be used to enhance gameplay.
        /// </summary>
        public ShipPhysicsModel shipPhysicsModel;

        // Physical characteristics
        /// <summary>
        /// Mass of the ship in kilograms. If you modify this, call ReinitialiseMass() on the ShipControlModule.
        /// </summary>
        public float mass;
        /// <summary>
        /// Centre of mass of the ship in local space. If you modify this, call ReinitialiseMass() on the ShipControlModule.
        /// </summary>
        public Vector3 centreOfMass;
        /// <summary>
        /// If set to true, the centre of mass will be set to the value of centreOfMass. 
        /// Otherwise the centre of mass will be set to the unity default specified by the rigdbody.
        /// NOTE: This value will not affect anything if changed at runtime.
        /// </summary>
        public bool setCentreOfMassManually;

        /// <summary>
        /// Show the centre of mass gizmos in the scene view.
        /// </summary>
        public bool showCOMGizmosInSceneView;

        /// <summary>
        /// Show the centre of lift and lift direction gizmos in the scene view.
        /// </summary>
        public bool showCOLGizmosInSceneView;

        /// <summary>
        /// Show the centre of forwards thrust gizmos in the scene view.
        /// </summary>
        public bool showCOTGizmosInSceneView;

        /// <summary>
        /// List of all thruster components for this ship. If you modify this, call ReinitialiseThrusterVariables() and ReinitialiseInputVariables().
        /// </summary>
        public List<Thruster> thrusterList;

        /// <summary>
        /// When the ship is enabled, but ship movement is disabled, should thrusters fire
        /// if they have isMinEffectsAlwaysOn enabled?
        /// </summary>
        public bool isThrusterFXStationary;

        /// <summary>
        /// Are the thruster systems online or in the process of coming online?
        /// At runtime call shipInstance.StartupThrusterSystems() or ShutdownThrusterSystems()
        /// </summary>
        public bool isThrusterSystemsStarted;
        /// <summary>
        /// The time, in seconds, it takes for the thrusters to fully come online
        /// </summary>
        [Range(0f, 60f)] public float thrusterSystemStartupDuration;
        /// <summary>
        /// The time, in seconds, it takes for the thrusters to fully shutdown
        /// </summary>
        [Range(0f, 60f)] public float thrusterSystemShutdownDuration;
        /// <summary>
        /// For thrusters, use a central fuel level, rather than fuel level per thruster.
        /// </summary>
        public bool useCentralFuel;
        /// <summary>
        /// The amount of fuel available when useCentralFuel is true - range 0.0 (empty) to 100.0 (full).
        /// At runtime call shipInstance.SetFuelLevel(..)
        /// </summary>
        [Range(0f, 100f)] public float centralFuelLevel;
        /// <summary>
        /// List of all wing components for this ship. If you modify this, call ReinitialiseWingVariables().
        /// </summary>
        public List<Wing> wingList;
        /// <summary>
        /// List of all control surface components for this ship. If you modify this, call ReinitialiseInputVariables().
        /// </summary>
        public List<ControlSurface> controlSurfaceList;
        /// <summary>
        /// List of all weapon components for this ship. If you modify this, call ReinitialiseWeaponVariables().
        /// </summary>
        public List<Weapon> weaponList;
        /// <summary>
        /// When the ship is enabled, but movement is disabled, weapons and damage are updated
        /// </summary>
        public bool useWeaponsWhenMovementDisabled = false;
        /// <summary>
        /// [READ ONLY] The number of weapons on this ship
        /// </summary>
        public int NumberOfWeapons { get { return weaponList == null ? 0 : weaponList.Count; } }

        /// <summary>
        /// [Internal Use] Is the thruster systems section expanded in the SCM Editor?
        /// </summary>
        public bool isThrusterSystemsExpanded;
        /// <summary>
        /// [Internal Use] Is the thruster list expanded in the SCM Editor?
        /// </summary>
        public bool isThrusterListExpanded;
        /// <summary>
        /// [Internal Use] Is the wing list expanded in the SCM Editor?
        /// </summary>
        public bool isWingListExpanded;
        /// <summary>
        /// [Internal Use] Is the control surface list expanded in the SCM Editor?
        /// </summary>
        public bool isControlSurfaceListExpanded;
        /// <summary>
        /// [Internal Use] Is the weapon list expanded in the SCM Editor?
        /// </summary>
        public bool isWeaponListExpanded;
        /// <summary>
        /// [Internal Use] Is the main damage expanded in the SCM Editor?
        /// </summary>
        public bool isMainDamageExpanded;
        /// <summary>
        /// [Internal Use] Is the local damage list expanded in the SCM Editor?
        /// </summary>
        public bool isDamageListExpanded;
        /// <summary>
        /// Whether the Damge is shown as expanded in the inspector window of the editor.
        /// </summary>
        public bool showDamageInEditor;

        // Aerodynamic properties
        /// <summary>
        /// The drag coefficients in the local x, y and z directions. Increasing the drag coefficients will increase drag.
        /// </summary>
        public Vector3 dragCoefficients;
        /// <summary>
        /// The projected areas (in square metres) of the ship in the local x, y and z planes.
        /// </summary>
        public Vector3 dragReferenceAreas;
        /// <summary>
        /// The centre of drag for moments causing rotation along the local x-axis.
        /// </summary>
        public Vector3 centreOfDragXMoment;
        /// <summary>
        /// The centre of drag for moments causing rotation along the local y-axis.
        /// </summary>
        public Vector3 centreOfDragYMoment;
        /// <summary>
        /// The centre of drag for moments causing rotation along the local z-axis.
        /// </summary>
        public Vector3 centreOfDragZMoment;
        /// <summary>
        /// Multiplier of the effect of angular drag. Only relevant when not in physics-based mode (when in this mode it is set to the physically realistic value of 1).
        /// </summary>
        [Range(0f, 4f)] public float angularDragFactor;
        /// <summary>
        /// When this is enabled, drag moments have no effect (i.e. drag cannot cause the ship to rotate). Only relevant when not in physics-based mode (when in this mode drag moments are always enabled).
        /// </summary>
        public bool disableDragMoments;
        /// <summary>
        /// A multiplier for drag moments causing rotation along the local (pitch) x-axis. Decreasing this will make these moments weaker.
        /// </summary>
        [Range(0f, 1f)] public float dragXMomentMultiplier;
        /// <summary>
        /// A multiplier for drag moments causing rotation along the local (yaw) y-axis. Decreasing this will make these moments weaker.
        /// </summary>
        [Range(0f, 1f)] public float dragYMomentMultiplier;
        /// <summary>
        /// A multiplier for drag moments causing rotation along the local (roll) z-axis. Decreasing this will make these moments weaker.
        /// </summary>
        [Range(0f, 1f)] public float dragZMomentMultiplier;

        /// <summary>
        /// How strong the effect of stalling is on wings (between 0 and 1). Higher values make the effect of stalling more prominent.
        /// </summary>
        [Range(0f, 1f)] public float wingStallEffect;

        /// <summary>
        /// Whether a braking component is enabled in arcade mode.
        /// </summary>
        public bool arcadeUseBrakeComponent;
        /// <summary>
        /// The strength of the brake force in arcade mode. Only relevant when not in physics-based mode and when 
        /// arcadeUseBrakeComponent is enabled.
        /// </summary>
        public float arcadeBrakeStrength;
        /// <summary>
        /// Whether the strength of the brake force ignores the density of the medium the ship is in (assuming it to be a constant
        /// value of one kilogram per cubic metre). Only relevant when not in physics-based mode and when arcadeUseBrakeComponent 
        /// is enabled.
        /// </summary>
        public bool arcadeBrakeIgnoreMediumDensity;
        /// <summary>
        /// The minimum braking acceleration (in metres per second) caused by the brake when the brake is fully engaged. Increase this
        /// value to make the ship come to a stop more quickly at low speeds. Only relevant when not in physics-based mode and when 
        /// arcadeUseBrakeComponent is enabled.
        /// </summary>
        public float arcadeBrakeMinAcceleration;

        // Input modulation
        /// <summary>
        /// Strength of the rotational flight assist. Set to zero to disable.
        /// </summary>
        [Range(0f, 10f)] public float rotationalFlightAssistStrength;
        /// <summary>
        /// Strength of the translational flight assist. Set to zero to disable.
        /// </summary>
        [Range(0f, 10f)] public float translationalFlightAssistStrength;
        /// <summary>
        /// Strength of the stability flight assist. Set to zero to disable.
        /// </summary>
        [Range(0f, 10f)] public float stabilityFlightAssistStrength;
        /// <summary>
        /// Strength of the brake flight assist. Set to zero to disable [Default = 0]
        /// Operates on Forward and Backward movements when there is no ship input.
        /// Is overridden in forwards direction when AutoCruise is enabled on PlayerInputModule.
        /// </summary>
        [Range(0f, 10f)] public float brakeFlightAssistStrength;
        /// <summary>
        /// The effective minimum speed in m/s that the brake flight assist will operate
        /// </summary>
        [Range(-1000f, 1000f)] public float brakeFlightAssistMinSpeed;
        /// <summary>
        /// The effective maximum speed in m/s that the brake flight assist will operate
        /// </summary>
        [Range(-1000f, 1000f)] public float brakeFlightAssistMaxSpeed;
        /// <summary>
        /// How much power of the ship's roll thrusters is used to execute roll maneuvers. 
        /// Increasing this value will increase the roll speed and responsiveness of the ship.
        /// </summary>
        [Range(0f, 1f)] public float rollPower;
        /// <summary>
        /// How much power of the ship's roll thrusters is used to execute pitching maneuvers. 
        /// Increasing this value will increase the pitch speed and responsiveness of the ship.
        /// </summary>
        [Range(0f, 1f)] public float pitchPower;
        /// <summary>
        /// How much power of the ship's roll thrusters is used to execute yaw maneuvers. 
        /// Increasing this value will increase the yaw speed and responsiveness of the ship.
        /// </summary>
        [Range(0f, 1f)] public float yawPower;
        /// <summary>
        /// How much steering inputs are prioritised over lateral inputs for thrusters.
        /// A value of 0 means no prioritisation takes place, while a value of 1 will almost completely deactivate relevant lateral
        /// thrusters whenever any opposing steering input at all is applied.
        /// </summary>
        [Range(0f, 1f)] public float steeringThrusterPriorityLevel;
        /// <summary>
        /// Whether pitch and roll is limited within a certain range.
        /// </summary>
        public bool limitPitchAndRoll;
        /// <summary>
        /// The maximum allowed pitch (in degrees). Only relevant when limitPitchAndRoll is set to true.
        /// </summary>
        [Range(0f, 75f)] public float maxPitch;
        /// <summary>
        /// The maximum allowed roll (in degrees) when turning. Only relevant when limitPitchAndRoll is set to true.
        /// </summary>
        [Range(0f, 75f)] public float maxTurnRoll;
        /// <summary>
        /// How quickly the ship pitches (in degrees per second) when pitch and roll is limited to a certain range. Only relevant when limitPitchAndRoll is set to true.
        /// </summary>
        [Range(10f, 90f)] public float pitchSpeed;
        /// <summary>
        /// How quickly the ship rolls (in degrees per second) when pitch and roll is limited to a certain range. Only relevant when limitPitchAndRoll is set to true.
        /// </summary>
        [Range(10f, 90f)] public float turnRollSpeed;
        /// <summary>
        /// The mode currently being used for controlling roll. Only relevant when limitPitchAndRoll is set to true. 
        /// Yaw Input: The ship's roll is dependent on the yaw input.
        /// Strafe Input: The ship's roll is dependent on the left-right strafe input.
        /// </summary>
        public RollControlMode rollControlMode;
        /// <summary>
        /// How quickly the ship pitches and rolls to match the target pitch and roll. Only relevant when limitPitchAndRoll is set to true.
        /// </summary>
        [Range(1f, 10f)] public float pitchRollMatchResponsiveness;
        /// <summary>
        /// Whether the ship will attempt to maintain a constant distance from the detectable ground surface.
        /// </summary>
        public bool stickToGroundSurface;
        /// <summary>
        /// Helps the ship to avoid crashing into the ground below it
        /// </summary>
        public bool avoidGroundSurface;
        /// <summary>
        /// Whether the ship will orient itself upwards when a ground surface is not detectable.
        /// </summary>
        public bool orientUpInAir;
        /// <summary>
        /// The distance from the ground the ship will attempt to maintain. Only relevant when stickToGroundSurface is set to true.
        /// </summary>
        public float targetGroundDistance;
        /// <summary>
        /// Whether the movement used for matching the Target Distance from the ground is smoothed. Enable this to prevent the ship
        /// from accelerating too quickly when near the Target Distance. Only relevant when stickToGroundSurface is set to true.
        /// If you modify this, call ReinitialiseGroundMatchVariables().
        /// </summary>
        public bool useGroundMatchSmoothing;
        /// <summary>
        /// Whether the ground match algorithm "looks ahead" to detect obstacles ahead of the ship. 
        /// Only relevant when stickToGroundSurface is set to true.
        /// </summary>
        public bool useGroundMatchLookAhead;
        /// <summary>
        /// The minimum distance from the ground the ship will attempt to maintain. Only relevant when stickToGroundSurface and 
        /// useGroundMatchSmoothing is set to true.
        /// </summary>
        public float minGroundDistance;
        /// <summary>
        /// The minimum distance from the ground the ship will attempt to maintain. Only relevant when stickToGroundSurface and 
        /// useGroundMatchSmoothing is set to true.
        /// </summary>
        public float maxGroundDistance;
        /// <summary>
        /// The maximum distance to check for the ground from the bottom of the ship via raycast. 
        /// Only relevant when stickToGroundSurface is set to true.
        /// </summary>
        [Range(0f, 100f)] public float maxGroundCheckDistance;
        /// <summary>
        /// How responsive the ship is to sudden changes in the distance to the ground. 
        /// Increasing this value will allow the ship to match the Target Distance more closely but may lead to a juddering effect 
        /// if increased too much. Only relevant when stickToGroundSurface is set to true.
        /// If you modify this, call ReinitialiseGroundMatchVariables().
        /// </summary>
        [Range(1f, 100f)] public float groundMatchResponsiveness;
        /// <summary>
        /// How much the up/down motion of the ship is damped when attempting to match the Target Distance.
        /// Increasing this value will reduce overshoot but may make the movement too rigid if increased too much. 
        /// Only relevant when stickToGroundSurface is set to true.
        /// If you modify this, call ReinitialiseGroundMatchVariables().
        /// </summary>
        [Range(1f, 100f)] public float groundMatchDamping;
        /// <summary>
        /// The limit to how quickly the ship can accelerate to maintain the Target Distance to the ground. 
        /// Increasing this value will allow the ship to match the Target Distance more closely but may look less natural.
        /// If you modify this, call ReinitialiseGroundMatchVariables().
        /// </summary>
        [Range(1f, 4f)] public float maxGroundMatchAccelerationFactor;
        /// <summary>
        /// The limit (when at the Target Distance from the ground) to how quickly the ship can accelerate to maintain the Target 
        /// Distance to the ground. Increasing this value will allow the ship to match the Target Distance more closely but may 
        /// look less natural. Only relevant when stickToGroundSurface and useGroundMatchSmoothing is set to true. 
        /// If you modify this, call ReinitialiseGroundMatchVariables().
        /// </summary>
        public float centralMaxGroundMatchAccelerationFactor;
        /// <summary>
        /// How the normal direction (orientation) is determined. 
        /// When Single Normal is selected, the single normal of each face of the ground geometry is used. 
        /// When Smoothed Normal is selected, the normals on each vertex of the face of the ground geometry are blended together to give a smoothed normal, which is used instead. 
        /// Smoothed Normal is more computationally expensive.
        /// </summary>
        public GroundNormalCalculationMode groundNormalCalculationMode;
        /// <summary>
        /// The number of past frames (including this frame) used to average ground normals over. Increase this value to make pitch and roll
        /// movement smoother. Decrease this value to make pitch and roll movement more responsive to changes in the ground
        /// surface. Only relevant when stickToGroundSurface is set to true. 
        /// If you modify this, call ReinitialiseGroundMatchVariables().
        /// </summary>
        [Range(1, 50)] public int groundNormalHistoryLength;
        /// <summary>
        /// Layermask determining which layers will be detected as part of the ground surface when raycasted against.
        /// </summary>
        public LayerMask groundLayerMask;
        /// <summary>
        /// The input axis to be controlled or limited. This is used to simulate 2.5D flight.
        /// </summary>
        public InputControlAxis inputControlAxis;
        /// <summary>
        /// The target value of the inputControlAxis.
        /// </summary>
        public float inputControlLimit;
        /// <summary>
        /// The rate at which force is applied to limit control
        /// </summary>
        [Range(0.1f, 100f)] public float inputControlMovingRigidness;
        /// <summary>
        /// The rate at which the ship turns to limit or correct the rotation
        /// </summary>
        [Range(0.1f, 100f)] public float inputControlTurningRigidness;
        /// <summary>
        /// Forward X angle for the plane the ship will fly in
        /// </summary>
        [Range(0f, 180f)] public float inputControlForwardAngle;

        // Arcade physics properties
        /// <summary>
        /// How fast the ship accelerates when pitching up and down in degrees per second squared. 
        /// Increasing this value will increase how fast the ship can be pitched up and down by pilot and rotational flight assist inputs.
        /// </summary>
        [Range(0f, 500f)] public float arcadePitchAcceleration;
        /// <summary>
        /// How fast the ship accelerates when turning left and right in degrees per second squared. 
        /// Increasing this value will increase how fast the ship can be turned left and right by pilot and rotational flight assist inputs.
        /// </summary>
        [Range(0f, 500f)] public float arcadeYawAcceleration;
        /// <summary>
        /// How fast the ship accelerates when rolling left and right in degrees per second squared. 
        /// Increasing this value will increase how fast the ship can be rolled left and right by pilot and rotational flight assist inputs.
        /// </summary>
        [Range(0f, 500f)] public float arcadeRollAcceleration;
        /// <summary>
        /// How quickly the ship accelerates in metres per second squared while in the air to move in the direction the ship is facing.
        /// </summary>
        [Range(0f, 1000f)] public float arcadeMaxFlightTurningAcceleration;
        /// <summary>
        /// How quickly the ship accelerates in metres per second squared while near the ground to move in the direction the ship is facing.
        /// </summary>
        [Range(0f, 1000f)] public float arcadeMaxGroundTurningAcceleration;

        /// <summary>
        /// [INTERNAL USE ONLY] shouldn't be called directly, call SendInput in the ship control module instead
        /// </summary>
        public Vector3 pilotForceInput { private get; set; }
        /// <summary>
        /// [INTERNAL USE ONLY] shouldn't be called directly, call SendInput in the ship control module instead
        /// </summary>
        public Vector3 pilotMomentInput { private get; set; }
        /// <summary>
        /// [INTERNAL USE ONLY] shouldn't be called directly, call SendInput in the ship control module instead
        /// </summary>
        public bool pilotPrimaryFireInput { private get; set; }
        /// <summary>
        /// [INTERNAL USE ONLY] shouldn't be called directly, call SendInput in the ship control module instead
        /// </summary>
        public bool pilotSecondaryFireInput { private get; set; }

        /// <summary>
        /// The density (in kilograms per cubic metre) of the fluid the ship is travelling through (usually air).
        /// </summary>
        public float mediumDensity;
        /// <summary>
        /// The magnitude of the acceleration (in metres per second squared) due to gravity.
        /// </summary>
        public float gravitationalAcceleration;
        /// <summary>
        /// The direction in world space in which gravity acts upon the ship.
        /// </summary>
        public Vector3 gravityDirection;

        // TODO: Update comment when adding more damage models
        /// <summary>
        /// What damage model the ship uses.
        /// Simple: Simplistic damage model with a single health value. Effects of damage are only visual until the ship is destroyed.
        /// Progressive: More complex damage model. As the ship takes damage the performance of parts is affected.
        /// Localised: More complex damage model. Similar to progressive, but different parts can be damaged independently of each other.
        /// If you modify this, call ReinitialiseDamageRegionVariables().
        /// </summary>
        public ShipDamageModel shipDamageModel;

        /// <summary>
        /// Damage Region used in the simple and progressive damage models. 
        /// If you modify this, call ReinitialiseDamageRegionVariables().
        /// NOTE: Not required if just changing the Health or ShieldHealth
        /// </summary>
        public DamageRegion mainDamageRegion;
        /// <summary>
        /// List of Damage Regions used in the localised damage model. 
        /// If you modify this, call ReinitialiseDamageRegionVariables().
        /// NOTE: Not required if just changing the Health or ShieldHealth
        /// </summary>
        public List<DamageRegion> localisedDamageRegionList;

        /// <summary>
        /// Whether damage multipliers are used when calculating damage from projectiles.
        /// </summary>
        public bool useDamageMultipliers;
        /// <summary>
        /// Whether damage multipliers are localised (i.e. there are different sets of damage multipliers for each damage region
        /// of the ship). Only relevant when useDamageMultipliers is set to true and shipDamageModel is set to Localised.
        /// </summary>
        public bool useLocalisedDamageMultipliers;

        /// <summary>
        /// [READONLY]
        /// Normalised (0.0 – 1.0) value of the overall health of the ship. To get the actual health values, see the damageRegions of the ship.
        /// </summary>
        public float HealthNormalised
        {
            get
            {
                float _healthN = mainDamageRegion.startingHealth == 0f ? 0f : mainDamageRegion.Health / mainDamageRegion.startingHealth;
                if (_healthN > 1f) { return 1f; }
                else if (_healthN < 0f) { return 0f; }
                else { return _healthN; }
            }
        }

        /// <summary>
        /// How respawning happens. If you change this to RespawningMode.RespawnAtLastPosition, call ReinitialiseRespawnVariables().
        /// </summary>
        public RespawningMode respawningMode;
        /// <summary>
        /// How long the respawning process takes (in seconds). Only relevant when respawningMode is not set to DontRespawn.
        /// </summary>
        public float respawnTime;
        /// <summary>
        /// The time (in seconds) between updates of the collision respawn position. Hence when the ship is destroyed by colliding with
        /// something, the ship respawn position will be where the ship was between this time ago and twice this time ago. Only
        /// relevant when respawningMode is set to RespawnAtLastPosition.
        /// </summary>
        public float collisionRespawnPositionDelay;
        /// <summary>
        /// Where the ship respawns from in world space when respawningMode is set to RespawnFromSpecifiedPosition.
        /// </summary>
        public Vector3 customRespawnPosition;
        /// <summary>
        /// The rotation the ship respawns from in world space when respawningMode is set to RespawnFromSpecifiedPosition.
        /// </summary>
        public Vector3 customRespawnRotation;
        /// <summary>
        /// The velocity the ship respawns with in local space. Only relevant when respawningMode is not set to DontRespawn.
        /// </summary>
        public Vector3 respawnVelocity;
        /// <summary>
        /// guidHash code of the Path which the ship will be respawned on when respawningMode is RespawnOnPath.
        /// </summary>
        public int respawningPathGUIDHash;
        /// <summary>
        /// [READONLY] The current respawn position
        /// </summary>
        public Vector3 RespawnPosition { get { return currentRespawnPosition; } }

        /// <summary>
        /// Whether controller rumble is applied to the ship by the ship control module.
        /// </summary>
        public bool applyControllerRumble = false;
        /// <summary>
        /// The minimum amount of damage that will cause controller rumble.
        /// </summary>
        public float minRumbleDamage = 0f;
        /// <summary>
        /// The amount of damage corresponding to maximum controller rumble.
        /// </summary>
        public float maxRumbleDamage = 10f;
        /// <summary>
        /// The time (in seconds) that a controller rumble event lasts for.
        /// </summary>
        [Range(0.1f, 5f)] public float damageRumbleTime = 0.5f;

        /// <summary>
        /// The amount of time that needs to elapse before a stationary ship is considered stuck.
        /// When the value is 0, a stationary ship is never considered stuck.
        /// </summary>
        [Range(0f, 300)] public float stuckTime;
        /// <summary>
        /// The maximum speed in m/sec the ship can be moving before it can be considered stuck
        /// </summary>
        [Range(0f, 1f)] public float stuckSpeedThreshold;
        /// <summary>
        /// The action to take when the ship is deemed stationary or stuck.
        /// </summary>
        public StuckAction stuckAction;
        /// <summary>
        /// guidHash code of the Path which the ship will be respawned on when it is stuck
        /// and the StuckAction is RespawnOnPath.
        /// </summary>
        public int stuckActionPathGUIDHash;

        /// <summary>
        /// [READONLY] The world velocity of the ship as a vector. Derived from the velocity of the rigidbody.
        /// </summary>
        public Vector3 WorldVelocity { get { return worldVelocity; } }

        /// <summary>
        /// [READONLY] The local velocity of the ship as a vector. Derived from the velocity of the rigidbody.
        /// </summary>
        public Vector3 LocalVelocity { get { return localVelocity; } }

        /// <summary>
        /// [READONLY] The world angular velocity of the ship as a vector. Derived from the angular velocity of the rigidbody.
        /// </summary>
        public Vector3 WorldAngularVelocity { get { return worldAngularVelocity; } }

        /// <summary>
        /// [READONLY] The local angular velocity of the ship as a vector. Derived from the angular velocity of the rigidbody.
        /// </summary>
        public Vector3 LocalAngularVelocity { get { return localAngularVelocity; } }

        /// <summary>
        /// [READONLY] The pitch angle, in degrees, above or below the artificial horizon
        /// </summary>
        public float PitchAngle { get { return Vector3.SignedAngle(new Vector3(trfmFwd.x, 0f, trfmFwd.z), trfmFwd, Vector3.right) * Mathf.Sign(-Vector3.Dot(Vector3.forward, trfmFwd)); } }

        /// <summary>
        /// [READONLY] The roll angle, in degrees, left (-ve) or right (+ve)
        /// </summary>
        public float RollAngle { get { Vector3 _localUpNormal = trfmInvRot * Vector3.up; return -Mathf.Atan2(_localUpNormal.x, _localUpNormal.y) * Mathf.Rad2Deg; } }

        /// <summary>
        /// [READONLY] The rigidbody position of the ship as a vector. Derived from the position of the rigidbody. This is
        /// where the physics engine says the ship is.
        /// </summary>
        public Vector3 RigidbodyPosition { get { return rbodyPos; } }

        /// <summary>
        /// [READONLY] The rigidbody rotation of the ship as a vector. Derived from the rotation of the rigidbody. This is
        /// where the physics engine says the ship is rotated.
        /// </summary>
        public Quaternion RigidbodyRotation { get { return rbodyRot; } }

        /// <summary>
        /// [READONLY] The rigidbody inverse rotation of the ship as a vector. Derived from the rotation of the rigidbody. This is
        /// the inverse of where the physics engine says the ship is rotated.
        /// </summary>
        public Quaternion RigidbodyInverseRotation { get { return rbodyInvRot; } }

        /// <summary>
        /// [READONLY] The rigidbody forward direction of the ship as a vector. Derived from the rotation of the rigidbody. This is
        /// the direction the physics engine says the ship is facing.
        /// </summary>
        public Vector3 RigidbodyForward { get { return rbodyFwd; } }

        /// <summary>
        /// [READONLY] The rigidbody right direction of the ship as a vector. Derived from the rotation of the rigidbody. This is
        /// the direction the physics engine says the ship's right direction is facing.
        /// </summary>
        public Vector3 RigidbodyRight { get { return rbodyRight; } }

        /// <summary>
        /// [READONLY] The rigidbody up direction of the ship as a vector. Derived from the rotation of the rigidbody. This is
        /// the direction the physics engine says the ship's up direction is facing.
        /// </summary>
        public Vector3 RigidbodyUp { get { return rbodyUp; } }

        /// <summary>
        /// [READONLY] The position of the ship as a vector. Derived from the position of the transform. You should use 
        /// RigidbodyPosition instead if you are using the data for physics calculations.
        /// </summary>
        public Vector3 TransformPosition { get { return trfmPos; } }

        /// <summary>
        /// [READONLY] The forward direction of the ship in world space as a vector. Derived from the forward direction of the transform. 
        /// You should use RigidbodyForward instead if you are using the data for physics calculations.
        /// </summary>
        public Vector3 TransformForward { get { return trfmFwd; } }

        /// <summary>
        /// [READONLY] The right direction of the ship in world space as a vector. Derived from the right direction of the transform.
        /// You should use RigidbodyRight instead if you are using the data for physics calculations.
        /// </summary>
        public Vector3 TransformRight { get { return trfmRight; } }

        /// <summary>
        /// [READONLY] The up direction of the ship in world space as a vector. Derived from the up direction of the transform.
        /// You should use RigidbodyUp instead if you are using the data for physics calculations.
        /// </summary>
        public Vector3 TransformUp { get { return trfmUp; } }

        /// <summary>
        /// [READONLY] The rotation of the ship in world space as a quaternion. Derived from the rotation of the transform.
        /// You should use RigidbodyRotation instead if you are using the data for physics calculations.
        /// </summary>
        public Quaternion TransformRotation { get { return trfmRot; } }

        /// <summary>
        /// [READONLY] The inverse rotation of the ship in world space as a quaternion. Derived from the rotation of the transform.
        /// You should use RigidbodyInverseRotation instead if you are using the data for physics calculations.
        /// </summary>
        public Quaternion TransformInverseRotation { get { return trfmInvRot; } }

        /// <summary>
        /// [READONLY] Whether the ship is currently sticking to a ground surface.
        /// </summary>
        public bool IsGrounded { get { return stickToGroundSurfaceEnabled; } }

        /// <summary>
        /// [READONLY] The current normal of the target plane in world space. This is the upwards direction the ship will attempt to
        /// orient itself to if limit pitch and roll is enabled.
        /// </summary>
        public Vector3 WorldTargetPlaneNormal { get { return worldTargetPlaneNormal; } }

        /// <summary>
        /// The faction or alliance the ship belongs to. This can be used to identify if a ship is friend or foe.
        /// Default (neutral) is 0.
        /// </summary>
        public int factionId;

        /// <summary>
        /// The squadron this ship is a member of.
        /// </summary>
        public int squadronId;

        /// <summary>
        /// The relative size of the blip on the radar mini-map
        /// </summary>
        [Range(1, 5)] public byte radarBlipSize;

        /// <summary>
        /// [INTERNAL USE ONLY] Instead, call shipControlModule.EnableRadar() or DisableRadar().
        /// </summary>
        public bool isRadarEnabled;

        /// <summary>
        /// [READONLY] The number used by the SSCRadar system to identify this ship at a point in time.
        /// This should not be stored across frames and is updated as required by the system.
        /// </summary>
        public int RadarId { get { return radarItemIndex; } }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// </summary>
        [System.NonSerialized] public SSCRadarPacket sscRadarPacket;

        /// <summary>
        /// Session-only transform InstanceID
        /// </summary>
        [System.NonSerialized] public int shipId;

        /// <summary>
        /// Session-only estimated maximum range of turret weapons
        /// </summary>
        [System.NonSerialized] public float estimatedMaxTurretRange;

        /// <summary>
        /// Session-only estimated maximum range of all weapons with isAutoTargetingEnabled = true
        /// </summary>
        [System.NonSerialized] public float estimatedMaxAutoTargetRange;

        #endregion

        #region Public Delegates

        public delegate void CallbackOnDamage(float mainDamageRegionHealth);
        public delegate void CallbackOnCameraShake(float shakeAmountNormalised);
        public delegate void CallbackOnWeaponFired(Ship ship, Weapon weapon);
        public delegate void CallbackOnWeaponNoAmmo(Ship ship, Weapon weapon);        

        /// <summary>
        /// The name of the custom method that is called immediately
        /// after damage has changed. Your method must take 1 float
        /// parameter. This should be a lightweight method to avoid
        /// performance issues. It could be used to update a HUD or
        /// take some other action.
        /// </summary>
        [NonSerialized] public CallbackOnDamage callbackOnDamage = null;

        /// <summary>
        /// Generally reserved for internal use by ShipCameraModule. If you use your own
        /// camera scripts, you can create a lightweight custom method and assign it at
        /// runtime so that this is called whenever camera shake data is available.
        /// </summary>
        [NonSerialized] public CallbackOnCameraShake callbackOnCameraShake = null;

        /// <summary>
        /// The name of the custom method that is called immediately after
        /// a weapon has fired.
        /// This should be a lightweight method to avoid performance issues that
        /// doesn't hold references the ship or weapon past the end of the current frame.
        /// </summary>
        [NonSerialized] public CallbackOnWeaponFired callbackOnWeaponFired = null;

        /// <summary>
        /// The name of the custom method that is called immediately after the weapon
        /// runs out of ammunition. Your method must take a ship and weapon parameter.
        /// This should be a lightweight method to avoid performance issues that
        /// doesn't hold references the ship or weapon past the end of the current frame.
        /// </summary>
        [NonSerialized] public CallbackOnWeaponNoAmmo callbackOnWeaponNoAmmo = null;
        #endregion

        #region Private and Internal Variables

        #region Private Variables - Cached
        private Vector3 worldVelocity;
        private Vector3 localVelocity;
        private Vector3 absLocalVelocity;
        private Vector3 worldAngularVelocity;
        private Vector3 localAngularVelocity;
        private Vector3 trfmUp;
        private Vector3 trfmFwd;
        private Vector3 trfmRight;
        private Vector3 trfmPos;
        private Vector3 rbodyPos;
        private Vector3 rbodyUp;
        private Vector3 rbodyFwd;
        private Vector3 rbodyRight;
        private Quaternion trfmRot;
        private Quaternion trfmInvRot;
        private Quaternion rbodyRot;
        private Quaternion rbodyInvRot;
        private Vector3 rBodyInertiaTensor;
        private bool shipPhysicsModelPhysicsBased;
        private bool shipDamageModelIsSimple;

        #endregion

        #region Private Variables - Timing related
        private float lastFixedUpdateTime = 0f;
        private Vector3 previousFrameWorldVelocity = Vector3.zero;
        internal float thrusterSystemStartupTimer = 0f;
        internal float thrusterSystemShutdownTimer = 0f;
        #endregion

        #region Private Variables - Cached input
        // Is each input being used on the ship?
        private bool longitudinalThrustInputUsed;
        private bool horizontalThrustInputUsed;
        private bool verticalThrustInputUsed;
        private bool pitchMomentInputUsed;
        private bool yawMomentInputUsed;
        private bool rollMomentInputUsed;
        #endregion

        #region Private Variables - Input
        private bool limitPitchAndRollEnabled = false;
        private bool stickToGroundSurfaceEnabled = false;
        private Vector3 forceInput = Vector3.zero;
        private Vector3 momentInput = Vector3.zero;
        private float pitchAmountInput = 0f, rollAmountInput = 0f;
        private float pitchAmount = 0f, rollAmount = 0f;
        private float maxFramePitchDelta, maxFrameRollDelta;
        private float targetLocalPitch, targetLocalRoll;
        private float yawVelocity;
        private float flightAssistValue;
        #endregion

        #region Private Variables - Thruster
        private Thruster thruster;
        private Vector3 thrustForce = Vector3.zero;
        #endregion

        #region Private Variables - Aerodynamics
        private float dragLength, dragWidth, dragHeight;
        private float quarticLengthXProj, quarticLengthYProj;
        private float quarticWidthYProj, quarticWidthZProj;
        private float quarticHeightXProj, quarticHeightZProj;
        private Vector3 localDragForce;
        private Vector3 localLiftForce;
        private Vector3 localInducedDragForce;
        private float liftForceMagnitude;
        private Wing wing;
        private Vector3 wingPlaneNormal = Vector3.right;
        private Vector3 localVeloInWingPlane = Vector3.zero;
        private float wingAngleOfAttack = 0f;
        private float angularDragMultiplier;
        private ControlSurface controlSurface;
        private float aerodynamicTrueAirspeed;
        private float liftCoefficient = 0f, phi0, phi1, phi2, phi3;
        private Vector3 controlSurfaceMovementAxis = Vector3.up;
        private float controlSurfaceInput = 0f, controlSurfaceAngle;
        private float controlSurfaceLiftDelta, controlSurfaceDragDelta;
        private Vector3 controlSurfaceRelativePosition;
        #endregion

        #region Private Variables - Gravity
        private Vector3 localGravityAcceleration;
        #endregion

        #region Private Variables - Arcade physics
        /// <summary>
        /// Note that this is actually an acceleration.
        /// </summary>
        private Vector3 arcadeMomentVector = Vector3.zero;
        /// <summary>
        /// Note that this is actually an acceleration.
        /// </summary>
        private Vector3 arcadeForceVector = Vector3.zero;
        private float arcadeBrakeForceMagnitude = 0f;
        private float arcadeBrakeForceMinMagnitude = 0f;
        private float maxGroundMatchAcceleration = 0f;
        private float centralMaxGroundMatchAcceleration = 0f;
        // Temp ground distance input required
        private float groundDistInput = 0f;
        #endregion

        #region Private Variables - Combat variables
        private Weapon weapon;
        private Vector3 weaponRelativeFirePosition = Vector3.zero;
        private Vector3 weaponRelativeFireDirection = Vector3.zero;
        private Vector3 weaponWorldBasePosition = Vector3.zero;
        private Vector3 weaponWorldFirePosition = Vector3.zero;
        private Vector3 weaponWorldFireDirection = Vector3.zero;
        private Vector3 currentRespawnPosition = Vector3.zero;
        private Quaternion currentRespawnRotation = Quaternion.identity;
        private Vector3 currentCollisionRespawnPosition = Vector3.zero;
        private Vector3 nextCollisionRespawnPosition = Vector3.zero;
        private Quaternion currentCollisionRespawnRotation = Quaternion.identity;
        private Quaternion nextCollisionRespawnRotation = Quaternion.identity;
        private float collisionRespawnPositionTimer = 0f;
        private bool lastDamageWasCollisionDamage = false;
        private int weaponFiringButtonInt;
        private int weaponPrimaryFiringInt, weaponSecondaryFiringInt, weaponAutoFiringInt;
        private int weaponTypeInt;
        private int lastDamageEventIndex = -1;
        private float damageRumbleAmountRequired = 0f;
        private float damageCameraShakeAmountRequired = 0f;

        // Damage Regions
        [System.NonSerialized] internal int numLocalisedDamageRegions;

        // Radar variables
        [System.NonSerialized] internal int radarItemIndex = -1;

        #endregion

        #region Private Variables - Collision
        
        /// <summary>
        /// Unordered unique set of colliders on objects that have been attached to this ship.
        /// These are ignored during collision events
        /// </summary>
        [NonSerialized] private HashSet<int> attachedCollisionColliders = new HashSet<int>();
        #endregion

        #region Private Variables - PID Controllers
        private PIDController pitchPIDController;
        private PIDController rollPIDController;
        private PIDController groundDistPIDController;
        private PIDController rFlightAssistPitchPIDController;
        private PIDController rFlightAssistYawPIDController;
        private PIDController rFlightAssistRollPIDController;
        private PIDController inputAxisForcePIDController;
        private PIDController inputAxisMomentPIDController;
        private PIDController sFlightAssistPitchPIDController;
        private PIDController sFlightAssistYawPIDController;
        private PIDController sFlightAssistRollPIDController;
        private float sFlightAssistTargetPitch = 0f;
        private float sFlightAssistTargetYaw = 0f;
        private float sFlightAssistTargetRoll = 0f;
        private bool sFlightAssistRotatingPitch = false;
        private bool sFlightAssistRotatingYaw = false;
        private bool sFlightAssistRotatingRoll = false;
        #endregion

        #region Private Variables - Normals
        private Vector3 worldTargetPlaneNormal = Vector3.up;
        private Vector3 localTargetPlaneNormal = Vector3.up;
        private float currentPerpendicularGroundDist = -1f;
        private MeshCollider groundMeshCollider;
        private MeshCollider cachedGroundMeshCollider;
        private Mesh cachedGroundMesh;
        private Vector3[] cachedGroundMeshNormals;
        private int[] cachedGroundMeshTriangles;
        private Vector3 groundNormal0;
        private Vector3 groundNormal1;
        private Vector3 groundNormal2;
        private Vector3 barycentricCoord;
        private Vector3[] groundNormalHistory;
        private int groundNormalHistoryIdx = 0;
        #endregion

        #region Private and Internal Variables - Misc

        private int componentIndex;
        private int componentListSize;

        private Ray raycastRay;
        private RaycastHit raycastHitInfo;

        /// <summary>
        /// [INTERNAL ONLY]
        /// Added in 1.3.3 to support muzzle FX parenting.
        /// MAY NEED TO BE REMOVED IN THE FUTURE.
        /// WARNING: Not fully tested with destroying ships
        /// in the scene. Need to be careful it doesn't prevent
        /// garabage collection of ShipControlModule.
        /// Where possible - use the cached trfmXXX itema above.
        /// </summary>
        [System.NonSerialized] internal Transform shipTransform;

        private SSCManager sscManager = null;

        // Reusable lists - typically used with GetComponents or GetComponentsInChildren to avoid GC.
        //private List<ShipControlModule> tempShipControlModuleList;

        // Add additional force or boost
        private Vector3 boostDirection;
        private float boostTimer;
        private float boostForce;  // in newtons

        #endregion

        #endregion

        #region Constructors

        // Class constructor
        public Ship()
        {
            SetClassDefaults();
        }

        // Copy constructor
        public Ship (Ship ship)
        {
            if (ship == null) { SetClassDefaults(); }
            else
            {
                this.initialiseOnAwake = ship.initialiseOnAwake;
                this.shipPhysicsModel = ship.shipPhysicsModel;
                this.mass = ship.mass;
                this.centreOfMass = ship.centreOfMass;
                this.setCentreOfMassManually = ship.setCentreOfMassManually;
                this.showCOMGizmosInSceneView = ship.showCOMGizmosInSceneView;
                this.showCOLGizmosInSceneView = ship.showCOLGizmosInSceneView;
                this.showCOTGizmosInSceneView = ship.showCOTGizmosInSceneView;

                if (ship.thrusterList == null)
                {
                    this.thrusterList = new List<Thruster>(25);

                    // Create a default forwards-facing thruster
                    Thruster thruster = new Thruster();
                    if (thruster != null)
                    {
                        thruster.name = "Forward Thruster";
                        thruster.forceUse = 1;
                        thruster.primaryMomentUse = 0;
                        thruster.secondaryMomentUse = 0;
                        thruster.maxThrust = 100000;
                        thruster.thrustDirection = Vector3.forward;
                        thruster.relativePosition = Vector3.zero;
                        this.thrusterList.Add(thruster);
                    }
                }
                else { this.thrusterList = ship.thrusterList.ConvertAll(th => new Thruster(th)); }

                if (ship.wingList == null) { this.wingList = new List<Wing>(5); }
                else { this.wingList = ship.wingList.ConvertAll(wg => new Wing(wg)); }

                if (ship.controlSurfaceList == null) { this.controlSurfaceList = new List<ControlSurface>(10); }
                else { this.controlSurfaceList = ship.controlSurfaceList.ConvertAll(cs => new ControlSurface(cs)); }

                if (ship.weaponList == null) { this.weaponList = new List<Weapon>(10); }
                else { this.weaponList = ship.weaponList.ConvertAll(wp => new Weapon(wp)); }

                this.isThrusterSystemsExpanded = ship.isThrusterSystemsExpanded;
                this.isThrusterListExpanded = ship.isThrusterListExpanded;
                this.isWingListExpanded = ship.isWingListExpanded;
                this.isControlSurfaceListExpanded = ship.isControlSurfaceListExpanded;
                this.isWeaponListExpanded = ship.isWeaponListExpanded;
                this.isMainDamageExpanded = ship.isMainDamageExpanded;
                this.isDamageListExpanded = ship.isDamageListExpanded;
                this.showDamageInEditor = ship.showDamageInEditor;

                this.isThrusterFXStationary = ship.isThrusterFXStationary;
                this.isThrusterSystemsStarted = ship.isThrusterSystemsStarted;
                this.thrusterSystemStartupDuration = ship.thrusterSystemStartupDuration;
                this.thrusterSystemShutdownDuration = ship.thrusterSystemShutdownDuration;
                this.useCentralFuel = ship.useCentralFuel;
                this.centralFuelLevel = ship.centralFuelLevel;

                this.dragCoefficients = ship.dragCoefficients;
                this.dragReferenceAreas = ship.dragReferenceAreas;
                this.centreOfDragXMoment = ship.centreOfDragXMoment;
                this.centreOfDragYMoment = ship.centreOfDragYMoment;
                this.centreOfDragZMoment = ship.centreOfDragZMoment;
                this.angularDragFactor = ship.angularDragFactor;
                this.disableDragMoments = ship.disableDragMoments;
                this.dragXMomentMultiplier = ship.dragXMomentMultiplier;
                this.dragYMomentMultiplier = ship.dragYMomentMultiplier;
                this.dragZMomentMultiplier = ship.dragZMomentMultiplier;
                this.wingStallEffect = ship.wingStallEffect;

                this.arcadeUseBrakeComponent = ship.arcadeUseBrakeComponent;
                this.arcadeBrakeStrength = ship.arcadeBrakeStrength;
                this.arcadeBrakeIgnoreMediumDensity = ship.arcadeBrakeIgnoreMediumDensity;
                this.arcadeBrakeMinAcceleration = ship.arcadeBrakeMinAcceleration;

                this.rotationalFlightAssistStrength = ship.rotationalFlightAssistStrength;
                this.translationalFlightAssistStrength = ship.translationalFlightAssistStrength;
                this.brakeFlightAssistStrength = ship.brakeFlightAssistStrength;
                this.brakeFlightAssistMaxSpeed = ship.brakeFlightAssistMinSpeed;
                this.brakeFlightAssistMaxSpeed = ship.brakeFlightAssistMaxSpeed;

                this.limitPitchAndRoll = ship.limitPitchAndRoll;
                this.maxPitch = ship.maxPitch;
                this.maxTurnRoll = ship.maxTurnRoll;
                this.pitchSpeed = ship.pitchSpeed;
                this.turnRollSpeed = ship.turnRollSpeed;
                this.rollControlMode = ship.rollControlMode;
                this.pitchRollMatchResponsiveness = ship.pitchRollMatchResponsiveness;
                this.stickToGroundSurface = ship.stickToGroundSurface;
                this.avoidGroundSurface = ship.avoidGroundSurface;
                this.useGroundMatchSmoothing = ship.useGroundMatchSmoothing;
                this.useGroundMatchLookAhead = ship.useGroundMatchLookAhead;
                this.orientUpInAir = ship.orientUpInAir;
                this.targetGroundDistance = ship.targetGroundDistance;
                this.minGroundDistance = ship.minGroundDistance;
                this.maxGroundDistance = ship.maxGroundDistance;
                this.maxGroundCheckDistance = ship.maxGroundCheckDistance;
                this.groundMatchResponsiveness = ship.groundMatchResponsiveness;
                this.groundMatchDamping = ship.groundMatchDamping;
                this.maxGroundMatchAccelerationFactor = ship.maxGroundMatchAccelerationFactor;
                this.centralMaxGroundMatchAccelerationFactor = ship.centralMaxGroundMatchAccelerationFactor;
                this.groundNormalCalculationMode = ship.groundNormalCalculationMode;
                this.groundNormalHistoryLength = ship.groundNormalHistoryLength;
                this.groundLayerMask = ship.groundLayerMask;

                this.inputControlAxis = ship.inputControlAxis;
                this.inputControlLimit = ship.inputControlLimit;
                this.inputControlForwardAngle = ship.inputControlForwardAngle;
                this.inputControlMovingRigidness = ship.inputControlMovingRigidness;
                this.inputControlTurningRigidness = ship.inputControlTurningRigidness;

                this.rollPower = ship.rollPower;
                this.pitchPower = ship.pitchPower;
                this.yawPower = ship.yawPower;
                this.steeringThrusterPriorityLevel = ship.steeringThrusterPriorityLevel;

                this.pilotMomentInput = ship.pilotMomentInput;
                this.pilotForceInput = ship.pilotForceInput;
                this.arcadePitchAcceleration = ship.arcadePitchAcceleration;
                this.arcadeYawAcceleration = ship.arcadeYawAcceleration;
                this.arcadeRollAcceleration = ship.arcadeRollAcceleration;
                this.arcadeMaxFlightTurningAcceleration = ship.arcadeMaxFlightTurningAcceleration;
                this.arcadeMaxGroundTurningAcceleration = ship.arcadeMaxGroundTurningAcceleration;

                this.mediumDensity = ship.mediumDensity;
                this.gravitationalAcceleration = ship.gravitationalAcceleration;
                this.gravityDirection = ship.gravityDirection;

                this.shipDamageModel = ship.shipDamageModel;
                if (ship.mainDamageRegion == null)
                {
                    this.mainDamageRegion = new DamageRegion();
                    this.mainDamageRegion.name = "Main Damage Region";
                }
                else { this.mainDamageRegion = new DamageRegion(ship.mainDamageRegion); }

                if (ship.localisedDamageRegionList == null) { this.localisedDamageRegionList = new List<DamageRegion>(10); }
                else { this.localisedDamageRegionList = ship.localisedDamageRegionList.ConvertAll(dr => new DamageRegion(dr)); }

                this.useDamageMultipliers = ship.useDamageMultipliers;
                this.useLocalisedDamageMultipliers = ship.useLocalisedDamageMultipliers;

                this.respawnTime = ship.respawnTime;
                this.collisionRespawnPositionDelay = ship.collisionRespawnPositionDelay;
                this.respawningMode = ship.respawningMode;
                this.customRespawnPosition = ship.customRespawnPosition;
                this.customRespawnRotation = ship.customRespawnRotation;
                this.respawnVelocity = ship.respawnVelocity;
                this.respawningPathGUIDHash = ship.respawningPathGUIDHash;
                this.minRumbleDamage = ship.minRumbleDamage;
                this.maxRumbleDamage = ship.maxRumbleDamage;
                this.stuckTime = ship.stuckTime;
                this.stuckSpeedThreshold = ship.stuckSpeedThreshold;
                this.stuckAction = ship.stuckAction;
                this.stuckActionPathGUIDHash = ship.stuckActionPathGUIDHash;
                this.factionId = ship.factionId;
                this.squadronId = ship.squadronId;

                this.isRadarEnabled = ship.isRadarEnabled;
                this.radarBlipSize = ship.radarBlipSize;
            }
        }

        #endregion

        #region Private and Internal Non-Static Methods

        /// <summary>
        /// [INTERNAL ONLY]
        /// Check if the thruster systems are starting up or shutting down.
        /// If the state has changed in this frame, like has started or shutdown, the method will return true.
        /// </summary>
        /// <param name="deltaTime"></param>
        /// <param name="hasShutdown"></param>
        /// <param name="hasStarted"></param>
        /// <returns></returns>
        internal bool CheckThrusterSystems(float deltaTime, out bool hasStarted, out bool hasShutdown)
        {
            bool hasStateChanged = false;
            hasShutdown = false;
            hasStarted = false;

            // Is the system shutting down?
            if (thrusterSystemShutdownTimer > 0f)
            {
                thrusterSystemShutdownTimer += deltaTime;

                //Debug.Log("[DEBUG] Thruster System shutting down: " + thrusterSystemShutdownTimer);

                if (thrusterSystemShutdownTimer > thrusterSystemShutdownDuration)
                {
                    ShutdownThrusterSystems(true);
                    hasStateChanged = true;
                    hasShutdown = true;
                }
                else
                {
                    SetThrottleAllThrusters(1f - thrusterSystemShutdownTimer / thrusterSystemShutdownDuration);
                }
            }
            else if (thrusterSystemStartupTimer > 0f)
            {
                thrusterSystemStartupTimer += deltaTime;

                //Debug.Log("[DEBUG] Thruster System starting up: " + thrusterSystemStartupTimer);

                if (thrusterSystemStartupTimer > thrusterSystemStartupDuration)
                {
                    StartupThrusterSystems(true);
                    hasStateChanged = true;
                    hasStarted = true;
                }
                else
                {
                    SetThrottleAllThrusters(thrusterSystemStartupTimer / thrusterSystemStartupDuration);
                }
            }

            return hasStateChanged;
        }

        /// <summary>
        /// Reset the thruster systems variables and thruster throttle values.
        /// The systems might have been starting or shutting down when the ship
        /// was destroyed.
        /// Typically used when a ship is respawned.
        /// </summary>
        internal void ResetThrusterSystems()
        {
            // If the systems are starting or shutting down, reset all the
            // thruster throttle values.
            if (thrusterSystemStartupTimer > 0f || thrusterSystemShutdownTimer > 0f)
            {
                SetThrottleAllThrusters(isThrusterSystemsStarted ? 1f : 0f);
            }

            thrusterSystemShutdownTimer = 0f;
            thrusterSystemStartupTimer = 0f;
        }

        /// <summary>
        /// Returns the angle between two vectors projected into a specified plane, with a sign indicating the direction.
        /// </summary>
        /// <param name="from"></param>
        /// <param name="to"></param>
        /// <param name="planeNormal"></param>
        /// <returns></returns>
        private float SignedAngleInPlane(Vector3 from, Vector3 to, Vector3 planeNormal)
        {
            return Vector3.SignedAngle(Vector3.ProjectOnPlane(from, planeNormal), Vector3.ProjectOnPlane(to, planeNormal), planeNormal);
        }

        /// <summary>
        /// Returns a float number raised to an integer power.
        /// </summary>
        /// <param name="baseNum"></param>
        /// <param name="indexNum"></param>
        /// <returns></returns>
        private float IntegerPower(float baseNum, int powerNum)
        {
            // Start power calculation at power of zero instead of power of one, so that anything to the power of zero is one
            float resultNum = 1f;
            // Iteratively mutiply by the base number
            for (int i = 0; i < powerNum; i++) { resultNum *= baseNum; }
            return resultNum;
        }

        /// <summary>
        /// Returns the damped (averaged) normal.
        /// </summary>
        /// <param name="currentNormal"></param>
        /// <returns></returns>
        private Vector3 GetDampedGroundNormal()
        {
            Vector3 dampedNormal = Vector3.zero;

            // Loop through ground normal
            for (int hIdx = 0; hIdx < groundNormalHistoryLength; hIdx++)
            {
                // Optimise by setting each axis rather than creating new vectors
                dampedNormal.x += groundNormalHistory[hIdx].x;
                dampedNormal.y += groundNormalHistory[hIdx].y;
                dampedNormal.z += groundNormalHistory[hIdx].z;
            }

            dampedNormal.x *= 1f / groundNormalHistoryLength;
            dampedNormal.y *= 1f / groundNormalHistoryLength;
            dampedNormal.z *= 1f / groundNormalHistoryLength;

            return dampedNormal.normalized;
        }

        /// <summary>
        /// Update the normal history.
        /// </summary>
        /// <param name="currentNormal"></param>
        private void UpdateGroundNormalHistory(Vector3 currentNormal)
        {
            // Update the current element of the history with the current normal
            if (groundNormalHistoryLength > groundNormalHistoryIdx) { groundNormalHistory[groundNormalHistoryIdx] = currentNormal; }
            // Keeps track of which array position to replace. Avoids having to shuffle elements
            groundNormalHistoryIdx++;
            if (groundNormalHistoryIdx >= groundNormalHistoryLength) { groundNormalHistoryIdx = 0; }
        }

        /// <summary>
        /// Returns a damped max acceleration input.
        /// </summary>
        /// <param name="currentDisplacement"></param>
        /// <param name="targetDisplacement"></param>
        /// <param name="minDisplacement"></param>
        /// <param name="maxDisplacement"></param>
        /// <param name="minAcceleration"></param>
        /// <param name="maxAcceleration"></param>
        /// <param name="inputScalingFactor"></param>
        /// <param name="inputPowerFactor"></param>
        /// <returns></returns>
        private float DampedMaxAccelerationInput(float currentDisplacement, float targetDisplacement, float minDisplacement,
            float maxDisplacement, float minAcceleration, float maxAcceleration, float inputScalingFactor, float inputPowerFactor)
        {
            // Highest possible max acceleration input is max acceleration
            float maxAccelerationInput = maxAcceleration;

            // The max acceleration is determined by how close we are to the ground
            // At the min/max allowed distance from the ground the limit is infinity
            // At the target distance from the ground the limit is some given value

            if (currentDisplacement < targetDisplacement && currentDisplacement > minDisplacement)
            {
                // Reciprocal
                float xVal = (currentDisplacement - minDisplacement) / (targetDisplacement - minDisplacement);
                maxAccelerationInput = minAcceleration + (inputScalingFactor * ((1f / Mathf.Pow(xVal, inputPowerFactor)) - 1f));
            }
            else if (currentDisplacement > targetDisplacement && currentDisplacement < maxDisplacement)
            {
                // Reciprocal
                float xVal = (currentDisplacement - maxDisplacement) / (targetDisplacement - maxDisplacement);
                maxAccelerationInput = minAcceleration + (inputScalingFactor * ((1f / Mathf.Pow(xVal, inputPowerFactor)) - 1f));
            }

            // Highest possible max acceleration input is max acceleration
            return maxAccelerationInput < maxAcceleration ? maxAccelerationInput : maxAcceleration;
        }

        /// <summary>
        /// Applies a specified amount of damage to the ship at a specified position.
        /// </summary>
        /// <param name="damageAmount"></param>
        /// <param name="damagePosition"></param>
        /// <param name="isCollisionDamage"></param>
        private void ApplyDamage(float damageAmount, ProjectileModule.DamageType damageType, Vector3 damagePosition, bool adjustForCollisionResistance)
        {
            // Determine which damage region was hit
            bool[] regionDamagedArray = new bool[1];
            float[] actualDamageArray = new float[1];
            int damageArrayLengths = 1;
            if (shipDamageModel == ShipDamageModel.Localised)
            {
                damageArrayLengths = localisedDamageRegionList.Count + 1;
                regionDamagedArray = new bool[damageArrayLengths];
                actualDamageArray = new float[damageArrayLengths];
            }

            // Loop through all damage regions
            DamageRegion thisDamageRegion;
            float actualDamage = 0f;
            bool regionDamaged = false;            
            bool isShipInvincible = mainDamageRegion.isInvincible;

            // Get damage position in local space (calc once outside the loop)
            Vector3 localDamagePosition = trfmInvRot * (damagePosition - trfmPos);

            for (int i = 0; i < damageArrayLengths; i++)
            {
                // Get the current damage region we are iterating over, and work out whether it has been hit
                if (i == 0)
                {
                    // Main damage region
                    thisDamageRegion = mainDamageRegion;
                    // Main damage region is always hit unless it is invincible (which would make the whole ship invincible)
                    regionDamaged = !isShipInvincible;
                }
                else
                {
                    // Localised damage region
                    thisDamageRegion = localisedDamageRegionList[i - 1];

                    // If the main damage region is invincible, so are all the damage regions
                    if (isShipInvincible || thisDamageRegion.isInvincible) { regionDamaged = false; }
                    else
                    {
                        // Calculate whether the hit point is within the volume, and hence populate regionDamaged variable
                        // First, get damage position in local space
                        //localDamagePosition = trfmInvRot * (damagePosition - trfmPos);

                        // Then check whether the position is within the bounds of this damage region
                        regionDamaged = thisDamageRegion.IsHit(localDamagePosition);

                        // For some reason we need to slightly expand the damage region when the collider is exactly the same size.
                        //regionDamaged = localDamagePosition.x >= thisDamageRegion.relativePosition.x - (thisDamageRegion.size.x / 2) - 0.0001f &&
                        //    localDamagePosition.x <= thisDamageRegion.relativePosition.x + (thisDamageRegion.size.x / 2) + 0.0001f &&
                        //    localDamagePosition.y >= thisDamageRegion.relativePosition.y - (thisDamageRegion.size.y / 2) - 0.0001f &&
                        //    localDamagePosition.y <= thisDamageRegion.relativePosition.y + (thisDamageRegion.size.y / 2) + 0.0001f &&
                        //    localDamagePosition.z >= thisDamageRegion.relativePosition.z - (thisDamageRegion.size.z / 2) - 0.0001f &&
                        //    localDamagePosition.z <= thisDamageRegion.relativePosition.z + (thisDamageRegion.size.z / 2) + 0.0001f;
                    }
                }

                // Don't do any further calculations if the region wasn't hit
                if (regionDamaged)
                {
                    // Adjust for collision resistance - value passed in is just an impulse magnitude
                    if (adjustForCollisionResistance)
                    {
                        if (thisDamageRegion.collisionDamageResistance < 0.01f) { actualDamage = Mathf.Infinity; }
                        else { actualDamage = damageAmount / thisDamageRegion.collisionDamageResistance * 4f / mass; }
                    }
                    else { actualDamage = damageAmount; }

                    // Modify damage dealt based on relevant damage multipliers
                    if (useDamageMultipliers)
                    {
                        if (useLocalisedDamageMultipliers)
                        {
                            actualDamage *= thisDamageRegion.GetDamageMultiplier(damageType);
                        }
                        else
                        {
                            actualDamage *= mainDamageRegion.GetDamageMultiplier(damageType);
                        }
                    }

                    // Determine whether shielding is active for this damage region
                    if (thisDamageRegion.useShielding && thisDamageRegion.ShieldHealth > 0f)
                    {
                        // Set the shielding to active
                        regionDamaged = false;
                        // Only do damage to the shield if the damage amount is above the shielding threshold
                        if (actualDamage >= thisDamageRegion.shieldingDamageThreshold)
                        {
                            thisDamageRegion.ShieldHealth -= actualDamage;
                            thisDamageRegion.shieldRechargeDelayTimer = 0f;
                            // If this damage destroys the shielding entirely...
                            if (thisDamageRegion.ShieldHealth <= 0f)
                            {
                                // Get the residual damage value
                                actualDamage = -thisDamageRegion.ShieldHealth;
                                // Set the shielding to inactive
                                regionDamaged = true;
                                thisDamageRegion.ShieldHealth = -0.01f;
                            }
                        }
                    }

                    if (regionDamaged)
                    {
                        // Reduce health of damage region itself
                        thisDamageRegion.Health -= actualDamage;
                    }
                }
                // Added v1.2.7 - regions not damaged should have 0 damage
                else { actualDamage = 0f; }

                // Set calculated array variables
                actualDamageArray[i] = actualDamage;
                regionDamagedArray[i] = regionDamaged;
            }

            // Need to re-check this variable, as damage can destroy shielding and then use residual damage to affect
            // the damage region as well
            if (!shipDamageModelIsSimple && !isShipInvincible)
            {
                // Loop through each part list and reduce health of all parts in the regions that have been damaged

                // Compute an index shift for localised damage regions
                // Main damage region is index 0, so localised damage regions are a one-based indexing system
                // as opposed to the standard zero-based indexing system
                int drIndexShift = 0, drShiftedIndex, drIndex;
                if (shipDamageModel == ShipDamageModel.Localised) { drIndexShift = 1; }

                componentListSize = thrusterList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    drIndex = thrusterList[componentIndex].damageRegionIndex;
                    drShiftedIndex = drIndex + drIndexShift;
                    if (drIndex > -1 && drShiftedIndex < damageArrayLengths && regionDamagedArray[drShiftedIndex])
                    {
                        thrusterList[componentIndex].Health -= actualDamageArray[drShiftedIndex];
                    }
                }

                componentListSize = wingList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    drIndex = wingList[componentIndex].damageRegionIndex;
                    drShiftedIndex = drIndex + drIndexShift;
                    if (drIndex > -1 && drShiftedIndex < damageArrayLengths && regionDamagedArray[drShiftedIndex])
                    {
                        wingList[componentIndex].Health -= actualDamageArray[drShiftedIndex];
                    }
                }

                componentListSize = controlSurfaceList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    drIndex = controlSurfaceList[componentIndex].damageRegionIndex;
                    drShiftedIndex = drIndex + drIndexShift;
                    if (drIndex > -1 && drShiftedIndex < damageArrayLengths && regionDamagedArray[drShiftedIndex])
                    {
                        controlSurfaceList[componentIndex].Health -= actualDamageArray[drShiftedIndex];
                    }
                }

                componentListSize = weaponList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    drIndex = weaponList[componentIndex].damageRegionIndex;
                    drShiftedIndex = drIndex + drIndexShift;
                    if (drIndex > -1 && drShiftedIndex < damageArrayLengths && regionDamagedArray[drShiftedIndex])
                    {
                        weaponList[componentIndex].Health -= actualDamageArray[drShiftedIndex];
                    }
                }
            }

            // Damage callback
            if (callbackOnDamage != null) { callbackOnDamage(mainDamageRegion.Health); }

            // Add a damage event
            lastDamageEventIndex++;
            // Calculate rumble and camera shake required
            float rumbleDamageAmount = damageAmount;
            if (adjustForCollisionResistance)
            {
                rumbleDamageAmount /= mainDamageRegion.collisionDamageResistance * 2500f;
            }

            // Start camera shake as damageAmount with adjusted collision amount if applicable
            damageCameraShakeAmountRequired = rumbleDamageAmount;

            if (rumbleDamageAmount <= minRumbleDamage) { damageRumbleAmountRequired = 0f; }
            else if (rumbleDamageAmount >= maxRumbleDamage) { damageRumbleAmountRequired = 1f; }
            else { damageRumbleAmountRequired = (rumbleDamageAmount - minRumbleDamage) / (maxRumbleDamage - minRumbleDamage); }

            // Calculate camera shake required
            if (damageCameraShakeAmountRequired < 0f) { damageCameraShakeAmountRequired = 0f; }
            else if (damageCameraShakeAmountRequired > 1f) { damageCameraShakeAmountRequired = 1f; }
        }

        /// <summary>
        /// Get the index or 0-based position in the list of damage regions.
        /// Although typically used to return the index in the localisedDamageRegionList,
        /// if passed the mainDamageRegion, it will always return 0.
        /// If no matching regions are found, it will return -1.
        /// </summary>
        /// <param name="damageRegion"></param>
        /// <returns></returns>
        private int GetDamageRegionIndex(DamageRegion damageRegion)
        {
            int damageRegionIndex = -1;

            if (damageRegion.guidHash == mainDamageRegion.guidHash) { return 0; }
            else
            {
                int localisedDamageRegionListCount = localisedDamageRegionList == null ? 0 : localisedDamageRegionList.Count;

                for (int drIdx = 0; drIdx < localisedDamageRegionListCount; drIdx++)
                {
                    if (localisedDamageRegionList[drIdx].guidHash == damageRegion.guidHash) { damageRegionIndex = drIdx; break; }
                }
            }

            return damageRegionIndex;
        }

        /// <summary>
        /// Update the maximum estimated range for all turrets with auto targeting enabled
        /// </summary>
        internal void UpdateMaxTurretRange()
        {
            int numWeapons = weaponList == null ? 0 : weaponList.Count;

            float maxRange = 0f;

            for (int wpIdx = 0; wpIdx < numWeapons; wpIdx++)
            {
                Weapon weapon = weaponList[wpIdx];

                // Only process turrets with Auto Targeting enabled
                if (weapon != null && weapon.isAutoTargetingEnabled && (weapon.weaponTypeInt == Weapon.TurretProjectileInt || weapon.weaponTypeInt == Weapon.TurretBeamInt))
                {
                    if (weapon.estimatedRange > maxRange) { maxRange = weapon.estimatedRange; }
                }
            }

            estimatedMaxTurretRange = maxRange;
        }

        /// <summary>
        /// Update the maximum estimated range for all weapons with auto targeting enabled
        /// </summary>
        internal void UpdateMaxAutoTargetRange()
        {
            int numWeapons = weaponList == null ? 0 : weaponList.Count;

            float maxRange = 0f;

            for (int wpIdx = 0; wpIdx < numWeapons; wpIdx++)
            {
                Weapon weapon = weaponList[wpIdx];

                // Only process weapons with Auto Targeting enabled
                if (weapon != null && weapon.isAutoTargetingEnabled)
                {
                    if (weapon.estimatedRange > maxRange) { maxRange = weapon.estimatedRange; }
                }
            }

            estimatedMaxAutoTargetRange = maxRange;
        }

        /// <summary>
        /// This gets called from FixedUpdate in BeamModule. It performs the following:
        /// 1. Checks if the beam should be despawned
        /// 2. Moves the beam
        /// 3. Changes the length
        /// 4. Checks if it hits anything
        /// 5. Updates damage on what it hits
        /// 6. Instantiate effects object at hit point
        /// 7. Consumes weapon power
        /// It needs to be a member of the ship instance as it requires both ship and beam data.
        /// Assumes the beam linerenderer has useWorldspace enabled.
        /// </summary>
        /// <param name="beamModule"></param>
        internal void MoveBeam(BeamModule beamModule)
        {
            if (beamModule.isInitialised && beamModule.isBeamEnabled && beamModule.weaponIndex >= 0 && beamModule.firePositionIndex >= 0)
            {
                Weapon weapon = weaponList[beamModule.weaponIndex];
                SSCBeamItemKey beamItemKey = weapon.beamItemKeyList[beamModule.firePositionIndex];

                // Read once from the deltaTime property getter
                float _deltaTime = Time.deltaTime;

                if ((weapon.weaponTypeInt == Weapon.FixedBeamInt || weapon.weaponTypeInt == Weapon.TurretBeamInt) && beamItemKey.beamSequenceNumber == beamModule.itemSequenceNumber)
                {
                    weaponFiringButtonInt = (int)weapon.firingButton;

                    // For autofiring weapons, this will return false.
                    bool isReadyToFire = (pilotPrimaryFireInput && weaponFiringButtonInt == weaponPrimaryFiringInt) || (pilotSecondaryFireInput && weaponFiringButtonInt == weaponSecondaryFiringInt);

                    // If this is auto-firing turret check if it is ready to fire
                    if (weaponFiringButtonInt == weaponAutoFiringInt && weapon.weaponTypeInt == Weapon.TurretBeamInt)
                    {
                        // Is turret locked on target and in direct line of sight?
                        // NOTE: If check LoS is enabled, it will still fire if another enemy is between the turret and the weapon.target.
                        isReadyToFire = weapon.isLockedOnTarget && (!weapon.checkLineOfSight || WeaponHasLineOfSight(weapon));

                        // BUILD TEST
                        //isReadyToFire = !weapon.checkLineOfSight || WeaponHasLineOfSight(weapon);
                        // TODO - Is target in range?
                    }

                    // Should this beam be despawned or returned to the pool?
                    // a) No charge in weapon
                    // b) Weapon has no health
                    // c) Weapon has no performance
                    // d) user stopped firing and has fired for min time permitted
                    // e) has exceeded the maximum firing duration
                    if (weapon.chargeAmount <= 0f || weapon.health <= 0f || weapon.currentPerformance == 0f || (!isReadyToFire && beamModule.burstDuration > beamModule.minBurstDuration) || beamModule.burstDuration > beamModule.maxBurstDuration)
                    {
                        // Unassign the beam from this weapon's fire position
                        weapon.beamItemKeyList[beamModule.firePositionIndex] = new SSCBeamItemKey(-1, -1, 0);

                        beamModule.DestroyBeam();
                    }
                    else
                    {
                        // Move the beam start
                        // Calculate the World-space Fire Position (like when weapon is first fired)
                        // Note: That the ship could have moved and rotated, so we recalc here.
                        Vector3 _weaponWSBasePos = GetWeaponWorldBasePosition(weapon);
                        Vector3 _weaponWSFireDir = GetWeaponWorldFireDirection(weapon);
                        Vector3 _weaponWSFirePos = GetWeaponWorldFirePosition(weapon, _weaponWSBasePos, beamModule.firePositionIndex);

                        // Move the beam transform but keep the first LineRenderer position at 0,0,0
                        beamModule.transform.position = _weaponWSFirePos;

                        beamModule.transform.SetPositionAndRotation(_weaponWSFirePos, Quaternion.LookRotation(_weaponWSFireDir, rbodyUp));

                        // Calc length and end position
                        float _desiredLength = beamModule.burstDuration * beamModule.speed;

                        if (_desiredLength > weapon.maxRange) { _desiredLength = weapon.maxRange; }

                        Vector3 _endPosition = _weaponWSFirePos + (_weaponWSFireDir * _desiredLength);

                        // Check if it hit anything
                        if (Physics.Linecast(_weaponWSFirePos, _endPosition, out raycastHitInfo, ~0, QueryTriggerInteraction.Ignore))
                        {
                            // Adjust the end position to the hit point
                            _endPosition = raycastHitInfo.point;

                            // Update damage if it hit a ship or an object with a damage receiver
                            if (!BeamModule.CheckShipHit(raycastHitInfo, beamModule, _deltaTime))
                            {
                                // If it hit an object with a DamageReceiver script attached, take appropriate action like call a custom method
                                BeamModule.CheckObjectHit(raycastHitInfo, beamModule, _deltaTime);
                            }

                            // ISSUES - the effect may not be visible while firing if:
                            // 1) if the effect despawn time is less than the beam max burst duration (We have a warning in the BeamModule editor)
                            // 2) if the effect does not have looping enabled (this could be more expensive to check)

                            // Add or Move the effects object
                            if (beamModule.effectsObjectPrefabID >= 0)
                            {
                                if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); }

                                if (sscManager != null)
                                {
                                    // If the effect has not been spawned, do now
                                    if (beamModule.effectsItemKey.effectsObjectSequenceNumber == 0)
                                    {
                                        InstantiateEffectsObjectParameters ieParms = new InstantiateEffectsObjectParameters
                                        {
                                            effectsObjectPrefabID = beamModule.effectsObjectPrefabID,
                                            position = _endPosition,
                                            rotation = beamModule.transform.rotation
                                        };

                                        // Instantiate the hit effects
                                        if (sscManager.InstantiateEffectsObject(ref ieParms) != null)
                                        {
                                            if (ieParms.effectsObjectSequenceNumber > 0)
                                            {
                                                // Record the hit effect item key
                                                beamModule.effectsItemKey = new SSCEffectItemKey(ieParms.effectsObjectPrefabID, ieParms.effectsObjectPoolListIndex, ieParms.effectsObjectSequenceNumber);
                                            }
                                        }
                                    }
                                    // Move the existing effects object to the end of the beam
                                    else
                                    {
                                        // Currently we are not checking sequence number matching (for pooled effects) as it is faster and can
                                        // avoid doing an additional GetComponent().
                                        sscManager.MoveEffectsObject(beamModule.effectsItemKey, _endPosition, beamModule.transform.rotation, false);
                                    }
                                }
                            }
                        }
                        // The beam isn't hitting anything AND there is an active effects object
                        else if (beamModule.effectsObjectPrefabID >= 0 && beamModule.effectsItemKey.effectsObjectSequenceNumber > 0)
                        {
                            // Destroy the effects object or return it to the pool
                            if (sscManager == null) { sscManager = SSCManager.GetOrCreateManager(); }

                            if (sscManager != null)
                            {
                                sscManager.DestroyEffectsObject(beamModule.effectsItemKey);
                            }
                        }

                        // Move the beam end (assumes world space)
                        // useWorldspace is enabled in beamModule.InitialiseBeam(..)
                        beamModule.lineRenderer.SetPosition(0, _weaponWSFirePos);
                        beamModule.lineRenderer.SetPosition(1, _endPosition);

                        // Consume weapon power. Consumes upt to 2x power when overheating.
                        if (weapon.rechargeTime > 0f) { weapon.chargeAmount -= _deltaTime * (weapon.heatLevel > weapon.overHeatThreshold ? 2f - weapon.currentPerformance : 1f) / beamModule.dischargeDuration; }

                        weapon.ManageHeat(_deltaTime, 1f);

                        // If we run out of power, de-activate it before weapon starts recharging
                        if (weapon.chargeAmount <= 0f)
                        {
                            weapon.chargeAmount = 0f;

                            // Unassign the beam from this weapon's fire position
                            weapon.beamItemKeyList[beamModule.firePositionIndex] = new SSCBeamItemKey(-1, -1, 0);

                            beamModule.DestroyBeam();
                        }
                    }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR ship.MoveBeam has been called on the wrong beam. isInitialised: " + beamModule.isInitialised +
                  " isBeamEnabled: " + beamModule.isBeamEnabled + " weaponIndex: " + beamModule.weaponIndex + " firePositionIndex: " + beamModule.firePositionIndex); }
            #endif
        }

        private Vector3 GetWeaponWorldBasePosition(Weapon weapon)
        {
            return (trfmRight * weapon.relativePosition.x) +
                   (trfmUp * weapon.relativePosition.y) +
                   (trfmFwd * weapon.relativePosition.z) +
                   trfmPos;
        }

        private Vector3 GetWeaponWorldFireDirection(Weapon weapon)
        {
            // Get relative fire direction
            weaponRelativeFireDirection = weapon.fireDirectionNormalised;

            // If this is a turret, adjust relative fire direction based on turret rotation
            if (weapon.weaponTypeInt == Weapon.TurretProjectileInt || weapon.weaponTypeInt == Weapon.TurretBeamInt)
            {
                // (turret rotation less ship rotate) - (original turret rotation less ship rotation) + relative fire direction
                weaponRelativeFireDirection = (trfmInvRot * weapon.turretPivotX.rotation) * weapon.intPivotYInvRot * weaponRelativeFireDirection;
            }

            // Get weapon world fire direction
            return (trfmRight * weaponRelativeFireDirection.x) +
                   (trfmUp * weaponRelativeFireDirection.y) +
                   (trfmFwd * weaponRelativeFireDirection.z);
        }

        private Vector3 GetWeaponWorldFirePosition(Weapon weapon, Vector3 weaponWorldBasePosition, int firePositionIndex)
        {
            // Check if there are multiple fire positions
            if (weapon.isMultipleFirePositions)
            {
                // Get relative fire position
                weaponRelativeFirePosition = weapon.firePositionList[firePositionIndex];
            }
            else
            {
                // If there is only one fire position, relative position must be the zero vector
                weaponRelativeFirePosition.x = 0f;
                weaponRelativeFirePosition.y = 0f;
                weaponRelativeFirePosition.z = 0f;
            }
            // If this is a turret, adjust relative fire position based on turret rotation
            if (weapon.weaponTypeInt == Weapon.TurretProjectileInt || weapon.weaponTypeInt == Weapon.TurretBeamInt)
            {
                weaponRelativeFirePosition = (trfmInvRot * weapon.turretPivotX.rotation) * weaponRelativeFirePosition;
            }

            return weaponWorldBasePosition +
                    (trfmRight * weaponRelativeFirePosition.x) +
                    (trfmUp * weaponRelativeFirePosition.y) +
                    (trfmFwd * weaponRelativeFirePosition.z);
        }


        /// <summary>
        /// Begin to shut down the thrusters. Optionally override the shutdown duration.
        /// See also shipControlModule.ShutdownThrusterSystems(..)
        /// </summary>
        internal void ShutdownThrusterSystems (bool isInstantShutdown = false)
        {
            // Instant shutdown can be called even while in the process of shutting down.
            if (isThrusterSystemsStarted || isInstantShutdown)
            {
                // Perform an instant shutdown?
                if (isInstantShutdown || thrusterSystemShutdownDuration <= 0f)
                {
                    isThrusterSystemsStarted = false;
                    thrusterSystemShutdownTimer = 0f;
                    thrusterSystemStartupTimer = 0f;

                    SetThrottleAllThrusters(0f);
                }
                else
                {
                    // Activate the shutdown sequence

                    // if startup was in progress, begin at that point
                    if (thrusterSystemStartupTimer > 0f && thrusterSystemStartupDuration > 0f)
                    {
                        thrusterSystemShutdownTimer = (1f - (thrusterSystemStartupTimer / thrusterSystemStartupDuration)) * thrusterSystemShutdownDuration;
                    }
                    else
                    {
                        thrusterSystemShutdownTimer = 0.001f;
                    }

                    // Reset the startup timer so that shutdown can continue
                    thrusterSystemStartupTimer = 0f;
                }
            }
        }

        /// <summary>
        /// Begin to bring the thrusters online. Optionally, override the startup duration.
        /// As soon as the systems begin to start up, isThrusterSystemsStarted will be true.
        /// See also shipControlModule.StartupThrusterSystems(..)
        /// </summary>
        internal void StartupThrusterSystems (bool isInstantStartup = false)
        {
            // Instant startup can be called even while in the process of
            // starting up. This is actually how we trigger the end of the
            // startup process when the timer expires in CheckThrusterSystems(..).

            // Perform an instant startup?
            if (isInstantStartup || thrusterSystemStartupDuration <= 0f)
            {
                thrusterSystemShutdownTimer = 0f;
                thrusterSystemStartupTimer = 0f;

                SetThrottleAllThrusters(1f);
            }
            else
            {
                // Activate the startup sequence

                // if shutdown was in progress, begin at that point
                if (thrusterSystemShutdownTimer > 0f && thrusterSystemShutdownDuration > 0f)
                {
                    thrusterSystemStartupTimer = (1f - (thrusterSystemShutdownTimer / thrusterSystemShutdownDuration)) * thrusterSystemStartupDuration;
                }
                else
                {
                    thrusterSystemStartupTimer = 0.001f;
                }

                // Reset the shutdown timer so that startup can continue
                thrusterSystemShutdownTimer = 0f;
            }

            isThrusterSystemsStarted = true;
        }

        #endregion

        #region Public Non-Static Methods

        #region Set Class Defaults

        public void SetClassDefaults()
        {
            this.initialiseOnAwake = true;

            this.shipPhysicsModel = ShipPhysicsModel.Arcade;

            this.mass = 5000f;
            this.centreOfMass = Vector3.zero;
            this.setCentreOfMassManually = false;
            this.showCOMGizmosInSceneView = true;
            this.showCOLGizmosInSceneView = false;
            this.showCOTGizmosInSceneView = false;

            this.thrusterList = new List<Thruster>(25);

            // Create a default forwards-facing thruster
            Thruster thruster = new Thruster();
            if (thruster != null)
            {
                thruster.name = "Forward Thruster";
                thruster.forceUse = 1;
                thruster.primaryMomentUse = 0;
                thruster.secondaryMomentUse = 0;
                thruster.maxThrust = 100000;
                thruster.thrustDirection = Vector3.forward;
                thruster.relativePosition = Vector3.zero;
                this.thrusterList.Add(thruster);
            }
            isThrusterFXStationary = false;
            isThrusterSystemsStarted = true;
            thrusterSystemStartupDuration = 5f;
            thrusterSystemShutdownDuration = 5f;
            useCentralFuel = true;
            centralFuelLevel = 100f;

            this.wingList = new List<Wing>(5);
            this.controlSurfaceList = new List<ControlSurface>(10);
            this.weaponList = new List<Weapon>(10);

            // Expand editor lists by default
            isThrusterSystemsExpanded = true;
            isThrusterListExpanded = true;
            isWingListExpanded = true;
            isControlSurfaceListExpanded = true;
            isWeaponListExpanded = true;
            isDamageListExpanded = true;
            isMainDamageExpanded = true;
            showDamageInEditor = true;

            this.dragCoefficients = new Vector3(0.5f, 1f, 0.1f);
            this.dragReferenceAreas = new Vector3(10f, 20f, 5f);
            this.centreOfDragXMoment = Vector3.zero;
            this.centreOfDragYMoment = Vector3.zero;
            this.centreOfDragZMoment = Vector3.zero;
            this.angularDragFactor = 1f;
            this.disableDragMoments = false;
            this.dragXMomentMultiplier = 1f;
            this.dragYMomentMultiplier = 1f;
            this.dragZMomentMultiplier = 1f;
            this.wingStallEffect = 1f;

            this.arcadeUseBrakeComponent = true;
            this.arcadeBrakeStrength = 100f;
            this.arcadeBrakeIgnoreMediumDensity = false;
            this.arcadeBrakeMinAcceleration = 25f;

            this.rotationalFlightAssistStrength = 3f;
            this.translationalFlightAssistStrength = 3f;
            this.stabilityFlightAssistStrength = 0f;
            this.brakeFlightAssistStrength = 0f;
            this.brakeFlightAssistMinSpeed = -10f;
            this.brakeFlightAssistMaxSpeed = 10f;

            this.limitPitchAndRoll = false;
            this.maxPitch = 10f;
            this.maxTurnRoll = 15f;
            this.pitchSpeed = 30f;
            this.turnRollSpeed = 30f;
            this.rollControlMode = RollControlMode.YawInput;
            this.pitchRollMatchResponsiveness = 4f;
            this.stickToGroundSurface = false;
            this.avoidGroundSurface = false;
            this.useGroundMatchSmoothing = false;
            this.useGroundMatchLookAhead = false;
            this.orientUpInAir = false;
            this.targetGroundDistance = 5f;
            this.minGroundDistance = 2f;
            this.maxGroundDistance = 8f;
            this.maxGroundCheckDistance = 25f;
            this.groundMatchResponsiveness = 50f;
            this.groundMatchDamping = 50f;
            this.maxGroundMatchAccelerationFactor = 3f;
            this.centralMaxGroundMatchAccelerationFactor = 1f;
            this.groundNormalCalculationMode = GroundNormalCalculationMode.SmoothedNormal;
            this.groundNormalHistoryLength = 5;
            this.groundLayerMask = ~0;

            this.inputControlAxis = InputControlAxis.None;
            this.inputControlLimit = 0f;
            this.inputControlForwardAngle = 0f;
            this.inputControlMovingRigidness = 25f;
            this.inputControlTurningRigidness = 25f;

            this.rollPower = 1f;
            this.pitchPower = 1f;
            this.yawPower = 1f;
            this.steeringThrusterPriorityLevel = 0f;

            this.pilotMomentInput = Vector3.zero;
            this.pilotForceInput = Vector3.zero;
            this.arcadePitchAcceleration = 100f;
            this.arcadeYawAcceleration = 100f;
            this.arcadeRollAcceleration = 250f;
            this.arcadeMaxFlightTurningAcceleration = 500f;
            this.arcadeMaxGroundTurningAcceleration = 500f;

            this.mediumDensity = 1.293f;
            this.gravitationalAcceleration = 9.81f;
            this.gravityDirection = -Vector3.up;

            this.shipDamageModel = ShipDamageModel.Simple;
            this.mainDamageRegion = new DamageRegion();
            this.mainDamageRegion.name = "Main Damage Region";
            this.localisedDamageRegionList = new List<DamageRegion>(10);
            this.useDamageMultipliers = false;
            this.useLocalisedDamageMultipliers = false;
            this.respawnTime = 5f;
            this.collisionRespawnPositionDelay = 5f;
            this.respawningMode = RespawningMode.DontRespawn;
            this.customRespawnPosition = Vector3.zero;
            this.customRespawnRotation = Vector3.zero;
            this.respawnVelocity = Vector3.zero;
            this.respawningPathGUIDHash = 0;
            this.minRumbleDamage = 0f;
            this.maxRumbleDamage = 10f;
            this.stuckTime = 0f;
            this.stuckSpeedThreshold = 0.1f;
            this.stuckAction = StuckAction.DoNothing;
            this.stuckActionPathGUIDHash = 0;

            // By default, all ships on the same faction/side/alliance.
            this.factionId = 0;

            squadronId = -1; // NOT SET

            this.isRadarEnabled = false;
            this.radarBlipSize = 1;
        }

        #endregion

        #region Initialisation

        /// <summary>
        /// Initialise all necessary ship data.
        /// </summary>
        public void Initialise(Transform trfm)
        {
            // Store physics model as a boolean value to save looking up enumeration at runtime
            shipPhysicsModelPhysicsBased = shipPhysicsModel == ShipPhysicsModel.PhysicsBased;

            // New values, better for pipes
            pitchPIDController = new PIDController(0.5f, 0f, 0.1f);
            rollPIDController = new PIDController(0.5f, 0f, 0.1f);
            // For pitch and roll derivative on measurement is disabled so that
            // a) in limit pitch/roll mode we can use local-space pitch and roll measurements
            // b) if the target pitch/roll is quickly moving away from the measured pitch/roll the
            //    PID controller can use a large input to prevent it
            pitchPIDController.derivativeOnMeasurement = false;
            rollPIDController.derivativeOnMeasurement = false;
            ReinitialisePitchRollMatchVariables();

            // Create a new PID controller for ground match
            groundDistPIDController = new PIDController(0f, 0f, 0f);
            // For ground dist derivative on measurement is disabled so that if the target distance is quickly moving away from
            // the measured pitch/roll the PID controller can use a large input to prevent it
            groundDistPIDController.derivativeOnMeasurement = false;
            // Initialise ground dist PID controller variables
            ReinitialiseGroundMatchVariables();

            // Create new PID controllers for input axis control
            inputAxisForcePIDController = new PIDController(0f, 0f, 0f);
            inputAxisMomentPIDController = new PIDController(0f, 0f, 0f);
            inputAxisForcePIDController.derivativeOnMeasurement = false;
            inputAxisMomentPIDController.derivativeOnMeasurement = false;
            ReinitialiseInputControlVariables();

            // Create new PID controllers for stability flight assist
            sFlightAssistPitchPIDController = new PIDController(0f, 0f, 0f);
            sFlightAssistYawPIDController = new PIDController(0f, 0f, 0f);
            sFlightAssistRollPIDController = new PIDController(0f, 0f, 0f);
            ReinitialiseStabilityFlightAssistVariables();

            // Initialise ray for raycasting against the ground
            raycastRay = new Ray(Vector3.zero, Vector3.up);

            // Initialise thruster, wing, weapon and damage region variables
            ReinitialiseThrusterVariables();
            ReinitialiseWingVariables();
            ReinitialiseWeaponVariables();
            ReinitialiseDamageRegionVariables(true);

            // Cache the Weapon FiringButton enumeration values to avoid looking up the enumeration
            // Used in CalculateForceAndMoment(..)
            weaponPrimaryFiringInt = (int)Weapon.FiringButton.Primary;
            weaponSecondaryFiringInt = (int)Weapon.FiringButton.Secondary;
            weaponAutoFiringInt = (int)Weapon.FiringButton.AutoFire;

            // Initialise respawn data
            ReinitialiseRespawnVariables();

            // Initialise input data
            ReinitialiseInputVariables();

            // Get a ship ID integer from the transform ID
            shipId = trfm.GetInstanceID();

            // Initially there is no radar item assigned in the radar system
            radarItemIndex = -1;

            // Added in 1.3.3 for muzzle FX reparenting ONLY.
            // Use with caution as may need to be removed in the future if
            // causes issues like when destroying the ShipControlModule.
            shipTransform = trfm;

            StopBoost();
        }

        /// <summary>
        /// Re-initialises all variables needed when changing the ship physics model.
        /// Call after modifying shipPhysicsModel.
        /// </summary>
        public void ReinitialiseShipPhysicsModel()
        {
            // Store physics model as a boolean value to save looking up enumeration at runtime
            shipPhysicsModelPhysicsBased = shipPhysicsModel == ShipPhysicsModel.PhysicsBased;

            ReinitialisePitchRollMatchVariables();
            ReinitialiseGroundMatchVariables();
            ReinitialiseInputVariables();
            ReinitialiseInputControlVariables();
        }

        /// <summary>
        /// Re-initialises variables related to the stability flight assist.
        /// Call after modifying stabilityFlightAssistStrength.
        /// </summary>
        private void ReinitialiseStabilityFlightAssistVariables()
        {
            // Configure pitch PID controller
            sFlightAssistPitchPIDController.pGain = 0.025f * stabilityFlightAssistStrength;
            sFlightAssistPitchPIDController.iGain = 0.005f * stabilityFlightAssistStrength;
            sFlightAssistPitchPIDController.dGain = 0.01f * stabilityFlightAssistStrength;
            sFlightAssistPitchPIDController.SetInputLimits(-1f, 1f);

            // Configure yaw PID controller
            sFlightAssistYawPIDController.pGain = 0.025f * stabilityFlightAssistStrength;
            sFlightAssistYawPIDController.iGain = 0.005f * stabilityFlightAssistStrength;
            sFlightAssistYawPIDController.dGain = 0.01f * stabilityFlightAssistStrength;
            sFlightAssistYawPIDController.SetInputLimits(-1f, 1f);

            // Configure roll PID controller
            sFlightAssistRollPIDController.pGain = 0.025f * stabilityFlightAssistStrength;
            sFlightAssistRollPIDController.iGain = 0.005f * stabilityFlightAssistStrength;
            sFlightAssistRollPIDController.dGain = 0.01f * stabilityFlightAssistStrength;
            sFlightAssistRollPIDController.SetInputLimits(-1f, 1f);
        }

        /// <summary>
        /// Re-initialises variables related to pitch and roll match.
        /// </summary>
        private void ReinitialisePitchRollMatchVariables()
        {
            if (shipPhysicsModelPhysicsBased)
            {
                // Pitch and roll PID controllers are always constrained to ship physics
                pitchPIDController.SetInputLimits(-1f, 1f);
                rollPIDController.SetInputLimits(-1f, 1f);
            }
            else
            {
                // Pitch and roll PID controllers are not constrained to ship physics
                pitchPIDController.SetInputLimits(Mathf.NegativeInfinity, Mathf.Infinity);
                rollPIDController.SetInputLimits(Mathf.NegativeInfinity, Mathf.Infinity);
            }

            // Reset PID controllers
            pitchPIDController.ResetController();
            rollPIDController.ResetController();
        }

        /// <summary>
        /// Re-initialises variables related to ground distance match calculations.
        /// Call after modifying useGroundMatchSmoothing, groundMatchResponsiveness, groundMatchDamping, 
        /// maxGroundMatchAccelerationFactor, centralMaxGroundMatchAccelerationFactor or groundNormalHistoryLength.
        /// </summary>
        public void ReinitialiseGroundMatchVariables()
        {
            // Calculate height range constant (if we are using a min-max range for target distance)
            float groundMatchHeightRangeConstant = 10f;
            if (useGroundMatchSmoothing)
            {
                // Set the constant to the minimum of: The difference between the min and target distances and the difference
                // between the max and target distances
                groundMatchHeightRangeConstant = targetGroundDistance - minGroundDistance;
                if (groundMatchHeightRangeConstant > maxGroundDistance - targetGroundDistance)
                {
                    groundMatchHeightRangeConstant = maxGroundDistance - targetGroundDistance;
                }
            }

            // P-gain is proportional to responsiveness and inversely proportional to height range
            groundDistPIDController.pGain = 100f * groundMatchResponsiveness / groundMatchHeightRangeConstant;
            // I-gain is half of p-gain
            groundDistPIDController.iGain = 50f * groundMatchResponsiveness / groundMatchHeightRangeConstant;
            // D-gain is proportional to damping and inversely proportional to height range
            groundDistPIDController.dGain = 10f * groundMatchDamping / groundMatchHeightRangeConstant;

            if (shipPhysicsModelPhysicsBased)
            {
                // In physics-based mode, the forces must stay within the limits of the ship thrusters
                groundDistPIDController.SetInputLimits(-1f, 1f);
            }
            else
            {
                // In arcade mode, the forces must stay within a maximum acceleration
                maxGroundMatchAcceleration = Mathf.Pow(10f, maxGroundMatchAccelerationFactor);
                groundDistPIDController.SetInputLimits(-maxGroundMatchAcceleration, maxGroundMatchAcceleration);
                centralMaxGroundMatchAcceleration = Mathf.Pow(10f, centralMaxGroundMatchAccelerationFactor);
            }

            // Reset the PID Controller
            groundDistPIDController.ResetController();

            // Initialise ground normal history
            groundNormalHistory = new Vector3[groundNormalHistoryLength];
            for (int gnIdx = 0; gnIdx < groundNormalHistoryLength; gnIdx++) { groundNormalHistory[gnIdx] = trfmUp; }
        }

        /// <summary>
        /// Re-initialises variables related to Input Control for 2.5D flight.
        /// Call this after modifying inputControlAxis, inputControlMovingRigidness or inputControlTurningRigidness.
        /// </summary>
        public void ReinitialiseInputControlVariables()
        {
            if (shipPhysicsModelPhysicsBased)
            {
                // Set up the physics-based movement PID controller
                // 0.5, 0.1, 0.1 TODO tweak for physics-based
                inputAxisForcePIDController.pGain = inputControlMovingRigidness * 0.1f;
                inputAxisForcePIDController.iGain = inputControlMovingRigidness * 0.02f;
                inputAxisForcePIDController.dGain = inputControlMovingRigidness * 0.02f;

                // Set up the physics-base rotation PID controller
                // 10, 2, 2 TODO tweak for physics-based
                inputAxisMomentPIDController.pGain = inputControlTurningRigidness * 1f;
                inputAxisMomentPIDController.iGain = inputControlTurningRigidness * 0.2f;
                inputAxisMomentPIDController.dGain = inputControlTurningRigidness * 0.2f;

                // Set physical input limits
                inputAxisForcePIDController.SetInputLimits(-1f, 1f);
                inputAxisMomentPIDController.SetInputLimits(-1f, 1f);
            }
            else
            {
                // Set up the arcade movement PID controller
                inputAxisForcePIDController.pGain = inputControlMovingRigidness * 1f;
                inputAxisForcePIDController.iGain = inputControlMovingRigidness * 0.2f;
                inputAxisForcePIDController.dGain = inputControlMovingRigidness * 0.2f;

                // Set up the arcade rotation PID controller
                inputAxisMomentPIDController.pGain = inputControlTurningRigidness * 1f;
                inputAxisMomentPIDController.iGain = inputControlTurningRigidness * 0.2f;
                inputAxisMomentPIDController.dGain = inputControlTurningRigidness * 0.2f;

                // Set non-physical input limits
                inputAxisForcePIDController.SetInputLimits(Mathf.NegativeInfinity, Mathf.Infinity);
                inputAxisMomentPIDController.SetInputLimits(Mathf.NegativeInfinity, Mathf.Infinity);
            }

            inputAxisForcePIDController.ResetController();
            inputAxisMomentPIDController.ResetController();
        }

        /// <summary>
        /// Re-initialises variables related to thrusters. 
        /// Call after modifying thrusterList.
        /// </summary>
        public void ReinitialiseThrusterVariables()
        {
            // Initialise all thrusters
            componentListSize = thrusterList == null ? 0 : thrusterList.Count;
            for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
            {
                thrusterList[componentIndex].Initialise();
            }

            // If thruster systems are not online, immediately turn off all thrusters.
            if (!isThrusterSystemsStarted)
            {
                ShutdownThrusterSystems(true);
            }
        }

        /// <summary>
        /// Re-initialises variables related to wings.
        /// Call after modifying wingList.
        /// </summary>
        public void ReinitialiseWingVariables()
        {
            // Initialise all wings
            componentListSize = wingList == null ? 0 : wingList.Count;
            for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
            {
                wingList[componentIndex].Initialise();
            }
        }

        /// <summary>
        /// Re-initialises variables related to weapons.
        /// Call after modifying weaponList.
        /// </summary>
        public void ReinitialiseWeaponVariables()
        {
            // Initialise all weapons
            componentListSize = weaponList == null ? 0 : weaponList.Count;
            for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
            {
                weaponList[componentIndex].Initialise(trfmInvRot);
            }
        }

        /// <summary>
        /// Re-initialises variables related to damage regions.
        /// Call after modifying mainDamageRegion or localDamageRegionList (or modifying shipDamageModel).
        /// NOTE: Not required if just changing the Health or ShieldHealth
        /// </summary>
        public void ReinitialiseDamageRegionVariables(bool refreshAll = false)
        {
            // Initialise the main damage region
            mainDamageRegion.Initialise();

            numLocalisedDamageRegions = 0;

            // Only initialise localised damage regions if the ship damage model is localised
            if (shipDamageModel == ShipDamageModel.Localised || refreshAll)
            {
                // Initialise all localised damage regions
                numLocalisedDamageRegions = localisedDamageRegionList == null ? 0 : localisedDamageRegionList.Count;
                for (componentIndex = 0; componentIndex < numLocalisedDamageRegions; componentIndex++)
                {
                    localisedDamageRegionList[componentIndex].Initialise();
                }
            }
        }

        /// <summary>
        /// Re-initialises respawn variables using the current position and rotation of the ship.
        /// Also needs to be called after changing respawningMode to RespawningMode.RespawnAtLastPosition.
        /// </summary>
        public void ReinitialiseRespawnVariables()
        {
            // Initialise current respawn position/rotation
            currentRespawnPosition = trfmPos;
            currentRespawnRotation = trfmRot;
            if (respawningMode == RespawningMode.RespawnAtLastPosition)
            {
                currentCollisionRespawnPosition = currentRespawnPosition;
                currentCollisionRespawnRotation = currentRespawnRotation;
                nextCollisionRespawnPosition = currentRespawnPosition;
                nextCollisionRespawnRotation = currentRespawnRotation;
            }
        }

        /// <summary>
        /// Re-initialises variables related to ship inputs. 
        /// Call after modifying thrusterList or controlSurfaceList.
        /// Call after modifying the forceUse/primaryMomentUse/secondaryMomentUse of a thruster or the type of a control surface.
        /// </summary>
        public void ReinitialiseInputVariables()
        {
            // Initially assume that no inputs are used
            longitudinalThrustInputUsed = false;
            horizontalThrustInputUsed = false;
            verticalThrustInputUsed = false;
            pitchMomentInputUsed = false;
            yawMomentInputUsed = false;
            rollMomentInputUsed = false;

            if (shipPhysicsModelPhysicsBased)
            {
                // Loop through all thrusters to check force and moment inputs
                componentListSize = thrusterList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    // Get the thruster we are concerned with
                    thruster = thrusterList[componentIndex];
                    // Check whether force inputs are used for this thruster
                    if (thruster.forceUse == 1 || thruster.forceUse == 2) { longitudinalThrustInputUsed = true; }
                    else if (thruster.forceUse == 3 || thruster.forceUse == 4) { verticalThrustInputUsed = true; }
                    else if (thruster.forceUse == 5 || thruster.forceUse == 6) { horizontalThrustInputUsed = true; }
                    // Check whether moment inputs are used for this thruster
                    if (thruster.primaryMomentUse == 1 || thruster.primaryMomentUse == 2) { rollMomentInputUsed = true; }
                    else if (thruster.primaryMomentUse == 3 || thruster.primaryMomentUse == 4) { pitchMomentInputUsed = true; }
                    else if (thruster.primaryMomentUse == 5 || thruster.primaryMomentUse == 6) { yawMomentInputUsed = true; }
                    if (thruster.secondaryMomentUse == 1 || thruster.secondaryMomentUse == 2) { rollMomentInputUsed = true; }
                    else if (thruster.secondaryMomentUse == 3 || thruster.secondaryMomentUse == 4) { pitchMomentInputUsed = true; }
                    else if (thruster.secondaryMomentUse == 5 || thruster.secondaryMomentUse == 6) { yawMomentInputUsed = true; }
                }

                // Loop through all control surfaces to check moment inputs and brake input
                componentListSize = controlSurfaceList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    // Get the control surface we are concerned with
                    controlSurface = controlSurfaceList[componentIndex];
                    // Check whether moment inputs are used for this control surface
                    if (controlSurface.type == ControlSurface.ControlSurfaceType.Aileron) { rollMomentInputUsed = true; }
                    else if (controlSurface.type == ControlSurface.ControlSurfaceType.Elevator) { pitchMomentInputUsed = true; }
                    else if (controlSurface.type == ControlSurface.ControlSurfaceType.Rudder) { yawMomentInputUsed = true; }
                    else if (controlSurface.type == ControlSurface.ControlSurfaceType.AirBrake) { longitudinalThrustInputUsed = true; }
                }
            }
            else
            {
                // Loop through all thrusters to check force inputs
                componentListSize = thrusterList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    // Get the thruster we are concerned with
                    thruster = thrusterList[componentIndex];
                    // Check whether force inputs are used for this thruster
                    if (thruster.forceUse == 1 || thruster.forceUse == 2) { longitudinalThrustInputUsed = true; }
                    else if (thruster.forceUse == 3 || thruster.forceUse == 4) { verticalThrustInputUsed = true; }
                    else if (thruster.forceUse == 5 || thruster.forceUse == 6) { horizontalThrustInputUsed = true; }
                }

                // In arcade mode, moment inputs only depend on the pitch/roll/yaw accelerations being non-zero
                pitchMomentInputUsed = arcadePitchAcceleration > 0.01f;
                yawMomentInputUsed = arcadeYawAcceleration > 0.01f;
                rollMomentInputUsed = arcadeRollAcceleration > 0.01f;

                // In arcade mode, brakes can count for longitudinal inputs
                if (arcadeUseBrakeComponent) { longitudinalThrustInputUsed = true; }
            }
        }

        #endregion

        #region Update Position And Movement Data

        /// <summary>
        /// Reset the cached velocity data. Typically called
        /// when the ship rigidbody is set to kinematic.
        /// </summary>
        public void ResetVelocityData()
        {
            worldVelocity = Vector3.zero;
            localVelocity = Vector3.zero;
            absLocalVelocity = Vector3.zero;
            worldAngularVelocity = Vector3.zero;
            localAngularVelocity = Vector3.zero;
        }

        /// <summary>
        /// Update position and movement data using data obtained from a transform and a rigidbody.
        /// </summary>
        /// <param name="transform"></param>
        /// <param name="rigidbody"></param>
        public void UpdatePositionAndMovementData(Transform transform, Rigidbody rigidbody)
        {
            // Update data obtained from transform
            trfmPos = transform.position;
            trfmFwd = transform.forward;
            trfmRight = transform.right;
            trfmUp = transform.up;
            trfmRot = transform.rotation;
            trfmInvRot = Quaternion.Inverse(trfmRot);

            // Update data obtained from rigidbody
            rbodyPos = rigidbody.position;
            rbodyRot = rigidbody.rotation;
            rbodyInvRot = Quaternion.Inverse(rbodyRot);
            rbodyFwd = rbodyRot * Vector3.forward;
            rbodyRight = rbodyRot * Vector3.right;
            rbodyUp = rbodyRot * Vector3.up;
            worldVelocity = rigidbody.velocity;
            worldAngularVelocity = rigidbody.angularVelocity;
            localVelocity = rbodyInvRot * worldVelocity;
            localAngularVelocity = rbodyInvRot * worldAngularVelocity;
            rBodyInertiaTensor = rigidbody.inertiaTensor;
        }

        #endregion

        #region Calculate Force and Moment

        /// <summary>
        /// Calculate the equivalent local resultant force and moment for the ship and store them in the supplied reference vectors.
        /// Should be called during FixedUpdate(), and have UpdatePositionAndMovementData() called prior to it.
        /// </summary>
        /// <param name="localResultantForce"></param>
        /// <param name="localResultantMoment"></param>
        public void CalculateForceAndMoment(ref Vector3 localResultantForce, ref Vector3 localResultantMoment)
        {
#if SSC_SHIP_DEBUG
            float initStartTime = Time.realtimeSinceStartup;
#endif

            // TODO: OPTIMISATIONS
            // Reduce number of function calls
            // Use less math functions
            // Remove variable declarations
            // Try not to use transform functions and vector3 functions
            // Do as much precalculation as possible i.e. store a bool for isPhysicsBased etc.

            #region Initialisation

            // Read from Time property getter once
            float _deltaTime = Time.deltaTime;

            // Remember the previous frame's world velocity
            previousFrameWorldVelocity = worldVelocity;

            // Reset local resultant force and moment
            localResultantForce = Vector3.zero;
            localResultantMoment = Vector3.zero;

            // Reset any inputs that aren't used (forceInput and momentInput variables used temporarily)
            forceInput.x = horizontalThrustInputUsed ? pilotForceInput.x : 0f;
            forceInput.y = verticalThrustInputUsed ? pilotForceInput.y : 0f;
            forceInput.z = longitudinalThrustInputUsed ? pilotForceInput.z : 0f;
            momentInput.x = pitchMomentInputUsed ? pilotMomentInput.x : 0f;
            momentInput.y = yawMomentInputUsed ? pilotMomentInput.y : 0f;
            momentInput.z = rollMomentInputUsed ? pilotMomentInput.z : 0f;
            pilotForceInput = forceInput;
            pilotMomentInput = momentInput;
            // Reset force and moment input
            forceInput = Vector3.zero;
            momentInput = Vector3.zero;

            // Reset arcade force and moment vectors
            arcadeForceVector = Vector3.zero;
            arcadeMomentVector = Vector3.zero;

            // Get the ship physics model
            shipPhysicsModelPhysicsBased = shipPhysicsModel == ShipPhysicsModel.PhysicsBased;
            // Get the ship damage model
            shipDamageModelIsSimple = shipDamageModel == ShipDamageModel.Simple;

            #endregion

#if SSC_SHIP_DEBUG
            float initEndTime = Time.realtimeSinceStartup;

            float groundStartTime = Time.realtimeSinceStartup;
#endif

            #region Get Ground Information

            if (limitPitchAndRoll)
            {
                if (stickToGroundSurface || avoidGroundSurface)
                {
                    // TODO: This shouldn't always be centre of the object
                    //raycastRay.origin = trfmPos + (trfmFwd * 3.5f);
                    raycastRay.origin = trfmPos;
                    // If we were on the ground last frame, raycast in the "down" direction indicated by the ground normal
                    if (stickToGroundSurfaceEnabled) { raycastRay.direction = -worldTargetPlaneNormal; }
                    // Otherwise raycast in the ship's down direction
                    else { raycastRay.direction = -trfmUp; }

                    // Perform the raycast
                    if (Physics.Raycast(raycastRay, out raycastHitInfo, maxGroundCheckDistance, groundLayerMask, QueryTriggerInteraction.Ignore))
                    {
                        // Single normal, derived from raycast hit normal
                        // This is the backup for smoothed normals and average height methods
                        worldTargetPlaneNormal = raycastHitInfo.normal;

                        if (groundNormalCalculationMode == GroundNormalCalculationMode.SmoothedNormal)
                        {
                            // Only use smoothed normals if the ground is using a mesh collider
                            // Check for a valid number of triangles, as sometimes can return -1. This potentially happens when
                            // Unity simplifies the collider (called degenerate) to improve performance. 
                            if (raycastHitInfo.collider != null && raycastHitInfo.collider.GetType() == typeof(MeshCollider) && raycastHitInfo.triangleIndex >= 0)
                            {
                                // Get the collider as a mesh collider
                                groundMeshCollider = (MeshCollider)raycastHitInfo.collider;
                                if (groundMeshCollider != null)
                                {
                                    // Check if this mesh collider is the cached one - if it is, we can use
                                    // the previously stored data without having to look it up from the mesh
                                    if (groundMeshCollider != cachedGroundMeshCollider)
                                    {
                                        // If this mesh collider is not the cached one, cache the mesh data for next time
                                        cachedGroundMeshCollider = groundMeshCollider;
                                        // Get the shared mesh being used by the ground collider and check that it isn't null
                                        cachedGroundMesh = groundMeshCollider.sharedMesh;
                                        if (cachedGroundMesh != null)
                                        {
                                            // Get the normals and triangle indices arrays
                                            cachedGroundMeshNormals = cachedGroundMesh.normals;
                                            cachedGroundMeshTriangles = cachedGroundMesh.triangles;
                                        }
                                    }

                                    // Get the normals from the triangle we hit using the cached data
                                    groundNormal0 = cachedGroundMeshNormals[cachedGroundMeshTriangles[raycastHitInfo.triangleIndex * 3 + 0]];
                                    groundNormal1 = cachedGroundMeshNormals[cachedGroundMeshTriangles[raycastHitInfo.triangleIndex * 3 + 1]];
                                    groundNormal2 = cachedGroundMeshNormals[cachedGroundMeshTriangles[raycastHitInfo.triangleIndex * 3 + 2]];
                                    // Get the barycentric coordinate of the point we hit - this gives us information as to
                                    // where in the triangle this point is
                                    barycentricCoord = raycastHitInfo.barycentricCoordinate;
                                    // Interpolate between the three normals using the barycentric coordinate to 
                                    // get the smoothed normal - this is how smooth shading works
                                    worldTargetPlaneNormal = groundMeshCollider.transform.TransformDirection(
                                        barycentricCoord[0] * groundNormal0 +
                                        barycentricCoord[1] * groundNormal1 +
                                        barycentricCoord[2] * groundNormal2).normalized;
                                }
                            }
                        }
                        //else if (groundNormalCalculationMode == GroundNormalCalculationMode.AverageHeight)
                        //{
                        //    float halfRectangleWidth = 3.5f;
                        //    float halfRectangleLength = 5.5f;
                        //    bool raycastFRHit, raycastFLHit, raycastRRHit, raycastRLHit;
                        //    float nUsableNormals = 0f;
                        //    Vector3 raycastFRHitPoint = Vector3.zero;
                        //    Vector3 raycastFLHitPoint = Vector3.zero;
                        //    Vector3 raycastRRHitPoint = Vector3.zero;
                        //    Vector3 raycastRLHitPoint = Vector3.zero;
                        //    RaycastHit testRaycastHitInfo;
                        //    Ray testRaycastRay = new Ray();
                        //    Vector3 averageNormal = Vector3.zero;

                        //    // Front-right raycast
                        //    testRaycastRay.origin = trfmPos + (trfmRight * halfRectangleWidth) + (trfmFwd * halfRectangleLength);
                        //    testRaycastRay.direction = -trfmUp;
                        //    if (Physics.Raycast(testRaycastRay, out testRaycastHitInfo, maxGroundCheckDistance, groundLayerMask, QueryTriggerInteraction.Ignore))
                        //    {
                        //        raycastFRHit = true;
                        //        raycastFRHitPoint = testRaycastHitInfo.point;
                        //    }
                        //    else { raycastFRHit = false; }

                        //    // Front-left raycast
                        //    testRaycastRay.origin = trfmPos - (trfmRight * halfRectangleWidth) + (trfmFwd * halfRectangleLength);
                        //    testRaycastRay.direction = -trfmUp;
                        //    if (Physics.Raycast(testRaycastRay, out testRaycastHitInfo, maxGroundCheckDistance, groundLayerMask, QueryTriggerInteraction.Ignore))
                        //    {
                        //        raycastFLHit = true;
                        //        raycastFLHitPoint = testRaycastHitInfo.point;
                        //    }
                        //    else { raycastFLHit = false; }

                        //    // Rear-right raycast
                        //    testRaycastRay.origin = trfmPos + (trfmRight * halfRectangleWidth) - (trfmFwd * halfRectangleLength);
                        //    testRaycastRay.direction = -trfmUp;
                        //    if (Physics.Raycast(testRaycastRay, out testRaycastHitInfo, maxGroundCheckDistance, groundLayerMask, QueryTriggerInteraction.Ignore))
                        //    {
                        //        raycastRRHit = true;
                        //        raycastRRHitPoint = testRaycastHitInfo.point;
                        //    }
                        //    else { raycastRRHit = false; }

                        //    // Rear-left raycast
                        //    testRaycastRay.origin = trfmPos - (trfmRight * halfRectangleWidth) - (trfmFwd * halfRectangleLength);
                        //    testRaycastRay.direction = -trfmUp;
                        //    if (Physics.Raycast(testRaycastRay, out testRaycastHitInfo, maxGroundCheckDistance, groundLayerMask, QueryTriggerInteraction.Ignore))
                        //    {
                        //        raycastRLHit = true;
                        //        raycastRLHitPoint = testRaycastHitInfo.point;
                        //    }
                        //    else { raycastRLHit = false; }

                        //    if (raycastFLHit && raycastFRHit && raycastRRHit)
                        //    {
                        //        nUsableNormals += 1f;
                        //        averageNormal += Vector3.Cross(raycastRRHitPoint - raycastFRHitPoint, raycastFLHitPoint - raycastFRHitPoint).normalized;
                        //    }
                        //    if (raycastFRHit && raycastRRHit && raycastRLHit)
                        //    {
                        //        nUsableNormals += 1f;
                        //        averageNormal += Vector3.Cross(raycastRLHitPoint - raycastRRHitPoint, raycastFRHitPoint - raycastRRHitPoint).normalized;
                        //    }
                        //    if (raycastRRHit && raycastRLHit && raycastFLHit)
                        //    {
                        //        nUsableNormals += 1f;
                        //        averageNormal += Vector3.Cross(raycastFLHitPoint - raycastRLHitPoint, raycastRRHitPoint - raycastRLHitPoint).normalized;
                        //    }
                        //    if (raycastRLHit && raycastFLHit && raycastFRHit)
                        //    {
                        //        nUsableNormals += 1f;
                        //        averageNormal += Vector3.Cross(raycastFRHitPoint - raycastFLHitPoint, raycastRLHitPoint - raycastFLHitPoint).normalized;
                        //    }

                        //    if (nUsableNormals > 0f) { worldTargetPlaneNormal = (averageNormal / nUsableNormals).normalized; }
                        //}

                        // Calculate a normal averaged from the last few frames
                        UpdateGroundNormalHistory(worldTargetPlaneNormal);
                        worldTargetPlaneNormal = GetDampedGroundNormal();

                        // Get target plane normal in local space
                        localTargetPlaneNormal = trfmInvRot * worldTargetPlaneNormal;

                        // Calculate distance to the ground in a direction parallel to the ground normal
                        // (from the ground directly beneath the ship)
                        currentPerpendicularGroundDist = Vector3.Dot(trfmPos - raycastHitInfo.point, worldTargetPlaneNormal);

                        //Debug.DrawRay(raycastRay.origin, raycastRay.direction.normalized * maxGroundCheckDistance, Color.blue);

                        // If required, use look ahead to check if there is an obstacle ahead we will need to go over
                        if (useGroundMatchLookAhead)
                        {
                            float maxRaycastDistance;
                            RaycastHit secondaryRaycastHitInfo;

                            // Loop through a series of different look ahead times, starting at half a second away
                            for (float groundMatchLookAheadTime = 0.5f; groundMatchLookAheadTime > 0.01f; groundMatchLookAheadTime -= 0.1f)
                            {
                                // Calculate the corresponding look ahead distance
                                float groundMatchLookAheadDistance = groundMatchLookAheadTime * localVelocity.z;

                                // Calculate the raycast direction
                                raycastRay.direction = (-currentPerpendicularGroundDist * worldTargetPlaneNormal) + (groundMatchLookAheadDistance * trfmFwd);
                                // Calculate the required raycast length to go down the same distance as the standard ground check
                                maxRaycastDistance = Mathf.Sqrt((currentPerpendicularGroundDist * currentPerpendicularGroundDist) +
                                    (groundMatchLookAheadDistance * groundMatchLookAheadDistance)) * (maxGroundCheckDistance / currentPerpendicularGroundDist);

                                // Perform the raycast
                                if (Physics.Raycast(raycastRay, out secondaryRaycastHitInfo, maxRaycastDistance, groundLayerMask,
                                    QueryTriggerInteraction.Ignore))
                                {
                                    // Calculate the perpendicular ground distance (projected onto the target normal)
                                    float futurePerpendicularGroundDistance = Vector3.Dot(trfmPos - secondaryRaycastHitInfo.point, worldTargetPlaneNormal);
                                    // If the perpendicular ground distance we have calculated is LESS than the current 
                                    // perpendicular ground distance (i.e. for the ground beneath the ship), use this
                                    // new ground distance instead
                                    if (futurePerpendicularGroundDistance <= currentPerpendicularGroundDist)
                                    {
                                        currentPerpendicularGroundDist = futurePerpendicularGroundDistance;
                                        //Debug.DrawRay(raycastRay.origin, raycastRay.direction.normalized * maxRaycastDistance, Color.green);
                                        // Exit the loop
                                        break;
                                    }
                                    //else
                                    //{
                                    //    Debug.DrawRay(raycastRay.origin, raycastRay.direction.normalized * maxRaycastDistance, Color.red);
                                    //}
                                }
                            }
                        }

                        // Set relevant variables, to indicate to later code that we have found a ground surface
                        limitPitchAndRollEnabled = true;
                        stickToGroundSurfaceEnabled = true;
                    }
                    else
                    {
                        // If we were on the ground on the previous frame, and now we are not,
                        // reset the PID controller to prevent spikes when we go back onto the ground again
                        if (stickToGroundSurfaceEnabled) { groundDistPIDController.ResetController(); }

                        limitPitchAndRollEnabled = orientUpInAir;
                        stickToGroundSurfaceEnabled = false;
                        if (limitPitchAndRollEnabled)
                        {
                            worldTargetPlaneNormal = Vector3.up;
                            // Update ground normal history
                            UpdateGroundNormalHistory(worldTargetPlaneNormal);
                            // Get target plane normal in local space
                            localTargetPlaneNormal = trfmInvRot * worldTargetPlaneNormal;
                        }
                        else
                        {
                            // Update ground normal history
                            UpdateGroundNormalHistory(trfmUp);
                        }
                    }
                }
                else
                {
                    limitPitchAndRollEnabled = true;
                    stickToGroundSurfaceEnabled = false;
                    worldTargetPlaneNormal = Vector3.up;
                    // Get target plane normal in local space
                    localTargetPlaneNormal = trfmInvRot * worldTargetPlaneNormal;
                }
            }
            else
            {
                limitPitchAndRollEnabled = false;
                stickToGroundSurfaceEnabled = false;
            }

            #endregion

#if SSC_SHIP_DEBUG
            float groundEndTime = Time.realtimeSinceStartup;
#endif

            #region Gravity

            // Calculate acceleration due to gravity in local space (storing the value for use later)
            localGravityAcceleration = trfmInvRot * gravityDirection * gravitationalAcceleration;
            // Then add it to local resultant force vector (multiplying by mass to turn it into a force)
            localResultantForce += localGravityAcceleration * mass;

            #endregion

#if SSC_SHIP_DEBUG
            float inputStartTime = Time.realtimeSinceStartup;
#endif

            #region Calculate Final Inputs

            // Calculate final input

            // | Assists/limiters info |
            // Rotational flight assist:
            // - Always on
            // - If the pilot has released an axial input but we are still spinning on that axis,
            //   modify the input to slow down the spin
            // Translational flight assist:
            // - Always on
            // - If the pilot has released a translational input but we are still moving on that axis,
            //   modify the input to slow down the movement
            // - Is overriden on y-axis by stick to ground surface
            // - Currently does not apply on z-axis
            // - Isn't available in arcade mode (it is instead replaced by velocity match code)
            // Brake flight assist:
            // - Off by default
            // - Translational flight assist on z-axis only
            // - Only applies when no z-axis input
            // - Only applies when fwd/back movement is non-zero and not close to zero
            // - Can be overridden in forwards direction by AutoCruise on PlayerInputModule
            // Limit pitch and roll:
            // - Overrides x and z axes of rotational flight assist
            // - Causes y-axis rotational flight assist to apply on an axis normal to the target plane
            // Stick to ground surface
            // - Overrides y axis of pilot force input

            // Moment inputs
            // - Rotational flight assist

            if (limitPitchAndRollEnabled)
            {
                // Ground angle match assist: Calculate a target pitch and roll from the ground angle
                // Pitch is still affected by pilot input but is clamped between min and max pitch
                // Roll is controlled by yaw input or strafe input
                // Calculate target plane normal in local space

                // Calculate target pitch (relative to the ground normal) in degrees
                // This is always dependent on the pitch input
                pitchAmountInput = pilotMomentInput.x * maxPitch;

                // Calculate target roll (relative to the ground normal) in degrees
                // a) From the yaw input
                if (rollControlMode == RollControlMode.YawInput) { rollAmountInput = pilotMomentInput.y * maxTurnRoll; }
                // b) From the strafe input
                else { rollAmountInput = pilotForceInput.x * maxTurnRoll; }

                // Calculate the maximum possible change in pitch/roll for this frame
                maxFramePitchDelta = _deltaTime * pitchSpeed;
                maxFrameRollDelta = _deltaTime * turnRollSpeed;

                // Move the pitch amount towards the target pitch input
                if (pitchAmountInput >= pitchAmount - maxFramePitchDelta && pitchAmountInput <= pitchAmount + maxFramePitchDelta) { pitchAmount = pitchAmountInput; }
                else if (pitchAmountInput > pitchAmount) { pitchAmount += maxFramePitchDelta; }
                else { pitchAmount -= maxFramePitchDelta; }

                // Move the roll amount towards the target roll input
                if (rollAmountInput >= rollAmount - maxFrameRollDelta && rollAmountInput <= rollAmount + maxFrameRollDelta) { rollAmount = rollAmountInput; }
                else if (rollAmountInput > rollAmount) { rollAmount += maxFrameRollDelta; }
                else { rollAmount -= maxFrameRollDelta; }

                // Calculate target local-space roll and pitch in degrees
                targetLocalPitch = ((float)Math.Atan2(localTargetPlaneNormal.z, localTargetPlaneNormal.y) * Mathf.Rad2Deg) + pitchAmount;
                targetLocalRoll = ((float)Math.Atan2(localTargetPlaneNormal.x, localTargetPlaneNormal.y) * Mathf.Rad2Deg) + rollAmount;

                //Debug.Log("Target pitch: " + targetLocalPitch.ToString("00.00") + ", target roll: " + targetLocalRoll.ToString("00.00"));

                if (shipPhysicsModelPhysicsBased)
                {
                    // Use the PID controllers to calculate the required moment inputs on the x and z axes
                    momentInput.z = rollPIDController.RequiredInput(targetLocalRoll, 0f, _deltaTime);
                    momentInput.x = pitchPIDController.RequiredInput(targetLocalPitch, 0f, _deltaTime);
                    momentInput.y = 0f;
                }
                else
                {
                    // Use the PID controllers to calculate the required moments on the x and z axes
                    arcadeMomentVector.z = rollPIDController.RequiredInput(targetLocalRoll, 0f, _deltaTime) * 50f * pitchRollMatchResponsiveness;
                    arcadeMomentVector.x = pitchPIDController.RequiredInput(targetLocalPitch, 0f, _deltaTime) * 50f * pitchRollMatchResponsiveness;
                    arcadeMomentVector.y = 0f;
                    // Completely reset moment input here
                    momentInput = Vector3.zero;
                }

                // Apply typical yaw rotation with respect to ground normal instead of local y coordinate
                // TODO: Surely this line can be optimised...?
                yawVelocity = Vector3.Dot(Vector3.Project(localAngularVelocity * Mathf.Rad2Deg, localTargetPlaneNormal), localTargetPlaneNormal);
                if (shipPhysicsModelPhysicsBased)
                {
                    // Activate rotational flight assist if pilot releases an input 
                    // or if input is in direction opposing rotational velocity
                    if ((pilotMomentInput.y > 0f) == (yawVelocity < 0f) || pilotMomentInput.y == 0f)
                    {
                        // Calculate the flight assist value on the ground normal axis
                        flightAssistValue = -yawVelocity / 100f * rotationalFlightAssistStrength;
                        // Choose whichever value has the highest absolute value: The flight assist value or the pilot input value
                        if ((flightAssistValue > 0f ? flightAssistValue : -flightAssistValue) >
                            (pilotMomentInput.y > 0f ? pilotMomentInput.y : -pilotMomentInput.y))
                        {
                            momentInput += localTargetPlaneNormal * flightAssistValue;
                        }
                        else { momentInput += localTargetPlaneNormal * pilotMomentInput.y; }
                    }
                    else { momentInput += localTargetPlaneNormal * pilotMomentInput.y; }
                }
                else
                {
                    momentInput += localTargetPlaneNormal * pilotMomentInput.y;
                    // Activate arcade rotational flight assist if pilot releases an input 
                    // or if input is in direction opposing rotational velocity
                    if ((pilotMomentInput.y > 0f) == (yawVelocity < 0f) || pilotMomentInput.y == 0f)
                    {
                        // Calculate the flight assist value on the ground normal axis
                        arcadeMomentVector += localTargetPlaneNormal * -yawVelocity * rotationalFlightAssistStrength;
                    }
                }
            }
            else
            {
                // Rotational flight assist: Input to counteract rotational velocity
                if (shipPhysicsModelPhysicsBased)
                {
                    // Activate rotational flight assist if pilot releases an input 
                    // or if input is in direction opposing rotational velocity
                    if ((pilotMomentInput.x > 0f) == (localAngularVelocity.x < 0f) || pilotMomentInput.x == 0f)
                    {
                        // Calculate the flight assist value on this axis
                        flightAssistValue = -localAngularVelocity.x * Mathf.Rad2Deg / 100f * rotationalFlightAssistStrength;
                        // Choose whichever value has the highest absolute value: The flight assist value or the pilot input value
                        if ((flightAssistValue > 0f ? flightAssistValue : -flightAssistValue) >
                            (pilotMomentInput.x > 0f ? pilotMomentInput.x : -pilotMomentInput.x)) { momentInput.x = flightAssistValue; }
                        else { momentInput.x = pilotMomentInput.x; }
                    }
                    else { momentInput.x = pilotMomentInput.x; }
                    if ((pilotMomentInput.y > 0f) == (localAngularVelocity.y < 0f) || pilotMomentInput.y == 0f)
                    {
                        // Calculate the flight assist value on this axis
                        flightAssistValue = -localAngularVelocity.y * Mathf.Rad2Deg / 100f * rotationalFlightAssistStrength;
                        // Choose whichever value has the highest absolute value: The flight assist value or the pilot input value
                        if ((flightAssistValue > 0f ? flightAssistValue : -flightAssistValue) >
                            (pilotMomentInput.y > 0f ? pilotMomentInput.y : -pilotMomentInput.y)) { momentInput.y = flightAssistValue; }
                        else { momentInput.y = pilotMomentInput.y; }
                    }
                    else { momentInput.y = pilotMomentInput.y; }
                    if ((pilotMomentInput.z > 0f) == (localAngularVelocity.z > 0f) || pilotMomentInput.z == 0f)
                    {
                        // Calculate the flight assist value on this axis
                        flightAssistValue = localAngularVelocity.z * Mathf.Rad2Deg / 100f * rotationalFlightAssistStrength;
                        // Choose whichever value has the highest absolute value: The flight assist value or the pilot input value
                        if ((flightAssistValue > 0f ? flightAssistValue : -flightAssistValue) >
                            (pilotMomentInput.z > 0f ? pilotMomentInput.z : -pilotMomentInput.z)) { momentInput.z = flightAssistValue; }
                        else { momentInput.z = pilotMomentInput.z; }
                    }
                    else { momentInput.z = pilotMomentInput.z; }
                }
                else
                {
                    // Activate arcade rotational flight assist if pilot releases an input 
                    // or if input is in direction opposing rotational velocity
                    momentInput = pilotMomentInput;
                    if ((pilotMomentInput.x > 0f) == (localAngularVelocity.x < 0f) || pilotMomentInput.x == 0f)
                    {
                        arcadeMomentVector.x = -localAngularVelocity.x * Mathf.Rad2Deg * rotationalFlightAssistStrength;
                    }
                    if ((pilotMomentInput.y > 0f) == (localAngularVelocity.y < 0f) || pilotMomentInput.y == 0f)
                    {
                        arcadeMomentVector.y = -localAngularVelocity.y * Mathf.Rad2Deg * rotationalFlightAssistStrength;
                    }
                    if ((pilotMomentInput.z > 0f) == (localAngularVelocity.z > 0f) || pilotMomentInput.z == 0f)
                    {
                        arcadeMomentVector.z = localAngularVelocity.z * Mathf.Rad2Deg * rotationalFlightAssistStrength;
                    }
                }
            }

            if (stabilityFlightAssistStrength > 0f)
            {
                // TODO I need to optimise the calculation section

                #region Calculate Current Pitch, Yaw and Roll

                // TODO I really need to optimise this properly
                // Calculate current pitch of the ship
                Vector3 pitchProjectionPlaneNormal = Vector3.Cross(Vector3.up, TransformForward);
                Vector3 pitchProjectedUpDirection = Vector3.ProjectOnPlane(TransformUp, pitchProjectionPlaneNormal);
                float stabilityCurrentPitch = Vector3.SignedAngle(Vector3.up, pitchProjectedUpDirection, pitchProjectionPlaneNormal);
                // Measure pitch with respect to target pitch direction
                stabilityCurrentPitch -= sFlightAssistTargetPitch;
                if (stabilityCurrentPitch > 180f) { stabilityCurrentPitch -= 360f; }
                else if (stabilityCurrentPitch < -180f) { stabilityCurrentPitch += 360f; }

                // Calculate current yaw of the ship
                // TODO this can be massively optimised by just setting the y-value to zero
                Vector3 yawProjectionPlaneNormal = Vector3.up;
                Vector3 yawProjectedForwardDirection = Vector3.ProjectOnPlane(TransformForward, yawProjectionPlaneNormal);
                float stabilityCurrentYaw = Vector3.SignedAngle(Vector3.forward, yawProjectedForwardDirection, yawProjectionPlaneNormal);
                // Measure yaw with respect to target yaw direction
                stabilityCurrentYaw -= sFlightAssistTargetYaw;
                if (stabilityCurrentYaw > 180f) { stabilityCurrentYaw -= 360f; }
                else if (stabilityCurrentYaw < -180f) { stabilityCurrentYaw += 360f; }

                // Calculate current roll of the ship
                Vector3 rollProjectionPlaneNormal = TransformForward;
                rollProjectionPlaneNormal.y = 0f;
                Vector3 rollProjectedUpDirection = Vector3.ProjectOnPlane(TransformUp, rollProjectionPlaneNormal);
                float stabilityCurrentRoll = -Vector3.SignedAngle(Vector3.up, rollProjectedUpDirection, rollProjectionPlaneNormal);
                // Measure roll with respect to target roll direction
                stabilityCurrentRoll -= sFlightAssistTargetRoll;
                if (stabilityCurrentRoll > 180f) { stabilityCurrentRoll -= 360f; }
                else if (stabilityCurrentRoll < -180f) { stabilityCurrentRoll += 360f; }

                #endregion

                #region Update Target Pitch, Yaw and Roll Values

                // How this works: There are two (mutually exclusive) modes of operation:
                // 1. Setting target values for pitch/yaw/roll
                // 2. Applying the stability assist for pitch/yaw/roll
                // For example, if the target pitch is being set, this means that the stability assist will not be used
                // for pitch in that frame
                bool allowPitchStabilityAssist = true;
                bool allowYawStabilityAssist = true;
                bool allowRollStabilityAssist = true;

                // Check if the pilot is initiating a manoeuvre on any of the three axes via pilot input
                bool initiatingPitchManoeuvre = pilotMomentInput.x > 0.01f || pilotMomentInput.x < -0.01f;
                bool initiatingYawManoeuvre = pilotMomentInput.y > 0.01f || pilotMomentInput.y < -0.01f;
                bool initiatingRollManoeuvre = pilotMomentInput.z > 0.01f || pilotMomentInput.z < -0.01f;

                // If there is currently some pilot pitch/yaw/roll input...
                if (initiatingPitchManoeuvre || initiatingRollManoeuvre || initiatingYawManoeuvre)
                {
                    // ... set the target pitch/yaw/roll
                    sFlightAssistTargetPitch += stabilityCurrentPitch;
                    sFlightAssistPitchPIDController.ResetController();
                    sFlightAssistTargetYaw += stabilityCurrentYaw;
                    sFlightAssistYawPIDController.ResetController();
                    sFlightAssistTargetRoll += stabilityCurrentRoll;
                    sFlightAssistRollPIDController.ResetController();
                    allowPitchStabilityAssist = false;
                    allowYawStabilityAssist = false;
                    allowRollStabilityAssist = false;

                    // If a manouevre has been initiated on a given axis via pilot input, remember it
                    // We can hence know when we are rotating on a particular axis due to pilot input
                    // or due to drag moments etc and act accordingly
                    // Note that pitch can affect yaw and vice versa
                    if (initiatingPitchManoeuvre || initiatingYawManoeuvre)
                    {
                        sFlightAssistRotatingPitch = true; 
                        sFlightAssistRotatingYaw = true;
                    }
                    if (initiatingRollManoeuvre) { sFlightAssistRotatingRoll = true; }
                }
                // Otherwise...
                else
                {
                    // If there is a significant pitch assist input to correct a pitch manoeuvre...
                    if (sFlightAssistRotatingPitch && (momentInput.x > 0.3f || momentInput.x < -0.3f))
                    {
                        // ... set the target pitch
                        sFlightAssistTargetPitch += stabilityCurrentPitch;
                        sFlightAssistPitchPIDController.ResetController();
                        allowPitchStabilityAssist = false;
                    }
                    // When the pitch assist moment input has reduced enough, we can assume we have finished
                    // any pitch manoeuvres initiated by the pilot
                    else { sFlightAssistRotatingPitch = false; }
                    // If there is a significant yaw assist input to correct a yaw manoeuvre...
                    if (sFlightAssistRotatingYaw && (momentInput.y > 0.3f || momentInput.y < -0.3f))
                    {
                        // ... set the target yaw
                        sFlightAssistTargetYaw += stabilityCurrentYaw;
                        sFlightAssistYawPIDController.ResetController();
                        allowYawStabilityAssist = false;
                    }
                    // When the yaw assist moment input has reduced enough, we can assume we have finished
                    // any yaw manoeuvres initiated by the pilot
                    else { sFlightAssistRotatingYaw = false; }
                    // If there is a significant roll assist input to correct a roll manoeuvre...
                    if (sFlightAssistRotatingRoll && (momentInput.z > 0.3f || momentInput.z < -0.3f))
                    {
                        // ... set the target roll
                        sFlightAssistTargetRoll += stabilityCurrentRoll;
                        sFlightAssistRollPIDController.ResetController();
                        allowRollStabilityAssist = false;
                    }
                    // When the roll assist moment input has reduced enough, we can assume we have finished
                    // any roll manoeuvres initiated by the pilot
                    else { sFlightAssistRotatingRoll = false; }
                }

                #endregion

                #region Apply Stability Assist

                if (allowPitchStabilityAssist)
                {
                    // Get input from pitch PID controller and apply it
                    momentInput.x += sFlightAssistPitchPIDController.RequiredInput(0f, stabilityCurrentPitch, _deltaTime);
                }
                
                if (allowYawStabilityAssist)
                {
                    // Get input from yaw PID controller and apply it
                    momentInput.y += sFlightAssistYawPIDController.RequiredInput(0f, stabilityCurrentYaw, _deltaTime);
                }

                if (allowRollStabilityAssist)
                {
                    // Get input from roll PID controller and apply it
                    momentInput.z += sFlightAssistRollPIDController.RequiredInput(0f, stabilityCurrentRoll, _deltaTime);
                }

                #endregion
            }

            if (shipPhysicsModelPhysicsBased)
            {
                // Scale moment inputs in physics-based mode - they generally need to be a lot less than force inputs
                momentInput.x *= pitchPower;
                momentInput.y *= yawPower;
                momentInput.z *= rollPower;

                // Clamp moment inputs to be between -1 and 1. We don't do this in arcade mode so that moment assists/limits
                // can use more than the specified maximum moments if necessary
                if (momentInput.x > 1f) { momentInput.x = 1f; }
                else if (momentInput.x < -1f) { momentInput.x = -1f; }
                if (momentInput.y > 1f) { momentInput.y = 1f; }
                else if (momentInput.y < -1f) { momentInput.y = -1f; }
                if (momentInput.z > 1f) { momentInput.z = 1f; }
                else if (momentInput.z < -1f) { momentInput.z = -1f; }
            }

            // Force inputs
            // - Translational flight assist (physics-based only)
            // - Stick to ground surface (physics-based only: arcade equivalent is applied separately)

            if (shipPhysicsModelPhysicsBased)
            {
                // Translational flight assist: Input to counteract translational velocity

                // Activate translational flight assist if pilot releases an input 
                // or if input is in direction opposing velocity
                if ((pilotForceInput.x > 0f) == (localVelocity.x < 0f) || pilotForceInput.x == 0f)
                {
                    // Calculate the flight assist value on this axis
                    flightAssistValue = -localVelocity.x / 100f * translationalFlightAssistStrength;
                    if (flightAssistValue > 1f) { flightAssistValue = 1f; }
                    else if (flightAssistValue < -1f) { flightAssistValue = -1f; }
                    // Choose whichever value has the highest absolute value: The flight assist value or the pilot input value
                    if ((flightAssistValue > 0f ? flightAssistValue : -flightAssistValue) >
                        (pilotForceInput.x > 0f ? pilotForceInput.x : -pilotForceInput.x)) { forceInput.x = flightAssistValue; }
                    else { forceInput.x = pilotForceInput.x; }
                }
                else { forceInput.x = pilotForceInput.x; }

                if (stickToGroundSurfaceEnabled)
                {
                    // Get input from ground distance PID controller
                    forceInput.y = groundDistPIDController.RequiredInput(targetGroundDistance, currentPerpendicularGroundDist, _deltaTime);

                    // NOTE: This hasn't been tested yet in physics-model mode
                    if (avoidGroundSurface)
                    {
                        // Only override user input if the ship is going below the targetGroundDistance
                        forceInput.y = forceInput.y > 0f ? forceInput.y : pilotForceInput.y;
                    }
                }
                else
                {
                    // Activate translational flight assist if pilot releases an input 
                    // or if input is in direction opposing velocity
                    if ((pilotForceInput.y > 0f) == (localVelocity.y < 0f) || pilotForceInput.y == 0f)
                    {
                        // Calculate the flight assist value on this axis
                        flightAssistValue = -localVelocity.y / 100f * translationalFlightAssistStrength;
                        if (flightAssistValue > 1f) { flightAssistValue = 1f; }
                        else if (flightAssistValue < -1f) { flightAssistValue = -1f; }
                        // Choose whichever value has the highest absolute value: The flight assist value or the pilot input value
                        if ((flightAssistValue > 0f ? flightAssistValue : -flightAssistValue) >
                            (pilotForceInput.y > 0f ? pilotForceInput.y : -pilotForceInput.y)) { forceInput.y = flightAssistValue; }
                        else { forceInput.y = pilotForceInput.y; }
                    }
                    else { forceInput.y = pilotForceInput.y; }
                }

                // Force input on z-axis is always just pilot input
                forceInput.z = pilotForceInput.z;
            }
            else
            {
                forceInput.x = pilotForceInput.x;
                // In arcade mode, stick-to-ground force isn't achieved through thrusters
                // Currently this implementation won't allow for thrust pushing up/down from ground
                // to activate particle trails in non-physics-based mode...
                forceInput.y = stickToGroundSurfaceEnabled ? 0f : pilotForceInput.y;
                forceInput.z = pilotForceInput.z;
            }

            #region Brake flight assist
            // Pilot or AI releases input and ship is moving forwards or backwards
            // Works within the speed range configured.
            // Does not make adjustments if velocity is very near zero.
            if (brakeFlightAssistStrength > 0f && forceInput.z == 0f && localVelocity.z > brakeFlightAssistMinSpeed && localVelocity.z < brakeFlightAssistMaxSpeed && (localVelocity.z < -0.01f || localVelocity.z > 0.01f))
            {
                // Max brakeFlightAssistStrength is 10, so div by 10 (x 0.1)
                // Apply thrust in the opposite direction to current z-axis velocity
                flightAssistValue = (localVelocity.z < 0f ? brakeFlightAssistStrength : -brakeFlightAssistStrength) * 0.1f;

                // Dampen based on how close velo is to 0 using a x^2 curve when velo between -10 and +10 m/s.
                if (localVelocity.z > 0f && localVelocity.z < 10f) { flightAssistValue *= localVelocity.z / 10f; }
                else if (localVelocity.z < 0f && localVelocity.z > -10f) { flightAssistValue *= localVelocity.z / -10f; }

                // Clamp -1.0 to 1.0
                if (flightAssistValue > 1f) { flightAssistValue = 1f; }
                else if (flightAssistValue < -1f) { flightAssistValue = -1f; }

                forceInput.z = flightAssistValue;
            }
            #endregion

            #region Input Control - 2.5D
            // TODO probably can significantly optimise the input control axis code

            if ((int)inputControlAxis > 0)
            {
                // Y axis for side-view OR top-down flight
                if ((int)inputControlAxis == 2)
                {
                    // Project the transform right direction onto the XZ plane
                    Vector3 flatTrfmRight = TransformRight;
                    flatTrfmRight.y = 0f;
                    // Project the transform forward direction onto the XZ plane...
                    Vector3 targetForward = TransformForward;
                    targetForward.y = 0f;
                    // ... then onto the plane of the ship's up and forwards directions while keeping it on the XZ plane
                    // To do this step we subtract the projection of this vector onto flatTrfmRight
                    targetForward -= (Vector3.Dot(targetForward, flatTrfmRight) / Vector3.Dot(flatTrfmRight, flatTrfmRight)) * flatTrfmRight;
                    // Measure the angle from this projected forwards direction (which will be the target) to the current forwards direction
                    float inputControlLimitAngleDelta = Vector3.SignedAngle(targetForward, TransformForward, TransformRight);

                    if (shipPhysicsModelPhysicsBased)
                    {
                        // Physics-based: Set the inputs to counteract any displacement from the target position/rotation
                        forceInput.y = inputAxisForcePIDController.RequiredInput(inputControlLimit, TransformPosition.y, _deltaTime);
                        momentInput.x = inputAxisMomentPIDController.RequiredInput(0f, inputControlLimitAngleDelta, _deltaTime);
                    }
                    else
                    {
                        // Arcade: Use arcade force/moment to counteract any displacement from the target position/rotation
                        arcadeForceVector.y += inputAxisForcePIDController.RequiredInput(inputControlLimit, TransformPosition.y, _deltaTime);
                        arcadeMomentVector.x += inputAxisMomentPIDController.RequiredInput(0f, inputControlLimitAngleDelta, _deltaTime);
                        // Set inputs to zero
                        forceInput.y = 0f;
                        momentInput.x = 0f;
                    }
                }
                // X axis for side-scroller-like flight
                else if ((int)inputControlAxis == 1)
                {
                    // Calculate the normal to the plane we want to remain in
                    Vector3 targetInputControlPlaneNormal = Quaternion.Euler(0f, inputControlForwardAngle, 0f) * Vector3.right;

                    // Project the transform up direction onto the YZ plane
                    Vector3 flatTrfmUp = TransformUp;
                    flatTrfmUp -= Vector3.Project(flatTrfmUp, targetInputControlPlaneNormal);
                    // Project the transform forward direction onto the YZ plane...
                    Vector3 targetForward = TransformForward;
                    targetForward -= Vector3.Project(targetForward, targetInputControlPlaneNormal);
                    // ... then onto the plane of the ship's right and forwards directions while keeping it on the YZ plane
                    // To do this step we subtract the projection of this vector onto flatTrfmUp
                    targetForward -= (Vector3.Dot(targetForward, flatTrfmUp) / Vector3.Dot(flatTrfmUp, flatTrfmUp)) * flatTrfmUp;
                    // Measure the angle from this projected forwards direction (which will be the target) to the current forwards direction
                    float inputControlLimitAngleDelta = Vector3.SignedAngle(targetForward, TransformForward, TransformUp);

                    if (shipPhysicsModelPhysicsBased)
                    {
                        // Physics-based: Set the inputs to counteract any displacement from the target position/rotation
                        forceInput.x = inputAxisForcePIDController.RequiredInput(inputControlLimit, TransformPosition.x, _deltaTime);
                        momentInput.y = inputAxisMomentPIDController.RequiredInput(0f, inputControlLimitAngleDelta, _deltaTime);
                    }
                    else
                    {
                        // Arcade: Use arcade force/moment to counteract any displacement from the target position/rotation
                        arcadeForceVector.x += inputAxisForcePIDController.RequiredInput(inputControlLimit, TransformPosition.x, _deltaTime);
                        arcadeMomentVector.y += inputAxisMomentPIDController.RequiredInput(0f, inputControlLimitAngleDelta, _deltaTime);
                        // Set inputs to zero
                        forceInput.x = 0f;
                        momentInput.y = 0f;
                    }
                }
            }
            #endregion

            #endregion

#if SSC_SHIP_DEBUG
            float inputEndTime = Time.realtimeSinceStartup;

            float thrusterStartTime = Time.realtimeSinceStartup;
#endif

            #region Thrusters

            if (thrusterList != null)
            {
                componentListSize = thrusterList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    // Get the thruster we are concerned with
                    thruster = thrusterList[componentIndex];
                    // Calculate thruster input - for this we need to know what type of thruster
                    // this is so that we can read the right input/s
                    // First set thruster input to zero
                    thruster.currentInput = 0f;

                    // Thrusters with max heat (temperature) or with no fuel produce no thrust etc.
                    // Check heat before fuels, so that if we run out of fuel, thruster
                    // can still cool down.

                    if (thruster.heatLevel >= 100f || (useCentralFuel ? centralFuelLevel <= 0f : thruster.fuelLevel <= 0f))
                    {
                        // Check if we need to cool the thruster down
                        thruster.ManageHeat(_deltaTime);
                        continue;
                    }

                    // Then add inputs for force/moments to give a final input...
                    switch (thruster.forceUse)
                    {
                        // Not a force thruster
                        case 0: break;
                        // Forward thruster
                        case 1: thruster.currentInput += (forceInput.z > 0f ? forceInput.z : 0f); break;
                        // Backward thruster
                        case 2: thruster.currentInput += (forceInput.z < 0f ? -forceInput.z : 0f); break;
                        // Upward thruster
                        case 3: thruster.currentInput += (forceInput.y > 0f ? forceInput.y : 0f); break;
                        // Downward thruster
                        case 4: thruster.currentInput += (forceInput.y < 0f ? -forceInput.y : 0f); break;
                        // Rightward thruster
                        case 5: thruster.currentInput += (forceInput.x > 0f ? forceInput.x : 0f); break;
                        // Leftward thruster
                        case 6: thruster.currentInput += (forceInput.x < 0f ? -forceInput.x : 0f); break;
                        // Not a valid thruster type
                        #if UNITY_EDITOR
                        default: Debug.Log("Ship - Invalid thruster force use: " + thruster.forceUse.ToString()); break;
                        #endif
                    }

                    // Only allow thrusters to affect rotation when using the physics-based model
                    if (shipPhysicsModelPhysicsBased)
                    {
                        // Addition of moment inputs can be negative i.e. if we need to rotate in the opposite direction, add weighting
                        // to tell this thruster not to activate

                        switch (thruster.primaryMomentUse)
                        {
                            // Not a moment thruster
                            case 0: break;
                            // Positive roll thruster
                            case 1: if (momentInput.z / rollPower >= -(1f - (steeringThrusterPriorityLevel * 0.9f))) { thruster.currentInput += momentInput.z; } else { thruster.currentInput = 0f; } break;
                            // Negative roll thruster
                            case 2: if (momentInput.z / rollPower <= (1f - (steeringThrusterPriorityLevel * 0.9f))) { thruster.currentInput -= momentInput.z; } else { thruster.currentInput = 0f; } break;
                            // Positive pitch thruster
                            case 3: if (momentInput.x / pitchPower >= -(1f - (steeringThrusterPriorityLevel * 0.9f))) { thruster.currentInput += momentInput.x; } else { thruster.currentInput = 0f; } break;
                            // Negative pitch thruster
                            case 4: if (momentInput.x / pitchPower <= (1f - (steeringThrusterPriorityLevel * 0.9f))) { thruster.currentInput -= momentInput.x; } else { thruster.currentInput = 0f; } break;
                            // Positive yaw thruster
                            case 5: if (momentInput.y / yawPower >= -(1f - (steeringThrusterPriorityLevel * 0.9f))) { thruster.currentInput += momentInput.y; } else { thruster.currentInput = 0f; } break;
                            // Negative yaw thruster
                            case 6: if (momentInput.y / yawPower <= (1f - (steeringThrusterPriorityLevel * 0.9f))) { thruster.currentInput -= momentInput.y; } else { thruster.currentInput = 0f; } break;
                            // Not a valid thruster type
                            #if UNITY_EDITOR
                            default: Debug.Log("Ship - Invalid thruster moment use: " + thruster.primaryMomentUse.ToString()); break;
                            #endif
                        }
                        switch (thruster.secondaryMomentUse)
                        {
                            // Not a moment thruster
                            case 0: break;
                            // Positive roll thruster
                            case 1: thruster.currentInput += momentInput.z; break;
                            // Negative roll thruster
                            case 2: thruster.currentInput -= momentInput.z; break;
                            // Positive pitch thruster
                            case 3: thruster.currentInput += momentInput.x; break;
                            // Negative pitch thruster
                            case 4: thruster.currentInput -= momentInput.x; break;
                            // Positive yaw thruster
                            case 5: thruster.currentInput += momentInput.y; break;
                            // Negative yaw thruster
                            case 6: thruster.currentInput -= momentInput.y; break;
                            // Not a valid thruster type
                            #if UNITY_EDITOR
                            default: Debug.Log("Ship - Invalid thruster moment use: " + thruster.secondaryMomentUse.ToString()); break;
                            #endif
                        }
                    }

                    // Calculate thrust force

                    // Allow for the thruster to be throttled up and down over time
                    // Input is clamped between 0 and 1 (can only act within physical constraints of thruster)
                    thruster.SmoothThrusterInput(_deltaTime);
                    thrustForce = thruster.thrustDirectionNormalised * thruster.maxThrust * thruster.currentInput;

                    if (useCentralFuel)
                    {
                        if (thruster.fuelBurnRate > 0f && thruster.currentInput > 0f)
                        {
                            // Burn fuel independently to the health level. A damaged thruster will burn the same
                            // amount of fuel but produce less thrust
                            SetFuelLevel(centralFuelLevel - (thruster.currentInput * thruster.fuelBurnRate * _deltaTime));
                        }
                    }
                    else
                    {
                        thruster.BurnFuel(_deltaTime);
                    }

                    thruster.ManageHeat(_deltaTime);

                    // Adjust for thruster damage
                    if (!shipDamageModelIsSimple && thruster.damageRegionIndex > -1)
                    {
                        thrustForce *= thruster.CurrentPerformance;
                    }

                    // Add calculated thrust force to local resultant force and moment
                    localResultantForce += thrustForce;
                    // Only allow thrusters to affect rotation when using the physics-based model
                    if (shipPhysicsModelPhysicsBased)
                    {
                        localResultantMoment += Vector3.Cross(thruster.relativePosition - centreOfMass, thrustForce);
                    }
                }
            }

            #endregion

#if SSC_SHIP_DEBUG
            float thrusterEndTime = Time.realtimeSinceStartup;

            float aeroStartTime = Time.realtimeSinceStartup;
#endif

            #region Boost
            // Is boost currently in operation?
            if (boostTimer > 0f)
            {
                boostTimer -= _deltaTime;
                localResultantForce += boostDirection * boostForce;
            }
            #endregion

            #region Aerodynamics

            // Dynamic viscosity of air at 25 deg C and 1 atm: 18.37 x 10^-6 Pa s
            // Density of air on earth at low altitudes: 1.293 kg/m^3
            // Aircraft head-on drag coefficient: ~0.05

            if (mediumDensity > 0f)
            {
                //float pDragStartTime = Time.realtimeSinceStartup;

                #region Profile Drag

                // Profile drag calculation

                // Calculate local drag force vector
                // Drag force magnitude = 0.5 * fluid density * velocity squared * drag coefficient * cross-sectional area
                localDragForce = 0.5f * mediumDensity * -localVelocity;
                localDragForce.x *= dragCoefficients.x * dragReferenceAreas.x * (localVelocity.x > 0 ? localVelocity.x : -localVelocity.x);
                localDragForce.y *= dragCoefficients.y * dragReferenceAreas.y * (localVelocity.y > 0 ? localVelocity.y : -localVelocity.y);
                localDragForce.z *= dragCoefficients.z * dragReferenceAreas.z * (localVelocity.z > 0 ? localVelocity.z : -localVelocity.z);
                // Add calculated drag force to local resultant force
                localResultantForce += localDragForce;

                if (!disableDragMoments)
                {
                    // Add calculated drag force to local resultant moment
                    // Centre of drag is precalculated separately for each axis

                    localResultantMoment.x += ((centreOfDragXMoment.y - centreOfMass.y) * localDragForce.z) * dragXMomentMultiplier;
                    localResultantMoment.x += ((centreOfDragXMoment.z - centreOfMass.z) * -localDragForce.y) * dragXMomentMultiplier;

                    localResultantMoment.y += ((centreOfDragYMoment.x - centreOfMass.x) * -localDragForce.z) * dragYMomentMultiplier;
                    localResultantMoment.y += ((centreOfDragYMoment.z - centreOfMass.z) * localDragForce.x) * dragYMomentMultiplier;

                    localResultantMoment.z += ((centreOfDragZMoment.x - centreOfMass.x) * localDragForce.y) * dragZMomentMultiplier;
                    localResultantMoment.z += ((centreOfDragZMoment.y - centreOfMass.y) * -localDragForce.x) * dragZMomentMultiplier;
                }

                #endregion

                //float pDragEndTime = Time.realtimeSinceStartup;

                //float aDragStartTime = Time.realtimeSinceStartup;

                #region Angular Profile Drag

                // Angular drag calculation

                if (angularDragFactor > 0f || shipPhysicsModelPhysicsBased)
                {
                    // TODO: Could the length, width and height be precalculated?
                    // Step 1: We are modelling the drag as if it is occuring on a rectangular prism,
                    // so calculate the hypothetical length, width and height of that prism from the given areas of each side
                    dragLength = (float)Math.Sqrt(dragReferenceAreas.x * dragReferenceAreas.y / dragReferenceAreas.z);
                    dragWidth = dragReferenceAreas.y / dragLength;
                    dragHeight = dragReferenceAreas.z / dragWidth;

                    // Step 2: Calculate the angular drag moment on each axis

                    // TODO: Re-comment this section
                    // TODO: Could these values be precalculated?
                    quarticLengthXProj = IntegerPower((dragLength / 2) + centreOfMass.z - centreOfDragYMoment.z, 4) + IntegerPower((dragLength / 2) - centreOfMass.z + centreOfDragYMoment.z, 4);
                    quarticLengthYProj = IntegerPower((dragLength / 2) + centreOfMass.z - centreOfDragXMoment.z, 4) + IntegerPower((dragLength / 2) - centreOfMass.z + centreOfDragXMoment.z, 4);
                    quarticWidthYProj = IntegerPower((dragWidth / 2) + centreOfMass.x - centreOfDragZMoment.x, 4) + IntegerPower((dragWidth / 2) - centreOfMass.x + centreOfDragZMoment.x, 4);
                    quarticWidthZProj = IntegerPower((dragWidth / 2) + centreOfMass.x - centreOfDragYMoment.x, 4) + IntegerPower((dragWidth / 2) - centreOfMass.x + centreOfDragYMoment.x, 4);
                    quarticHeightXProj = IntegerPower((dragHeight / 2) + centreOfMass.y - centreOfDragZMoment.y, 4) + IntegerPower((dragHeight / 2) - centreOfMass.y + centreOfDragZMoment.y, 4);
                    quarticHeightZProj = IntegerPower((dragHeight / 2) + centreOfMass.y - centreOfDragXMoment.y, 4) + IntegerPower((dragHeight / 2) - centreOfMass.y + centreOfDragXMoment.y, 4);
                    if (shipPhysicsModelPhysicsBased) { angularDragMultiplier = 1f; }
                    else { angularDragMultiplier = (float)Math.Pow(10f, angularDragFactor - 1f); }
                    // Formula: For a rotating rectangle with the pivot at one end:
                    // Drag moment = 1/8 * fluid density * angular velocity squared * height * drag coefficient * length to the power of four
                    // X-axis angular drag
                    localResultantMoment.x += mediumDensity / 8f * localAngularVelocity.x * localAngularVelocity.x * dragWidth *
                        dragCoefficients.y * quarticLengthYProj * (localAngularVelocity.x > 0f ? -1f : 1f) * angularDragMultiplier;
                    localResultantMoment.x += mediumDensity / 8f * localAngularVelocity.x * localAngularVelocity.x * dragWidth *
                        dragCoefficients.z * quarticHeightZProj * (localAngularVelocity.x > 0f ? -1f : 1f) * angularDragMultiplier;
                    // Y-axis angular drag
                    localResultantMoment.y += mediumDensity / 8f * localAngularVelocity.y * localAngularVelocity.y * dragHeight *
                        dragCoefficients.x * quarticLengthXProj * (localAngularVelocity.y > 0f ? -1f : 1f) * angularDragMultiplier;
                    localResultantMoment.y += mediumDensity / 8f * localAngularVelocity.y * localAngularVelocity.y * dragHeight *
                        dragCoefficients.z * quarticWidthZProj * (localAngularVelocity.y > 0f ? -1f : 1f) * angularDragMultiplier;
                    // Z-axis angular drag
                    localResultantMoment.z += mediumDensity / 8f * localAngularVelocity.z * localAngularVelocity.z * dragLength *
                        dragCoefficients.y * quarticWidthYProj * (localAngularVelocity.z > 0f ? -1f : 1f) * angularDragMultiplier;
                    localResultantMoment.z += mediumDensity / 8f * localAngularVelocity.z * localAngularVelocity.z * dragLength *
                        dragCoefficients.x * quarticHeightXProj * (localAngularVelocity.z > 0f ? -1f : 1f) * angularDragMultiplier;
                }

                #endregion

                //float aDragEndTime = Time.realtimeSinceStartup;

                //float wingsStartTime = Time.realtimeSinceStartup;

                // Calculate the "true airspeed" - this is just velocity in forwards direction
                aerodynamicTrueAirspeed = localVelocity.z;

                #region Wings

                // Wing simulation

                componentListSize = wingList == null ? 0 : wingList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    // Get the wing we are concerned with
                    wing = wingList[componentIndex];
                    // Only calculate lift if some component of the velocity is in the negative airflow direction
                    if (aerodynamicTrueAirspeed > 0f)
                    {
                        //float aoaStartTime = Time.realtimeSinceStartup;

                        #region Angle of Attack

                        // OLD UNOPTIMISED CODE. TODO: Delete
                        ////Vector3 wingPlaneNormal = Vector3.Cross(wing.liftDirection, wing.airflowDirection);
                        //wingPlaneNormal = Vector3.Cross(wing.liftDirection, Vector3.back);
                        //localVeloInWingPlane = Vector3.ProjectOnPlane(localVelocity, wingPlaneNormal);
                        //// Is this the right direction?
                        ////float angleOfAttack = Vector3.SignedAngle(-wing.airflowDirection, localVeloInWingPlane, wingPlaneNormal) + wing.angleOfAttack;
                        //wingAngleOfAttack = Vector3.SignedAngle(Vector3.forward, localVeloInWingPlane, wingPlaneNormal) + wing.angleOfAttack;

                        // Project the local velocity into the plane containing the lift direction and the forwards (negative airflow) direction
                        // Take the cross product of lift direction and forwards direction to get the normal of the plane
                        // Normalise it for the dot product in the next step
                        //wingPlaneNormal = Vector3.Cross(wing.liftDirectionNormalised, Vector3.forward).normalized;
                        wingPlaneNormal.x = wing.liftDirectionNormalised.y;
                        wingPlaneNormal.y = -wing.liftDirectionNormalised.x;
                        wingPlaneNormal.z = 0f;
                        wingPlaneNormal.Normalize();
                        //localVeloInWingPlane = (localVelocity - (Vector3.Dot(localVelocity, wingPlaneNormal) * wingPlaneNormal)).normalized;
                        // Multiplty the dot product of the local velocity and the wing plane normal with the wing plane normal to 
                        // get the component of the local velocity in the direction of the wing plane normal, then subtract
                        // this vector from the local velocity to project it into the desired plane
                        // Then normalise the vector for use in the dot product of the next step
                        // NOTE: The dot product does not require a z component as the z component of wing plane normal will always be zero,
                        //       as it is perpendicular to the forwards direction
                        localVeloInWingPlane = (localVelocity - (((localVelocity.x * wingPlaneNormal.x) + (localVelocity.y * wingPlaneNormal.y)) * wingPlaneNormal)).normalized;

                        // The angle of attack of the wing is equal to:
                        // The angle between the lift direction and the velocity in the wing plane (computed via arccos of dot product) MINUS
                        // The angle between the lift direction and the forwards direction PLUS
                        // The angle of attack of the wing due to camber
                        // Resultant of the first thing minus the second gives the angle between forwards direction and velocity, with
                        // forwards above velocity resulting in positive numbers

                        wingAngleOfAttack = ((float)(Math.Acos(Mathf.Clamp((wing.liftDirectionNormalised.x * localVeloInWingPlane.x) +
                                                            (wing.liftDirectionNormalised.y * localVeloInWingPlane.y) +
                                                            (wing.liftDirectionNormalised.z * localVeloInWingPlane.z), -1f, 1f)) -
                                                        Math.Acos(Mathf.Clamp(wing.liftDirectionNormalised.z, -1f, 1f))) * Mathf.Rad2Deg) +
                                                        wing.angleOfAttack;

                        #endregion

                        //float aoaEndTime = Time.realtimeSinceStartup;

                        //float lcStartTime = Time.realtimeSinceStartup;

                        #region Lift Coefficient

                        phi0 = (20f * wingStallEffect) - 25f;
                        phi1 = 15f;
                        phi2 = 45f - (25f * wingStallEffect);
                        phi3 = 75f - (50f * wingStallEffect);

                        //// If angle of attack is not between the min and max wing angles, the wing will stall and not produce any lift
                        //if (angleOfAttack > -5f && angleOfAttack < 15f)
                        //{
                        //    // Approximation of lift coefficient, adapted from thin airfoil theory: 
                        //    // Lift coefficient = (m * angle of attack) + d
                        //    // Where m = max lift coefficient / (max wing angle - min wing angle) and d = -m * min wing angle
                        //    // Max lift coefficient = 1.5, min wing angle = -5 degrees, max wing angle = 15 degrees
                        //    float liftCoefficient = (0.075f * angleOfAttack) + 0.375f;

                        //}

                        // TODO: NEED A COMMENT FOR BELOW THEORY
                        // Also should look at whether I should just enable/disable stalling
                        // and just use predetermined values
                        // Also, possibly stall angle actually comes from chord (wing width)?

                        // Stall region
                        if (wingAngleOfAttack < phi0) { liftCoefficient = 0f; }
                        // Increasing lift region
                        else if (wingAngleOfAttack < phi1) { liftCoefficient = 1.6f * (wingAngleOfAttack - phi0) / (phi1 - phi0); }
                        // Maximum lift region
                        else if (wingAngleOfAttack < phi2) { liftCoefficient = 1.6f; }
                        // Falloff region
                        else if (wingAngleOfAttack < phi3) { liftCoefficient = 1.6f * (wingAngleOfAttack - phi3) / (phi2 - phi3); }
                        // Stall region
                        else { liftCoefficient = 0f; }

                        #endregion

                        //float lcEndTime = Time.realtimeSinceStartup;

                        //float lidStartTime = Time.realtimeSinceStartup;

                        #region Lift and Induced Drag

                        // Lift force magnitude = 0.5 * fluid density * velocity squared * planform wing area * coefficient of lift
                        liftForceMagnitude = 0.5f * mediumDensity * aerodynamicTrueAirspeed * aerodynamicTrueAirspeed * (wing.span * wing.chord * (float)Math.Cos(wingAngleOfAttack * Mathf.Deg2Rad)) * liftCoefficient;

                        // Adjust for wing damage
                        if (!shipDamageModelIsSimple && wing.damageRegionIndex > -1)
                        {
                            liftForceMagnitude *= wing.CurrentPerformance;
                        }

                        localLiftForce = liftForceMagnitude * wing.liftDirectionNormalised;
                        // Induced drag force magnitude = 2 * lift squared / (fluid density * velocity squared * pi * wingspan squared)
                        localInducedDragForce = 2f * liftForceMagnitude * liftForceMagnitude /
                            (mediumDensity * aerodynamicTrueAirspeed * aerodynamicTrueAirspeed * Mathf.PI * wing.span * wing.span) * Vector3.back;

                        #endregion

                        //float lidEndTime = Time.realtimeSinceStartup;

                        //float forceStartTime = Time.realtimeSinceStartup;

                        // Add calculated lift and induced drag forces to local resultant force and moment
                        localResultantForce += localLiftForce + localInducedDragForce;
                        if (shipPhysicsModelPhysicsBased)
                        {
                            localResultantMoment += Vector3.Cross(wing.relativePosition - centreOfMass, localLiftForce + localInducedDragForce);
                        }

                        //float forceEndTime = Time.realtimeSinceStartup;

                        //if (componentIndex == 0)
                        //{
                        //    Debug.Log("| Angle of attack: " + ((aoaEndTime - aoaStartTime) * 1000f).ToString("0.0000") + " ms | " +
                        //        "Lift coefficient: " + ((lcEndTime - lcStartTime) * 1000f).ToString("0.0000") + " ms | " +
                        //        "Lift and induced drag: " + ((lidEndTime - lidStartTime) * 1000f).ToString("0.0000") + " ms | " +
                        //        "Force and moment: " + ((forceEndTime - forceStartTime) * 1000f).ToString("0.0000") + " ms | ");
                        //}
                    }
                }

                #endregion

                //float wingsEndTime = Time.realtimeSinceStartup;

                //float controlStartTime = Time.realtimeSinceStartup;

                // Control surfaces only available in physics-based mode
                if (shipPhysicsModelPhysicsBased)
                {
                    #region Control Surfaces

                    // Control surface simulation

                    componentListSize = controlSurfaceList == null ? 0 : controlSurfaceList.Count;
                    for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                    {
                        // Get the control surface we are concerned with
                        controlSurface = controlSurfaceList[componentIndex];

                        #region Control Surface Type

                        switch (controlSurface.type)
                        {
                            case ControlSurface.ControlSurfaceType.Aileron:
                                // Aileron
                                controlSurfaceMovementAxis = Vector3.up;
                                controlSurfaceInput = momentInput.z * (controlSurface.relativePosition.x > 0f ? 1f : -1f);
                                break;
                            case ControlSurface.ControlSurfaceType.Elevator:
                                // Elevator
                                controlSurfaceMovementAxis = Vector3.up;
                                controlSurfaceInput = -momentInput.x;
                                break;
                            case ControlSurface.ControlSurfaceType.Rudder:
                                // Rudder
                                controlSurfaceMovementAxis = Vector3.right;
                                controlSurfaceInput = momentInput.y;
                                break;
                            case ControlSurface.ControlSurfaceType.AirBrake:
                                // Air brake
                                controlSurfaceMovementAxis = Vector3.zero;
                                controlSurfaceInput = forceInput.z < 0f ? -forceInput.z : 0f;
                                break;
#if UNITY_EDITOR
                            default:
                                // Custom (TODO)
                                Debug.Log("Ship - Custom control surfaces not supported yet.");
                                break;
#endif
                        }

                        #endregion

                        // When there is no input the control surface does not apply any force
                        if (controlSurfaceInput != 0f)
                        {
                            #region Calculate Lift and Drag Forces

                            // Calculate the angle the control surface is inclined at
                            // Absolute value of controlSurfaceInput is used as we only want magnitude - direction is added in a later step
                            controlSurfaceAngle = Mathf.PI / 2f * (controlSurfaceInput > 0f ? controlSurfaceInput : -controlSurfaceInput);
                            // Calculate the magnitude of the change (delta) in lift and drag caused by the inclination of the control surface
                            // This is essentially the change in planform area (for lift) and the change in profile area (for drag)
                            // multiplied by the usual aerodynamic formulae for each
                            // TODO: Should probably work out something mildly more accurate for lift coefficient (currently is just 1.6)
                            // TODO: Should probably have something for a drag coefficient as well (currently is just 2)
                            // Then convert the forces into vectors by multiplying by their directions
                            // TODO: Include relative position based on inclination
                            // - I think this means the position of the centre of the CS adjusted for where it has been rotated to?
                            if (controlSurfaceMovementAxis != Vector3.zero)
                            {
                                controlSurfaceLiftDelta = (1f - (float)Math.Cos(controlSurfaceAngle)) * 0.5f * mediumDensity *
                                    aerodynamicTrueAirspeed * aerodynamicTrueAirspeed * controlSurface.chord * controlSurface.span * 1.6f;
                            }
                            else { controlSurfaceLiftDelta = 0f; }

                            controlSurfaceDragDelta = (float)Math.Sin(controlSurfaceAngle) * 0.5f * mediumDensity *
                                aerodynamicTrueAirspeed * aerodynamicTrueAirspeed * controlSurface.chord * controlSurface.span * 2f;

                            // Adjust for control surface damage
                            if (!shipDamageModelIsSimple && controlSurface.damageRegionIndex > -1)
                            {
                                controlSurfaceLiftDelta *= controlSurface.CurrentPerformance;
                                controlSurfaceDragDelta *= controlSurface.CurrentPerformance;
                            }

                            localLiftForce = controlSurfaceLiftDelta * (controlSurfaceInput > 0f ? -1f : 1f) * controlSurfaceMovementAxis;
                            localDragForce = controlSurfaceDragDelta * (aerodynamicTrueAirspeed > 0f ? 1f : -1f) * Vector3.back;

                            #endregion

                            // Adjust relative position to take into account current inclination of control surface
                            controlSurfaceRelativePosition = controlSurface.relativePosition + (controlSurfaceMovementAxis *
                                controlSurface.chord * 0.5f * (float)Math.Sin(controlSurfaceAngle));

                            // Add calculated lift and drag forces to resultant force and moment
                            localResultantForce += localLiftForce + localDragForce;
                            localResultantMoment += Vector3.Cross(controlSurfaceRelativePosition - centreOfMass, localLiftForce + localDragForce);
                        }
                    }

                    #endregion
                }

                //float controlEndTime = Time.realtimeSinceStartup;

                //Debug.Log("| Profile Drag: " + ((pDragEndTime - pDragStartTime) * 1000f).ToString("0.0000") + " ms | " +
                //    "Angular Drag: " + ((aDragEndTime - aDragStartTime) * 1000f).ToString("0.0000") + " ms | " +
                //    "Wings: " + ((wingsEndTime - wingsStartTime) * 1000f).ToString("0.0000") + " ms | " +
                //    "Control Surfaces: " + ((controlEndTime - controlStartTime) * 1000f).ToString("0.0000") + " ms | ");
            }

            #endregion

#if SSC_SHIP_DEBUG
            float aeroEndTime = Time.realtimeSinceStartup;

            float arcadeStartTime = Time.realtimeSinceStartup;
#endif

            #region Arcade Physics Adjustments

            if (!shipPhysicsModelPhysicsBased)
            {
                // Apply moments to match pitch, yaw and roll inputs, as well as for various arcade adjustments
                // These include rotational flight assist and target pitch/roll moment
                // Multiplication by inertia tensor is used so that we can use acceleration parameters instead of force parameters
                localResultantMoment.x += ((momentInput.x * arcadePitchAcceleration) + arcadeMomentVector.x) * Mathf.Deg2Rad * rBodyInertiaTensor.x;
                localResultantMoment.y += ((momentInput.y * arcadeYawAcceleration) + arcadeMomentVector.y) * Mathf.Deg2Rad * rBodyInertiaTensor.y;
                localResultantMoment.z += ((momentInput.z * arcadeRollAcceleration) + arcadeMomentVector.z) * Mathf.Deg2Rad * rBodyInertiaTensor.z * -1f;

                // TODO: Currently this doesn't take into account the force already being applied by the ship,
                // calculated prior to this point
                // Should it do that?

                if (stickToGroundSurfaceEnabled)
                {
                    // Calculate the limits for input for ground matching
                    float maxGroundDistInput = maxGroundMatchAcceleration;
                    if (useGroundMatchSmoothing)
                    {
                        // Previous values (for last two arguments): 50f, 2f
                        maxGroundDistInput = DampedMaxAccelerationInput(currentPerpendicularGroundDist, targetGroundDistance,
                            minGroundDistance, maxGroundDistance, centralMaxGroundMatchAcceleration, maxGroundMatchAcceleration, 200f, 0.5f);
                    }

                    // Set input limits
                    groundDistPIDController.SetInputLimits(-maxGroundDistInput, maxGroundDistInput);

                    // Get input from ground distance PID controller
                    groundDistInput = groundDistPIDController.RequiredInput(targetGroundDistance, currentPerpendicularGroundDist, _deltaTime);

                    //Debug.Log("Ground dist input: " + groundDistInput.ToString("000000.0"));
                    //Debug.Log("Current ground dist: " + currentPerpendicularGroundDist.ToString("00.00"));

                    // Are we preventing an arcade model ship from going below the target (minimum) distnace?
                    if (avoidGroundSurface)
                    {
                        // Only override user input if the ship is going below the targetGroundDistance
                        stickToGroundSurfaceEnabled = groundDistInput > 0f;
                    }
                }

                if (stickToGroundSurfaceEnabled)
                {
                    // Apply turning force counteracting velocity on x axis if pilot releases the input 
                    // or if input is in direction opposing velocity
                    if ((forceInput.x > 0f) == (localVelocity.x < 0f) || forceInput.x == 0f)
                    {
                        // The force also includes a component to counteract gravity on this axis
                        arcadeForceVector.x = -((localVelocity.x * 0.5f / _deltaTime) + localGravityAcceleration.x);
                    }
                    else { arcadeForceVector.x = 0f; }
                    // When we are sticking to the ground surface don't apply turning force on y axis,
                    // as this force should be primarily handled by the force keeping the ship at the
                    // target distance from the ground
                    arcadeForceVector.y = 0f;
                    // TODO: Maybe want to have an option to allow force to act on z as well (resulting in very arcade-like handling)
                    arcadeForceVector.z = 0f;

                    // Clamp the magnitude of the turning force vector so that the force applied does not exceed
                    // the maximum on-ground turning acceleration. Multiply by mass to convert the acceleration into a force
                    arcadeForceVector = Vector3.ClampMagnitude(arcadeForceVector, arcadeMaxGroundTurningAcceleration) * mass;

                    // Moved above in 1.2.6
                    //// Calculate the limits for input for ground matching
                    //float maxGroundDistInput = maxGroundMatchAcceleration;
                    //if (useGroundMatchSmoothing)
                    //{
                    //    // Previous values (for last two arguments): 50f, 2f
                    //    maxGroundDistInput = DampedMaxAccelerationInput(currentPerpendicularGroundDist, targetGroundDistance,
                    //        minGroundDistance, maxGroundDistance, centralMaxGroundMatchAcceleration, maxGroundMatchAcceleration, 200f, 0.5f);
                    //}

                    //// Set input limits
                    //groundDistPIDController.SetInputLimits(-maxGroundDistInput, maxGroundDistInput);

                    // Add a force acting in the direction of the ground normal to keep the ship at a target distance
                    // from the ground (the magnitude of the force is calculated by the associated PID controller)
                    arcadeForceVector += trfmInvRot * worldTargetPlaneNormal * groundDistInput * mass;

                    // pre-v1.2.6 (calculated above in 1.2.6+ as groundDistInput variable)
                    //arcadeForceVector += trfmInvRot * worldTargetPlaneNormal *
                    //    groundDistPIDController.RequiredInput(targetGroundDistance, currentPerpendicularGroundDist, _deltaTime) * mass;

                    //localResultantForce.y += stickToGroundAcc * mass;

                    //Debug.Log("Dist: " + currentPerpendicularGroundDist.ToString("00.00") + " m, acc: " + stickToGroundAcc.ToString("000.00") + "m/s^2");
                    //Debug.Log("Speed: " + speed.ToString("0000") + " m/s");
                }
                else
                {
                    // Apply turning force counteracting velocity on x and y axes if pilot releases the input 
                    // or if input is in direction opposing velocity
                    if ((forceInput.x > 0f) == (localVelocity.x < 0f) || forceInput.x == 0f)
                    {
                        // The force also includes a component to counteract gravity on this axis
                        arcadeForceVector.x = -((localVelocity.x * 0.5f / _deltaTime) + localGravityAcceleration.x);
                    }
                    else { arcadeForceVector.x = 0f; }
                    if ((forceInput.y > 0f) == (localVelocity.y < 0f) || forceInput.y == 0f)
                    {
                        // The force also includes a component to counteract gravity on this axis
                        arcadeForceVector.y = -((localVelocity.y * 0.5f / _deltaTime) + localGravityAcceleration.y);
                    }
                    else { arcadeForceVector.y = 0f; }
                    // Maybe want to have an option to allow force to act on z as well (resulting in very arcade-like handling)
                    arcadeForceVector.z = 0f;

                    // Clamp the magnitude of the turning force vector so that the force applied does not exceed
                    // the maximum in-flight turning acceleration. Multiply by mass to convert the acceleration into a force (f = ma)
                    arcadeForceVector = Vector3.ClampMagnitude(arcadeForceVector, arcadeMaxFlightTurningAcceleration) * mass;
                }

                // Braking component for arcade mode - active when moving forward and braking
                if (arcadeUseBrakeComponent && localVelocity.z > 0f && forceInput.z < 0f)
                {
                    // Calculate magnitude of braking force using drag formula
                    arcadeBrakeForceMagnitude = 0.5f * (arcadeBrakeIgnoreMediumDensity ? 1f : mediumDensity) * localVelocity.z * localVelocity.z * arcadeBrakeStrength * -forceInput.z;
                    // Calculate min braking force (f = ma)
                    arcadeBrakeForceMinMagnitude = arcadeBrakeMinAcceleration * mass;
                    // Make sure min braking force doesn't overcompensate and cause the ship to start moving backwards
                    if (arcadeBrakeForceMinMagnitude > localVelocity.z / _deltaTime * mass)
                    {
                        arcadeBrakeForceMinMagnitude = localVelocity.z / _deltaTime * mass;
                    }
                    // Add braking force or min braking force - whichever is larger
                    arcadeForceVector.z -= arcadeBrakeForceMagnitude > arcadeBrakeForceMinMagnitude ? arcadeBrakeForceMagnitude : arcadeBrakeForceMinMagnitude;
                }

                // Add the calculated arcade force to the local resultant force
                localResultantForce += arcadeForceVector;
            }

            #endregion

#if SSC_SHIP_DEBUG
            float arcadeEndTime = Time.realtimeSinceStartup;
#endif

            // Remember time of this fixed update
            lastFixedUpdateTime = Time.time;

#if SSC_SHIP_DEBUG
            //Debug.Log("| Init: " + ((initEndTime - initStartTime) * 1000f).ToString("0.0000") + " ms | " +
            //    "Ground: " + ((groundEndTime - groundStartTime) * 1000f).ToString("0.0000") + " ms | " +
            //    "Input: " + ((inputEndTime - inputStartTime) * 1000f).ToString("0.0000") + " ms | " +
            //    "Thrusters: " + ((thrusterEndTime - thrusterStartTime) * 1000f).ToString("0.0000") + " ms | " +
            //    "Aero: " + ((aeroEndTime - aeroStartTime) * 1000f).ToString("0.0000") + " ms | " +
            //    "Arcade: " + ((arcadeEndTime - arcadeStartTime) * 1000f).ToString("0.0000") + " ms | " +
            //    "Total: " + ((arcadeEndTime - initStartTime) * 1000f).ToString("0.0000") + " ms | ");
#endif
        }

        #endregion

        #region Update Weapons And Damage

        /// <summary>
        /// Update weapons and damage information for this ship. Should be called during Update(), and have 
        /// UpdatePositionAndMovementData() called prior to it.
        /// NOTE: We often hardcode the weaponType int rather than looking up the enum for the sake of performance.
        /// </summary>
        /// <param name="shipControllerManager"></param>
        public void UpdateWeaponsAndDamage(SSCManager shipControllerManager)
        {
            #region Initialisation

            float _deltaTime = Time.deltaTime;

            // Get the ship damage model
            shipDamageModelIsSimple = shipDamageModel == ShipDamageModel.Simple;

            // Update the local reference, which is used also in MoveBeam()
            if (sscManager == null) { sscManager = shipControllerManager; }

            #endregion

            #region Weapons

            componentListSize = weaponList == null ? 0 : weaponList.Count;
            for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
            {
                // Get the weapon we are concerned with
                weapon = weaponList[componentIndex];
                if (weapon == null) { continue; }

                // Check that the weapon has had a projectile ID or beam ID assigned
                // Assumes that the IDs have been correctly assigned based on the weaponType.
                if (weapon.projectilePrefabID >= 0 || weapon.beamPrefabID >= 0)
                {
                    // Lookup the enumeration once
                    weaponTypeInt = weapon.weaponTypeInt;
                    weaponFiringButtonInt = (int)weapon.firingButton;

                    // Does the weapon need to cool down??
                    weapon.ManageHeat(_deltaTime, 0f);

                    // If the weapon is not connected to a firing mechanism we can probably ignore the reload timer
                    // Weapons with no health or performance cannot aim, reload, move turrets, or fire.
                    if (weaponFiringButtonInt > 0 && weapon.health > 0f && weapon.currentPerformance > 0f)
                    {
                        if (weaponTypeInt == Weapon.TurretProjectileInt || weaponTypeInt == Weapon.TurretBeamInt) { weapon.MoveTurret(worldVelocity, false); }

                        // Does the beam weapon need charging?
                        if ((weaponTypeInt == Weapon.FixedBeamInt || weaponTypeInt == Weapon.TurretBeamInt) && weapon.rechargeTime > 0f && weapon.chargeAmount < 1f)
                        {
                            weapon.chargeAmount += _deltaTime * 1f / weapon.rechargeTime;
                            if (weapon.chargeAmount > 1f) { weapon.chargeAmount = 1f; }
                        }

                        // Check if this projectile weapon is reloading or not
                        // Check if this beam weapon is powering-up or not
                        if (weapon.reloadTimer > 0f)
                        {
                            // If reloading (or powering-up), update reload timer
                            weapon.reloadTimer -= _deltaTime;
                        }
                        // Check if this weapon is damaged or not
                        else if (shipDamageModelIsSimple || weapon.damageRegionIndex < 0 || weapon.Health > 0f)
                        {
                            // If not reloading and not damaged, check if we should fire the weapon or not
                            // For autofiring weapons, this will return false.
                            bool isReadyToFire = (pilotPrimaryFireInput && weaponFiringButtonInt == weaponPrimaryFiringInt) || (pilotSecondaryFireInput && weaponFiringButtonInt == weaponSecondaryFiringInt);

                            // If this is auto-firing turret check if it is ready to fire
                            if (weaponFiringButtonInt == weaponAutoFiringInt && (weaponTypeInt == Weapon.TurretProjectileInt || weaponTypeInt == Weapon.TurretBeamInt))
                            {
                                // Is turret locked on target and in direct line of sight?
                                // NOTE: If check LoS is enabled, it will still fire if another enemy is between the turret and the weapon.target.
                                isReadyToFire = weapon.isLockedOnTarget && (!weapon.checkLineOfSight || WeaponHasLineOfSight(weapon));

                                // TODO - Is target in range?

                            }

                            // If not reloading and not damaged, check if we should fire the weapon or not
                            if (isReadyToFire && weapon.ammunition != 0)
                            {
                                // ==============================================================================
                                // NOTE: We need to do most of these steps also in MoveBeam(..). So the commented
                                // out methods below called in MoveBeam(..)
                                // ==============================================================================

                                // Get base weapon world position (not accounting for relative fire position)
                                //weaponWorldBasePosition = GetWeaponWorldBasePosition(weapon);
                                weaponWorldBasePosition = (trfmRight * weapon.relativePosition.x) +
                                        (trfmUp * weapon.relativePosition.y) +
                                        (trfmFwd * weapon.relativePosition.z) +
                                        trfmPos;

                                // Get relative fire direction
                                weaponRelativeFireDirection = weapon.fireDirectionNormalised;
                                // If this is a turret, adjust relative fire direction based on turret rotation
                                if (weaponTypeInt == Weapon.TurretProjectileInt || weaponTypeInt == Weapon.TurretBeamInt)
                                {
                                    // (turret rotation less ship rotate) - (original turret rotation less ship rotation) + relative fire direction
                                    weaponRelativeFireDirection = (trfmInvRot * weapon.turretPivotX.rotation) * weapon.intPivotYInvRot * weaponRelativeFireDirection;
                                }

                                // Get weapon world fire direction
                                weaponWorldFireDirection = (trfmRight * weaponRelativeFireDirection.x) +
                                            (trfmUp * weaponRelativeFireDirection.y) +
                                            (trfmFwd * weaponRelativeFireDirection.z);

                                //weaponWorldFireDirection = GetWeaponWorldFireDirection(weapon);

                                // Start the firing cycle with no heat generated
                                float heatValue = 0f;
                                bool hasFired = false;

                                // Loop through all fire positions
                                int firePositionListCount = weapon.isMultipleFirePositions ? weapon.firePositionList.Count : 1;
                                for (int fp = 0; fp < firePositionListCount; fp++)
                                {
                                    // Only fire if there is unlimited ammunition (-1) or greater than 0 projectiles available
                                    if (weapon.ammunition != 0)
                                    {
                                        // If not unlimited ammo, decrement the quantity available
                                        if (weapon.ammunition > 0) { weapon.ammunition--; }

                                        // Check if there are multiple fire positions
                                        if (weapon.isMultipleFirePositions)
                                        {
                                            // Get relative fire position
                                            weaponRelativeFirePosition = weapon.firePositionList[fp];
                                        }
                                        else
                                        {
                                            // If there is only one fire position, relative position must be the zero vector
                                            weaponRelativeFirePosition.x = 0f;
                                            weaponRelativeFirePosition.y = 0f;
                                            weaponRelativeFirePosition.z = 0f;
                                        }
                                        // If this is a turret, adjust relative fire position based on turret rotation
                                        if (weaponTypeInt == Weapon.TurretProjectileInt || weaponTypeInt == Weapon.TurretBeamInt)
                                        {
                                            weaponRelativeFirePosition = (trfmInvRot * weapon.turretPivotX.rotation) * weaponRelativeFirePosition;
                                        }

                                        // Get weapon world fire position
                                        weaponWorldFirePosition = weaponWorldBasePosition +
                                            (trfmRight * weaponRelativeFirePosition.x) +
                                            (trfmUp * weaponRelativeFirePosition.y) +
                                            (trfmFwd * weaponRelativeFirePosition.z);

                                        //weaponWorldFirePosition = GetWeaponWorldFirePosition(weapon, weaponWorldBasePosition, weaponRelativeFirePosition);

                                        if (weaponTypeInt == Weapon.FixedBeamInt || weaponTypeInt == Weapon.TurretBeamInt)
                                        {
                                            // if the sequence number is 0, it means it is not active
                                            if (weapon.beamItemKeyList[fp].beamSequenceNumber == 0)
                                            {
                                                InstantiateBeamParameters ibParms = new InstantiateBeamParameters()
                                                {
                                                    beamPrefabID = weapon.beamPrefabID,
                                                    position = weaponWorldFirePosition,
                                                    fwdDirection = weaponWorldFireDirection,
                                                    upDirection = trfmUp,
                                                    shipId = shipId,
                                                    squadronId = squadronId,
                                                    weaponIndex = componentIndex,
                                                    firePositionIndex = fp,
                                                    beamSequenceNumber = 0,
                                                    beamPoolListIndex = -1
                                                };

                                                // Create a beam using the ship control Manager (SSCManager)
                                                // Pass InstantiateBeamParameters by reference so we can get the beam index and sequence number back
                                                BeamModule beamModule = shipControllerManager.InstantiateBeam(ref ibParms);
                                                if (beamModule != null)
                                                {
                                                    beamModule.callbackOnMove = MoveBeam;
                                                    // Retrieve the unique identifiers for the beam instance
                                                    weapon.beamItemKeyList[fp] = new SSCBeamItemKey(weapon.beamPrefabID, ibParms.beamPoolListIndex, ibParms.beamSequenceNumber);
                                                    // Immediately update the beam position (required for pooled beams that have previously been used)
                                                    MoveBeam(beamModule);

                                                    hasFired = true;

                                                    // Was a poolable muzzle fx spawned?
                                                    if (beamModule.muzzleEffectsItemKey.effectsObjectSequenceNumber > 0)
                                                    {
                                                        // Only get the transform if the muzzle EffectsModule has Is Reparented enabled.
                                                        Transform muzzleFXTrfm = shipControllerManager.GetEffectsObjectTransform(beamModule.muzzleEffectsItemKey.effectsObjectTemplateListIndex, beamModule.muzzleEffectsItemKey.effectsObjectPoolListIndex, true);

                                                        if (muzzleFXTrfm != null)
                                                        {
                                                            muzzleFXTrfm.SetParent(beamModule.transform);
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                        else
                                        {
                                            // Create a new projectile using the ship controller Manager (SSCManager)
                                            // Velocity is world velocity of ship plus relative velocity of weapon due to angular velocity
                                            InstantiateProjectileParameters ipParms = new InstantiateProjectileParameters()
                                            {
                                                projectilePrefabID = weapon.projectilePrefabID,
                                                position = weaponWorldFirePosition,
                                                fwdDirection = weaponWorldFireDirection,
                                                upDirection = trfmUp,
                                                weaponVelocity = worldVelocity + Vector3.Cross(worldAngularVelocity, weaponWorldFirePosition - trfmPos),
                                                gravity = gravitationalAcceleration,
                                                gravityDirection = gravityDirection,
                                                shipId = shipId,
                                                squadronId = squadronId,
                                                targetShip = weapon.isProjectileKGuideToTarget ? weapon.targetShip : null,
                                                targetGameObject = weapon.isProjectileKGuideToTarget ? weapon.target : null,
                                                targetguidHash = weapon.isProjectileKGuideToTarget ? weapon.targetguidHash : 0,
                                            };

                                            shipControllerManager.InstantiateProjectile(ref ipParms);

                                            // For now, assume projectile was instantiated
                                            // Heat value is inversely proportional to the firing interval (reload time)
                                            if (weapon.reloadTime > 0f) { heatValue += 1f / weapon.reloadTime; };

                                            hasFired = true;

                                            // Was a poolable muzzle fx spawned?
                                            if (ipParms.muzzleEffectsObjectPoolListIndex >= 0)
                                            {
                                                // Only get the transform if the muzzle EffectsModule has Is Reparented enabled.
                                                Transform muzzleFXTrfm = shipControllerManager.GetEffectsObjectTransform(ipParms.muzzleEffectsObjectPrefabID, ipParms.muzzleEffectsObjectPoolListIndex, true);

                                                if (muzzleFXTrfm != null)
                                                {
                                                    if (weaponTypeInt == Weapon.TurretProjectileInt)
                                                    {
                                                        muzzleFXTrfm.SetParent(weapon.turretPivotX);
                                                    }
                                                    else
                                                    {
                                                        // This may be risky as it could create a dependency that
                                                        // causes GC and/or circular reference issues when the ShipControlModule is destroyed.
                                                        // Might need a better method of doing this in the future.
                                                        muzzleFXTrfm.SetParent(shipTransform);
                                                    }
                                                }
                                            }
                                        }

                                        // Set reload timer to reload time
                                        weapon.reloadTimer = weapon.reloadTime;

                                        // Did we just run out of ammo?
                                        if (weapon.ammunition == 0 && callbackOnWeaponNoAmmo != null)
                                        {
                                            callbackOnWeaponNoAmmo.Invoke(this, weapon);
                                        }
                                    }
                                }

                                if (heatValue > 0f) { weapon.ManageHeat(_deltaTime, heatValue); }

                                // If the ship has a callback configured for the weapon, and it
                                // has fired at least one fire point, invoke the callback.
                                if (hasFired && callbackOnWeaponFired != null)
                                {
                                    callbackOnWeaponFired.Invoke(this, weapon);
                                }
                            }
                        }
                    }
                }
            }

            #endregion

            #region Damage

            if (!Destroyed())
            {
                if (respawningMode == RespawningMode.RespawnAtLastPosition)
                {
                    // Update current respawn position and rotation
                    currentRespawnPosition = trfmPos;
                    currentRespawnRotation = trfmRot;

                    // Increment collision respawn position timer
                    collisionRespawnPositionTimer += _deltaTime;
                    // When timer reaches the interval amount...
                    if (collisionRespawnPositionTimer > collisionRespawnPositionDelay)
                    {
                        // Update current respawn position/rotation (from what was next)
                        currentCollisionRespawnPosition = nextCollisionRespawnPosition;
                        currentCollisionRespawnRotation = nextCollisionRespawnRotation;
                        // Set next respawn position/rotation from current position/rotation
                        nextCollisionRespawnPosition = trfmPos;
                        nextCollisionRespawnRotation = trfmRot;
                        // Reset the timer
                        collisionRespawnPositionTimer = 0f;
                    }
                }
            }

            #endregion
        }

        #endregion

        #endregion

        #region Public API Methods

        #region Damage API Methods

        /// <summary>
        /// When attaching an object to the ship, call this method for each non-trigger collider.
        /// It is automatically called when using VR hands to avoid them colliding with the ship
        /// and causing damage.
        /// </summary>
        /// <param name="colliderToAttach"></param>
        public void AttachCollider (Collider colliderToAttach)
        {
            if (colliderToAttach != null && !colliderToAttach.isTrigger)
            {
                attachedCollisionColliders.Add(colliderToAttach.GetInstanceID());
            }
        }

        /// <summary>
        /// When attaching an object to the ship, call this method with an array of non-trigger collider.
        /// It is automatically called when using VR hands to avoid them colliding with the ship
        /// and causing damage. See also AttachCollider(..) and DetachCollider(..).
        /// </summary>
        /// <param name="collidersToAttach"></param>
        public void AttachColliders (Collider[] collidersToAttach)
        {
            int numAttachedColliders = collidersToAttach == null ? 0 : collidersToAttach.Length;

            for (int colIdx = 0; colIdx < numAttachedColliders; colIdx++)
            {
                Collider attachCollider = collidersToAttach[colIdx];
                if (attachCollider.enabled && !attachCollider.isTrigger) { AttachCollider(attachCollider); }
            }
        }

        /// <summary>
        /// When detaching or removing an object from the ship, call this method for each non-trigger collider.
        /// This is only required if it was first registered with AttachCollider(..).
        /// USAGE: DetachCollider (collider.GetInstanceID())
        /// </summary>
        /// <param name="colliderID"></param>
        public void DetachCollider (int colliderID)
        {
            attachedCollisionColliders.Remove(colliderID);
        }

        /// <summary>
        /// When detaching or removing an object from the ship, call this method with an array
        /// of non-trigger colliders. This is only required if they were first attached.
        /// </summary>
        /// <param name="collidersToDetach"></param>
        public void DetachColliders (Collider[] collidersToDetach)
        {
            int numAttachedColliders = collidersToDetach == null ? 0 : collidersToDetach.Length;

            for (int colIdx = 0; colIdx < numAttachedColliders; colIdx++)
            {
                Collider attachCollider = collidersToDetach[colIdx];
                if (attachCollider != null && attachCollider.enabled) { DetachCollider(attachCollider.GetInstanceID()); }
            }
        }

        /// <summary>
        /// Get the localised damage region with guidHash in the list of regions.
        /// The ship must be initialised.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public DamageRegion GetDamageRegion(int guidHash)
        {
            DamageRegion damageRegion = null;

            if (guidHash != 0 && shipDamageModel == ShipDamageModel.Localised)
            {
                // Attempt to find a matching guidHash
                for (int dmIdx = 0; dmIdx < numLocalisedDamageRegions; dmIdx++)
                {
                    if (localisedDamageRegionList[dmIdx].guidHash == guidHash)
                    {
                        damageRegion = localisedDamageRegionList[dmIdx]; break;
                    }
                }
            }

            return damageRegion;
        }

        /// <summary>
        /// Get the localised damage region with the zero-based index in the list of regions.
        /// The ship must be initialised.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public DamageRegion GetDamageRegionByIndex(int index)
        {
            if (shipDamageModel == ShipDamageModel.Localised && index >= 0 && index < numLocalisedDamageRegions)
            {
                return localisedDamageRegionList[index];
            }
            else { return null; }
        }

        /// <summary>
        /// Get the zero-based index of a local damage region in the ship given the damage region name.
        /// Returns -1 if not found or the Damage Model is not Localised.
        /// Use this sparingly as it will incur garabage. Always declare the parameter as a static readonly variable.
        /// Usage:
        /// private static readonly string EngineDamageRegionName = "Engines";
        /// ..
        /// int drIdx = GetDamageRegionIndexByName(EngineDamageRegionName);
        /// DamageRegion damageRegion = GetDamageRegionByIndex(drIdx);
        /// </summary>
        /// <param name="damageRegionName"></param>
        /// <returns></returns>
        public int GetDamageRegionIndexByName(string damageRegionName)
        {
            if (shipDamageModel == ShipDamageModel.Localised && localisedDamageRegionList != null)
            {
                return localisedDamageRegionList.FindIndex(dr => dr.name == damageRegionName);
            }
            else { return -1; }
        }

        /// <summary>
        /// Get the world space central position of the localised damage region
        /// with the given guidHash. If the ship is not using the Localised damage model
        /// or a damage region with guidHash cannot be found, this returns the world space
        /// position of the ship.
        /// The ship must be initialised.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public Vector3 GetDamageRegionWSPosition(int guidHash)
        {
            DamageRegion damageRegion = GetDamageRegion(guidHash);
            if (damageRegion != null)
            {
                return (trfmRight * damageRegion.relativePosition.x) +
                       (trfmUp * damageRegion.relativePosition.y) +
                       (trfmFwd * damageRegion.relativePosition.z) +
                       trfmPos;
            }
            else { return TransformPosition; }
        }

        /// <summary>
        /// Get the world space central position of the damage region.
        /// The ship must be initialised.
        /// </summary>
        /// <param name="damageRegion"></param>
        /// <returns></returns>
        public Vector3 GetDamageRegionWSPosition(DamageRegion damageRegion)
        {
            return (trfmRight * damageRegion.relativePosition.x) +
                    (trfmUp * damageRegion.relativePosition.y) +
                    (trfmFwd * damageRegion.relativePosition.z) +
                    trfmPos;
        }

        /// <summary>
        /// Is this world space point on the ship, currently shielded?
        /// If the main damage region is invincible, it will always return true.
        /// If the ShipDamageModel is NOT Localised, the worldSpacePoint is ignored.
        /// </summary>
        /// <param name="worldSpacePoint"></param>
        /// <returns></returns>
        public bool HasActiveShield (Vector3 worldSpacePoint)
        {
            bool isShielded = false;

            if (mainDamageRegion.isInvincible) { isShielded = true; }
            else if (shipDamageModel != ShipDamageModel.Localised)
            {
                isShielded = mainDamageRegion.useShielding && mainDamageRegion.ShieldHealth > 0f;
            }
            else
            {
                // Determine which damage region was hit
                int numDamageRegions = localisedDamageRegionList.Count + 1;

                // Loop through all damage regions
                DamageRegion thisDamageRegion;

                // Get damage position in local space (calc once outside the loop)
                Vector3 localDamagePosition = trfmInvRot * (worldSpacePoint - trfmPos);

                for (int i = 0; i < numDamageRegions; i++)
                {
                    thisDamageRegion = i == 0 ? mainDamageRegion : localisedDamageRegionList[i - 1];

                    if (thisDamageRegion.useShielding && thisDamageRegion.ShieldHealth > 0f && thisDamageRegion.IsHit(localDamagePosition))
                    {
                        isShielded = true;
                        break;
                    }
                }
            }

            return isShielded;
        }

        /// <summary>
        /// Check if a world-space point is within the volume area of a damage region.
        /// </summary>
        /// <param name="damageRegion"></param>
        /// <param name="worldSpacePoint"></param>
        /// <returns></returns>
        public bool IsPointInDamageRegion (DamageRegion damageRegion, Vector3 worldSpacePoint)
        {
            return damageRegion == null ? false : damageRegion.IsHit(trfmInvRot * (worldSpacePoint - trfmPos));
        }

        /// <summary>
        /// Make the whole ship invincible to damage.
        /// For individual damageRegions change the isInvisible value on the localised region.
        /// </summary>
        public void MakeShipInvincible()
        {
            mainDamageRegion.isInvincible = true;
        }

        /// <summary>
        /// Make the whole ship vincible to damage. When hit, the ship or shields will take damage.
        /// For individual damageRegions change the isInvisible value on the localised region.
        /// </summary>
        public void MakeShipVincible()
        {
            mainDamageRegion.isInvincible = false;
        }

        /// <summary>
        /// Resets health data for the ship. Used when initialising and respawning the ship.
        /// </summary>
        /// <returns></returns>
        public void ResetHealth()
        {
            // Reset health value for the damage region
            mainDamageRegion.Health = mainDamageRegion.startingHealth;

            // Reset shield health value for the damage region
            mainDamageRegion.ShieldHealth = mainDamageRegion.shieldingAmount;

            mainDamageRegion.shieldRechargeDelayTimer = 0f;

            mainDamageRegion.isDestructObjectActivated = false;
            mainDamageRegion.isDestroyed = false;

            damageCameraShakeAmountRequired = 0f;

            if (!shipDamageModelIsSimple)
            {
                // Reset health value for each component

                componentListSize = thrusterList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    thrusterList[componentIndex].Repair();
                }

                componentListSize = wingList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    wingList[componentIndex].Health = wingList[componentIndex].startingHealth;
                }

                componentListSize = controlSurfaceList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    controlSurfaceList[componentIndex].Health = controlSurfaceList[componentIndex].startingHealth;
                }

                componentListSize = weaponList.Count;
                for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                {
                    weaponList[componentIndex].Repair();
                }

                if (shipDamageModel == ShipDamageModel.Localised)
                {
                    // For each damage region:
                    // reset health value, shield health value, effects object or destruct instantiated flags, and child tranform

                    for (componentIndex = 0; componentIndex < numLocalisedDamageRegions; componentIndex++)
                    {
                        DamageRegion _damageRegion = localisedDamageRegionList[componentIndex];

                        _damageRegion.Health = localisedDamageRegionList[componentIndex].startingHealth;
                        _damageRegion.ShieldHealth = localisedDamageRegionList[componentIndex].shieldingAmount;
                        _damageRegion.shieldRechargeDelayTimer = 0f;
                        _damageRegion.isDestructionEffectsObjectInstantiated = false;
                        _damageRegion.isDestructObjectActivated = false;
                        _damageRegion.isDestroyed = false;
                        // if there is one, reactivate the transform for this region.
                        if (_damageRegion.regionChildTransform != null && !_damageRegion.regionChildTransform.gameObject.activeSelf)
                        {
                            _damageRegion.regionChildTransform.gameObject.SetActive(true);
                        }
                    }
                }
            }

            if (callbackOnDamage != null) { callbackOnDamage(mainDamageRegion.Health); }
        }

        /// <summary>
        /// Typically used when ShipDamageModel is Simple or Progressive, add health to the ship.
        /// If isAffectShield is true, and the health reaches the maximum configured, excess health will
        /// be applied to the shield for the specified DamageRegion. For Progressive Damage, health is
        /// also added to components that have Use Progressive Damage enabled.
        /// </summary>
        /// <param name="healthAmount"></param>
        /// <param name="isAffectShield"></param>
        public void AddHealth(float healthAmount, bool isAffectShield)
        {
            AddHealth(mainDamageRegion, healthAmount, isAffectShield);
        }

        /// <summary>
        /// Add health to a specific DamageRegion.
        /// If isAffectShield is true, and the health reaches the maximum configured, excess health will
        /// be applied to the shield for the specified DamageRegion. For Progressive Damage, health is
        /// also added to components that have Use Progressive Damage enabled.
        /// NOTE: -ve values are ignored. To incur damage use the ApplyNormalDamage or ApplyCollisionDamage
        /// API methods.
        /// </summary>
        /// <param name="damageRegion"></param>
        /// <param name="healthAmount"></param>
        /// <param name="isAffectShield"></param>
        public void AddHealth(DamageRegion damageRegion, float healthAmount, bool isAffectShield)
        {
            if (damageRegion != null && healthAmount > 0f)
            {
                float health = damageRegion.Health;
                if (health < 0f) { health = healthAmount; }
                else { health += healthAmount; }

                if (health > damageRegion.startingHealth)
                {
                    if (isAffectShield && damageRegion.useShielding)
                    {
                        float newShieldHealth = 0f;
                        // When shielding is -ve (e.g. -0.01 when it has been used up) set the shielding amount rather than adding it
                        if (damageRegion.ShieldHealth < 0f) { newShieldHealth = health - damageRegion.startingHealth; }
                        else { newShieldHealth = damageRegion.ShieldHealth + health - damageRegion.startingHealth; }

                        // Cap shielding to maximum permitted.
                        if (newShieldHealth > damageRegion.shieldingAmount) { newShieldHealth = damageRegion.shieldingAmount; }

                        damageRegion.ShieldHealth = newShieldHealth;
                    }

                    // Cap health to maximum permitted
                    health = damageRegion.startingHealth;
                }

                damageRegion.Health = health;

                // Not sure if this is correct... why do we do this??
                damageRegion.isDestructionEffectsObjectInstantiated = false;

                // Use !shipDamageModelIsSimple first to avoid always having to lookup the enumeration 
                if (!shipDamageModelIsSimple)
                {
                    int damageRegionIndex = GetDamageRegionIndex(damageRegion);

                    // Apply health to each of the components
                    if (damageRegionIndex >= 0)
                    {
                        // For Progressive, a damageRegionIndex of 0 on the component means Use Progressive Damage.
                        // If -1, the component doesn't use Progressive damage.
                        // For localised damage, the damageRegionIndex needs to match the damage region that health is being added to.

                        // Add health to Thrusters
                        componentListSize = thrusterList.Count;
                        for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                        {
                            // For Progressive, a region index of 0 means Use Progressive Damage.
                            if (thrusterList[componentIndex].damageRegionIndex == damageRegionIndex)
                            {
                                health = thrusterList[componentIndex].Health;
                                if (health < 0f) { health = healthAmount; }
                                else { health += healthAmount; }
                                if (health > thrusterList[componentIndex].startingHealth) { health = thrusterList[componentIndex].startingHealth; }

                                // Only set the health value once for a given thruster
                                thrusterList[componentIndex].Health = health;
                            }
                        }

                        //Add health to Wings
                        componentListSize = wingList.Count;
                        for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                        {
                            // For Progressive, a region index of 0 means Use Progressive Damage
                            if (wingList[componentIndex].damageRegionIndex == damageRegionIndex)
                            {
                                health = wingList[componentIndex].Health;
                                if (health < 0f) { health = healthAmount; }
                                else { health += healthAmount; }
                                if (health > wingList[componentIndex].startingHealth) { health = wingList[componentIndex].startingHealth; }

                                // Only set the health value once for a given wing
                                wingList[componentIndex].Health = health;
                            }
                        }

                        // Add health to Control Surfaces
                        componentListSize = controlSurfaceList.Count;
                        for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                        {
                            // For Progressive, a region index of 0 means Use Progressive Damage
                            if (controlSurfaceList[componentIndex].damageRegionIndex == damageRegionIndex)
                            {
                                health = controlSurfaceList[componentIndex].Health;
                                if (health < 0f) { health = healthAmount; }
                                else { health += healthAmount; }
                                if (health > controlSurfaceList[componentIndex].startingHealth) { health = controlSurfaceList[componentIndex].startingHealth; }

                                // Only set the health value once for a given Control Surface
                                controlSurfaceList[componentIndex].Health = health;
                            }
                        }

                        // Add health to Weapons
                        componentListSize = weaponList.Count;
                        for (componentIndex = 0; componentIndex < componentListSize; componentIndex++)
                        {
                            // For Progressive, a region index of 0 means Use Progressive Damage
                            if (weaponList[componentIndex].damageRegionIndex == damageRegionIndex)
                            {
                                health = weaponList[componentIndex].Health;
                                if (health < 0f) { health = healthAmount; }
                                else { health += healthAmount; }
                                if (health > weaponList[componentIndex].startingHealth) { health = weaponList[componentIndex].startingHealth; }

                                // Only set the health value once for a given Weapon
                                weaponList[componentIndex].Health = health;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Applies damage to the ship due to a collision. The damage is registered as "collision" damage,
        /// meaning that if it destroys the ship the ship will be respawned at a previously recorded respawn position.
        /// </summary>
        /// <param name="collisionInfo"></param>
        public void ApplyCollisionDamage (Collision collisionInfo)
        {
            int hitColliderID = collisionInfo.collider.GetInstanceID();

            // Avoid collision with colliders that have been registered with (attached to) this ship
            if (!attachedCollisionColliders.Contains(hitColliderID))
            {
                // Loop through contact points
                int numberOfContactPoints = collisionInfo.contactCount;
                float damageAtEachPoint = collisionInfo.impulse.magnitude / numberOfContactPoints;
                for (int c = 0; c < numberOfContactPoints; c++)
                {
                    //Debug.Log("[DEBUG] " + collisionInfo.collider.name + " hit " + collisionInfo.GetContact(c).thisCollider.name + " T:" + Time.time);

                    // Apply damage at each contact point
                    ApplyDamage(damageAtEachPoint, ProjectileModule.DamageType.Default, collisionInfo.GetContact(c).point, true);
                }

                // Remember that this was collision damage
                lastDamageWasCollisionDamage = true;
            }
        }

        /// <summary>
        /// Applies a specified amount of damage to the ship at a specified position. The damage is registered as "collision" damage,
        /// meaning that if it destroys the ship the ship will be respawned at a previously recorded respawn position.
        /// </summary>
        /// <param name="damageAmount"></param>
        /// <param name="damagePosition"></param>
        public void ApplyCollisionDamage(float damageAmount, Vector3 damagePosition)
        {
            ApplyDamage(damageAmount, ProjectileModule.DamageType.Default, damagePosition, false);
            // Remember that this was collision damage
            lastDamageWasCollisionDamage = true;
        }

        /// <summary>
        /// Applies a specified amount of damage to the ship at a specified position.
        /// </summary>
        /// <param name="damageAmount"></param>
        /// <param name="damagePosition"></param>
        public void ApplyNormalDamage(float damageAmount, ProjectileModule.DamageType damageType, Vector3 damagePosition)
        {
            ApplyDamage(damageAmount, damageType, damagePosition, false);
            // Remember that this was not collision damage
            lastDamageWasCollisionDamage = false;
        }

        /// <summary>
        /// Returns the index of the last damage event. When this value changes, a damage event has occurred.
        /// </summary>
        /// <returns></returns>
        public int LastDamageEventIndex() { return lastDamageEventIndex; }

        /// <summary>
        /// Returns the amount of rumble required due to the last damage event.
        /// </summary>
        /// <returns></returns>
        public float RequiredDamageRumbleAmount() { return damageRumbleAmountRequired; }

        /// <summary>
        /// Returns the amount of camera shake required due to the last damage event
        /// </summary>
        /// <returns></returns>
        public float RequiredCameraShakeAmount() { return damageCameraShakeAmountRequired; }
        #endregion

        #region General API Methods

        /// <summary>
        /// Returns an interpolated velocity in world space (akin to to the interpolated position/rotation on rigidbodies).
        /// </summary>
        public Vector3 GetInterpolatedWorldVelocity()
        {
            // Get the time in seconds since the last fixed update
            float timeSinceLastFixedUpdate = Time.time - lastFixedUpdateTime;
            // Interpolate between the world velocities of the last frame and the previous frame to get an interpolated world velocity
            return previousFrameWorldVelocity + (worldVelocity - previousFrameWorldVelocity) * (timeSinceLastFixedUpdate / Time.fixedDeltaTime);
        }

        /// <summary>
        /// Add temporary boost to the ship in a normalised local-space forceDirection
        /// which a force of forceAmount Newtons for a period of duration seconds.
        /// IMPORTANT: forceDirection must be normalised, otherwise you will get odd results.
        /// </summary>
        /// <param name="forceDirection"></param>
        /// <param name="forceAmount"></param>
        /// <param name="duration"></param>
        public void AddBoost(Vector3 forceDirection, float forceAmount, float duration)
        {
            boostDirection = forceDirection;
            boostForce = forceAmount;
            boostTimer = duration;
        }

        /// <summary>
        /// Immediately stop any boost that has been applied with AddBoost(..).
        /// </summary>
        public void StopBoost()
        {
            boostTimer = 0f;
        }

        #endregion

        #region Respawning API Methods

        /// <summary>
        /// Returns true if the ship has been destroyed.
        /// </summary>
        public bool Destroyed()
        {
            if (shipDamageModel == ShipDamageModel.Simple || shipDamageModel == ShipDamageModel.Progressive)
            {
                return mainDamageRegion.Health <= 0f;
            }
            else
            {
                return mainDamageRegion.Health <= 0f;
            }
        }

        /// <summary>
        /// Returns the position for the ship to respawn at.
        /// </summary>
        /// <returns></returns>
        public Vector3 GetRespawnPosition()
        {
            if (respawningMode == RespawningMode.RespawnAtLastPosition)
            {
                if (lastDamageWasCollisionDamage) { return currentCollisionRespawnPosition; }
                else { return currentRespawnPosition; }
            }
            else if (respawningMode == RespawningMode.RespawnAtOriginalPosition)
            {
                return currentRespawnPosition;
            }
            else if (respawningMode == RespawningMode.RespawnAtSpecifiedPosition)
            {
                return customRespawnPosition;
            }
            else { return Vector3.zero; }
        }

        /// <summary>
        /// Returns the rotation for the ship to respawn with.
        /// </summary>
        /// <returns></returns>
        public Quaternion GetRespawnRotation()
        {
            if (respawningMode == RespawningMode.RespawnAtLastPosition)
            {
                if (lastDamageWasCollisionDamage) { return currentCollisionRespawnRotation; }
                else { return currentRespawnRotation; }
            }
            else if (respawningMode == RespawningMode.RespawnAtOriginalPosition)
            {
                return currentRespawnRotation;
            }
            else if (respawningMode == RespawningMode.RespawnAtSpecifiedPosition)
            {
                return Quaternion.Euler(customRespawnRotation);
            }
            else { return Quaternion.identity; }
        }

        #endregion

        #region Thruster API Methods

        /// <summary>
        /// Get the (central) fuel level of the ship. Fuel Level Range 0.0 (empty) to 100.0 (full).
        /// </summary>
        /// <returns></returns>
        public float GetFuelLevel()
        {
            return centralFuelLevel;
        }

        /// <summary>
        /// Get the fuel level of the Thruster based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// Fuel Level Range 0.0 (empty) to 100.0 (full).
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public float GetFuelLevel (int thrusterNumber)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                return thrusterList[thrusterNumber - 1].fuelLevel;
            }
            else { return 0f; }
        }

        /// <summary>
        /// Get the heat level of the Thruster based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// Heat Level Range 0.0 (min) to 100.0 (max).
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public float GetHeatLevel (int thrusterNumber)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                return thrusterList[thrusterNumber - 1].heatLevel;
            }
            else { return 0f; }
        }

        /// <summary>
        /// Get the overheating threshold of the Thruster based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public float GetOverheatingThreshold (int thrusterNumber)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                return thrusterList[thrusterNumber - 1].overHeatThreshold;
            }
            else { return 0f; }
        }

        /// <summary>
        /// Get the Maximum Thrust of the Thruster based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1. Values are returned in kilo Newtons.
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public float GetMaxThrust (int thrusterNumber)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                return thrusterList[thrusterNumber-1].maxThrust;
            }
            else { return 0f; }
        }

        /// <summary>
        /// Is the thruster heat level at or above the overheating threshold?
        /// Thruster Number is based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public bool IsThrusterOverheating (int thrusterNumber)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                return thrusterList[thrusterNumber - 1].IsThrusterOverheating();
            }
            else { return false; }
        }

        /// <summary>
        /// Repair the health of a thruster. Will also set the heat level to 0.
        /// Can be useful if a thruster has burnt out after being over heated.
        /// Thruster Number is based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// </summary>
        /// <param name="thrusterNumber"></param>
        public void RepairThruster (int thrusterNumber)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                thrusterList[thrusterNumber - 1].Repair();
            }
        }

        /// <summary>
        /// Set the central fuel level for the whole ship. If useCentralFuel is false,
        /// use SetFuelLevel (thrusterNumber, new FuelLevel).
        /// </summary>
        /// <param name="newFuelLevel"></param>
        public void SetFuelLevel (float newFuelLevel)
        {
            // Clamp the fuel level
            if (newFuelLevel < 0f) { newFuelLevel = 0f; }
            else if (newFuelLevel > 100f) { newFuelLevel = 100f; }

            centralFuelLevel = newFuelLevel;
        }

        /// <summary>
        /// Set the fuel level of the Thruster based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// Fuel Level Range 0.0 (empty) to 100.0 (full).
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public void SetFuelLevel (int thrusterNumber, float newFuelLevel)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            { 
                thrusterList[thrusterNumber - 1].SetFuelLevel(newFuelLevel);
            }
        }

        /// <summary>
        /// Set the heat level of the Thruster based on the order it appears in the Thrusters tab
        /// in the ShipControlModule. Numbers begin at 1.
        /// Heat Level Range 0.0 (min) to 100.0 (max).
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <returns></returns>
        public void SetHeatLevel (int thrusterNumber, float newHeatLevel)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                thrusterList[thrusterNumber - 1].SetHeatLevel(newHeatLevel);
            }
        }

        /// <summary>
        /// Set the Maximum Thrust of the Thruster based on the order it appears in the Thruster tab
        /// in the ShipControlModule. Numbers begin at 1. Values should be in kilo Newtons
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <param name="newMaxThrustkN"></param>
        public void SetMaxThrust (int thrusterNumber, int newMaxThrustkN)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                thrusterList[thrusterNumber - 1].maxThrust = newMaxThrustkN * 1000f;
            }
        }

        /// <summary>
        /// Use this if you want fine-grained control over max thruster force, otherwise use SetMaxThrust.
        /// Set the Maximum Thrust of the Thruster based on the order it appears in the Thruster tab
        /// in the ShipControlModule. Numbers begin at 1. Values are in Newtons. 
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <param name="newMaxThrustNewtons"></param>
        public void SetMaxThrustNewtons (int thrusterNumber, float newMaxThrustNewtons)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                thrusterList[thrusterNumber - 1].maxThrust = newMaxThrustNewtons;
            }
        }

        /// <summary>
        /// Set all thrusters to the same throttle value.
        /// Values are clamped to between 0.0 and 1.0.
        /// </summary>
        /// <param name="newThrottleValue"></param>
        public void SetThrottleAllThrusters (float newThrottleValue)
        {
            // Clamp values
            if (newThrottleValue < 0f) { newThrottleValue = 0f; }
            else if (newThrottleValue > 1f) { newThrottleValue = 1f; }

            int numThrusters = thrusterList == null ? 0 : thrusterList.Count;

            for (int thIdx = 0; thIdx < numThrusters; thIdx++)
            {
                thrusterList[thIdx].throttle = newThrottleValue;
            }
        }

        /// <summary>
        /// Set the throttle for a given thruster. Numbers begin at 1. Values should be between 0.0 and 1.0.
        /// </summary>
        /// <param name="thrusterNumber"></param>
        /// <param name="newThrottleValue"></param>
        public void SetThrusterThrottle (int thrusterNumber, float newThrottleValue)
        {
            if (thrusterNumber > 0 && thrusterNumber <= thrusterList.Count)
            {
                thrusterList[thrusterNumber - 1].throttle = newThrottleValue < 0f ? 0f : newThrottleValue > 1f ? 1f : newThrottleValue;
            }
        }

        #endregion

        #region Weapon API Methods

        /// <summary>
        /// Deactivate all beam weapons that are currently firing
        /// </summary>
        public void DeactivateBeams(SSCManager sscManager)
        {
            int numWeapons = NumberOfWeapons;

            for (int weaponIdx = 0; weaponIdx < numWeapons; weaponIdx++)
            {
                Weapon weapon = weaponList[weaponIdx];
                if (weapon != null && (weapon.weaponTypeInt == Weapon.FixedBeamInt || weaponTypeInt == Weapon.TurretBeamInt))
                {
                    int numBeams = weapon.beamItemKeyList == null ? 0 : weapon.beamItemKeyList.Count;

                    for (int bItemIdx = 0; bItemIdx < numBeams; bItemIdx++)
                    {
                        if (weapon.beamItemKeyList[bItemIdx].beamSequenceNumber > 0)
                        {
                            sscManager.DeactivateBeam(weapon.beamItemKeyList[bItemIdx]);
                        }

                        weapon.beamItemKeyList[bItemIdx] = new SSCBeamItemKey(-1, -1, 0);
                    }
                }
            }
        }

        /// <summary>
        /// Get the zero-based index of a weapon on the ship given the weapon name. Returns -1 if not found.
        /// Use this sparingly as it will incur garabage. Always declare the parameter as a static readonly variable.
        /// Usage:
        /// private static readonly string WPNturret1Name = "Turret 1";
        /// ..
        /// int wpIdx = GetWeaponIndexByName(WPNturret1Name);
        /// </summary>
        /// <param name="weaponName"></param>
        /// <returns></returns>
        public int GetWeaponIndexByName(string weaponName)
        {
            if (weaponList != null) { return weaponList.FindIndex(wp => wp.name == weaponName); }
            else { return -1; }
        }

        /// <summary>
        /// Get the weapon on the ship from a zero-based index in the list of weapons.
        /// This will be 1 less than the number shown next to the weapon on the Combat tab.
        /// It validates the weaponIdx so if possible, don't call this every frame.
        /// </summary>
        /// <param name="weaponIdx"></param>
        /// <returns></returns>
        public Weapon GetWeaponByIndex(int weaponIdx)
        {
            if (weaponIdx >= 0 && weaponIdx < (weaponList == null ? 0 : weaponList.Count))
            {
                return weaponList[weaponIdx];
            }
            else { return null; }
        }

        /// <summary>
        /// Get the heat level of the Weapon with the zero-based index in list of weapons.
        /// Heat Level Range 0.0 (min) to 100.0 (max).
        /// </summary>
        /// <param name="weaponIdx"></param>
        /// <returns></returns>
        public float GetWeaponHeatLevel (int weaponIdx, float newHeatLevel)
        {
            if (weaponIdx >= 0 && weaponIdx < (weaponList == null ? 0 : weaponList.Count))
            {
                return weaponList[weaponIdx].heatLevel;
            }
            else { return 0f; }
        }

        /// <summary>
        /// Get the index in the weaponList of next weapon that has AutoTargetingEnabled.
        /// If no match is found, return -1. startIdx is the zero-based index in the
        /// weaponList to begin the search.
        /// </summary>
        /// <param name="startIdx"></param>
        /// <returns></returns>
        public int GetNextAutoTargetingWeaponIndex(int startIdx)
        {
            int foundWeaponIdx = -1;

            Weapon _tempWeapon = null;
            int numWeapons = weaponList == null ? 0 : weaponList.Count;
            if (startIdx >= 0 && startIdx < numWeapons)
            {
                // Use a for-loop rather than list.Find to avoid GC
                for (int weaponIdx = startIdx; weaponIdx < numWeapons; weaponIdx++)
                {
                    _tempWeapon = weaponList[weaponIdx];
                    if (_tempWeapon != null && _tempWeapon.isAutoTargetingEnabled)
                    {
                        foundWeaponIdx = weaponIdx;
                        break;
                    }
                }
            }
            return foundWeaponIdx;
        }

        /// <summary>
        /// Clears all targeting information for the weapon. This should be called if you do not
        /// know if the target is a ship or a gameobject.
        /// WARNING: For the sake of performance, does not validate weaponIdx.
        /// </summary>
        /// <param name="weaponIdx">zero-based index in list of weapons</param>
        public void ClearWeaponTarget(int weaponIdx)
        {
            if (weaponIdx >= 0) { weaponList[weaponIdx].ClearTarget(); }
        }

        /// <summary>
        /// Has the weapon with the zero-based index in list of weapons got a target
        /// assigned to it? The target could be a GameObject or a Ship.
        /// WARNING: For the sake of performance, does not validate weaponIdx.
        /// </summary>
        /// <param name="weaponIdx"></param>
        /// <returns></returns>
        public bool HasWeaponTarget(int weaponIdx)
        {
            if (weaponIdx >= 0) { return weaponList[weaponIdx].target != null; }
            else { return false; }
        }

        /// <summary>
        /// Set the heat level of the Weapon with the zero-based index in list of weapons.
        /// Heat Level Range 0.0 (min) to 100.0 (max).
        /// </summary>
        /// <param name="weaponIdx"></param>
        /// <returns></returns>
        public void SetWeaponHeatLevel (int weaponIdx, float newHeatLevel)
        {
            if (weaponIdx >= 0 && weaponIdx < (weaponList == null ? 0 : weaponList.Count))
            {
                weaponList[weaponIdx].SetHeatLevel(newHeatLevel);
            }
        }

        /// <summary>
        /// Sets the gameobject that the weapon will attempt to aim at. Currently only applies
        /// to Weapon.WeaponType.TurretProjectile and FixedProjectile weapons with guided projectiles.
        /// However, for the sake of performance, this method does not do any WeaponType validation.
        /// WARNING: This method will generate garbage. Where possible call
        /// SetWeaponTarget(int weaponIdx, GameObject target). If only the name is known,
        /// first call GetWeaponIndexByName(string weaponName) once in your Awake() routine.
        /// </summary>
        /// <param name="weaponName"></param>
        /// <param name="target"></param>
        public void SetWeaponTarget(string weaponName, GameObject target)
        {
            int wpIdx = GetWeaponIndexByName(weaponName);
            if (wpIdx >= 0)
            {
                // It is quicker not to check the weapon type and just set the target
                // Call SetTarget so it is configured correctly rather than setting the public variable
                weaponList[wpIdx].SetTarget(target);
            }
        }

        /// <summary>
        /// Sets the gameobject that the weapon will attempt to aim at. Currently only applies
        /// to Weapon.WeaponType.TurretProjectile and FixedProjectile weapons with guided projectiles.
        /// However, for the sake of performance, this method does not do any WeaponType validation.
        /// WARNING: For the sake of performance, does not validate weaponIdx.
        /// </summary>
        /// <param name="weaponIdx">zero-based index in list of weapons</param>
        /// <param name="target"></param>
        public void SetWeaponTarget(int weaponIdx, GameObject target)
        {
            // Currently don't check weapon type or number of weapons as this would incur overhead.
            if (weaponIdx >= 0)
            {
                // Call SetTarget so it is configured correctly rather than setting the public variable
                weaponList[weaponIdx].SetTarget(target);
            }
        }

        /// <summary>
        /// Sets the ship that the weapon will attempt to aim at. Currently only applies
        /// to Weapon.WeaponType.TurretProjectile and FixedProjectile weapons with guided projectiles.
        /// However, for the sake of performance, this method does not do any WeaponType validation.
        /// WARNING: For the sake of performance, does not validate weaponIdx.
        /// </summary>
        /// <param name="weaponIdx">zero-based index in list of weapons</param>
        /// <param name="targetShipControlModule"></param>
        public void SetWeaponTargetShip(int weaponIdx, ShipControlModule targetShipControlModule)
        {
            // Currently don't check weapon type or number of weapons as this would incur overhead.
            if (weaponIdx >= 0)
            {
                // Call SetTargetShip so it is configured correctly rather than setting the public variable
                weaponList[weaponIdx].SetTargetShip(targetShipControlModule);
            }
        }

        /// <summary>
        /// Sets the ship's damage region that the weapon will attempt to aim at. Currently only applies
        /// to Weapon.WeaponType.TurretProjectile and FixedProjectile weapons with guided projectiles.
        /// However, for the sake of performance, this method does not do any WeaponType validation.
        /// WARNING: For the sake of performance, does not validate weaponIdx.
        /// </summary>
        /// <param name="weaponIdx"></param>
        /// <param name="targetShipControlModule"></param>
        /// <param name="damageRegion"></param>
        public void SetWeaponTargetShipDamageRegion (int weaponIdx, ShipControlModule targetShipControlModule, DamageRegion damageRegion)
        {
            // Currently don't check weapon type or number of weapons as this would incur overhead.
            if (weaponIdx >= 0)
            {
                // Call SetTargetShipDamageRegion so it is configured correctly rather than setting the public variable
                weaponList[weaponIdx].SetTargetShipDamageRegion(targetShipControlModule, damageRegion);
            }
        }

        /// <summary>
        /// Performs a once-off change to the direction the weapon will fire based on a
        /// "target" position in world-space. The weapon will NOT stay locked onto this
        /// position.
        /// NOTE: Should NOT be used for Turrets. Use SetWeaponTarget(..) instead.
        /// WARNING: For the sake of performance, does not validate weaponIdx.
        /// </summary>
        /// <param name="weaponIdx">zero-based position in the list of weapons</param>
        /// <param name="aimAtWorldSpacePosition"></param>
        public void SetWeaponFireDirection(int weaponIdx, Vector3 aimAtWorldSpacePosition)
        {
            // Currently don't check weapon type or number of weapons as this would incur overhead.
            if (weaponIdx >= 0)
            {
                Weapon weapon = weaponList[weaponIdx];
                if (weapon != null)
                {
                    // Get base weapon world position (not accounting for relative fire position)
                    weaponWorldBasePosition = (trfmRight * weapon.relativePosition.x) +
                                            (trfmUp * weapon.relativePosition.y) +
                                            (trfmFwd * weapon.relativePosition.z) +
                                            trfmPos;

                    // Convert ws fire direction into local ship space
                    weapon.fireDirection = trfmInvRot * (aimAtWorldSpacePosition - weaponWorldBasePosition);
                    weapon.Initialise(trfmInvRot);
                }
            }
        }

        /// <summary>
        /// Returns whether a weapon has line of sight to the weapon's specified target (i.e. weapon.target).
        /// If directToTarget is set to true, will raycast directly from the weapon to the target.
        /// If directToTarget is set to false, will raycast in the direction the weapon is facing.
        /// This method will return true if the raycast hits:
        /// a) The weapon's specified target,
        /// b) An enemy ship (distinguished by faction ID) - even if it is not the target and anyEnemy is true,
        /// c) An object that isn't the target (if obstaclesBlockLineOfSight is set to false),
        /// d) Nothing.
        /// This method will return false if the raycast hits:
        /// a) A friendly ship (distinguished by faction ID),
        /// b) An object that isn't the target (if obstaclesBlockLineOfSight is set to true).
        /// c) An enemy ship that is not the target when anyEnemy is false
        /// </summary>
        /// <param name="weapon"></param>
        /// <param name="directToTarget"></param>
        /// <returns></returns>
        public bool WeaponHasLineOfSight (Weapon weapon, bool directToTarget = false, bool obstaclesBlockLineOfSight = true, bool anyEnemy = true)
        {
            return WeaponHasLineOfSight(weapon, weapon.target, directToTarget, obstaclesBlockLineOfSight, anyEnemy);
        }

        /// <summary>
        /// Returns whether a weapon has line of sight to a target.
        /// If directToTarget is set to true, will raycast directly from the weapon to the target.
        /// If directToTarget is set to false, will raycast in the direction the weapon is facing.
        /// This method will return true if the raycast hits:
        /// a) The target,
        /// b) An enemy ship (distingushed by faction ID) - even if it is not the target and anyEnemy is true,
        /// c) An object that isn't the target (if obstaclesBlockLineOfSight is set to false),
        /// d) Nothing.
        /// This method will return false if the raycast hits:
        /// a) A friendly ship (distinguished by faction ID),
        /// b) An object that isn't the target (if obstaclesBlockLineOfSight is set to true).
        /// c) An enemy ship that is not the target when anyEnemy is false
        /// </summary>
        /// <param name="weapon"></param>
        /// <param name="target"></param>
        /// <param name="directToTarget"></param>
        /// <returns></returns>
        public bool WeaponHasLineOfSight(Weapon weapon, GameObject target, bool directToTarget = false, bool obstaclesBlockLineOfSight = true, bool anyEnemy = true)
        {
            // Update the line-of-sight property
            weapon.UpdateLineOfSight(target, trfmPos, trfmRight, trfmUp, trfmFwd, trfmInvRot, factionId, 
                false, directToTarget, obstaclesBlockLineOfSight, anyEnemy);

            // Return the updated property
            return weapon.HasLineOfSight;
        }

        #endregion

        #endregion
    }
}