//==============================================================================
// Generic chopper
//------------------------------------------------------------------------------
//
// Known issues:
//
//  - The ship stops its small random movements some time after the pilot moves
//    to a gunner seat.
//==============================================================================
class XS_Chopper extends ONSChopperCraft
    abstract;

struct stExhaust
{
    var vector Offset;
    var rotator Dir;
    var float Size; // Size relative to the emitter.
    var float MinThrust; // Minimum thrust, when no input or reverse input is given.
    var class<XS_Exhaust> ExhaustClass; // If none, base class is used.
    var name Bone; // Optional bone to attach the exhaust to.
};

var(Exhaust)    array<stExhaust> Exhausts;
var(Exhaust)    class<XS_Exhaust> ExhaustClass;
var(Exhaust)    array<XS_Exhaust> myExhausts;

var(Streamer)   array<vector>   StreamerOffsets;
var(Streamer)   class<ONSAttackCraftStreamer> StreamerClass;
var(Streamer)   array<ONSAttackCraftStreamer> Streamers;
var(Streamer)   range           StreamerOpacityRamp;
var(Streamer)   float           StreamerOpacityChangeRate;
var(Streamer)   float           StreamerOpacityMax;
var(Streamer)   float           StreamerCurrentOpacity;
var(Streamer)   bool            StreamerActive;

var(Landing)    float           LandingImpactDamageMultiplier;
var(Landing)    float           FlightFriction;

var(Sound)      float           MaxPitchSpeed;
var(Sound)      range           EnginePitchRange;

var(AI)         bool            bFreelanceStart;
var(AI)         bool            bCheckFreelanceStart;

var(Movement)   float           MinThrottle;
var(Movement)   float           InputThrottle;
var(Movement)   float           PitchCorrection;

var(Tick)       XS_Chopper_Ticker Ticker;
var(Tick)       bool            bUseTicker;

var(Camera)     float           CameraWobbliness;
var(Camera)     float           CameraDistanceAdjustment;
var(Camera)     float           CameraFOVAdjustment;

/** Enable this to have speed displayed on screen. */
var(Speed)      bool            bDebugSpeed;
var(Speed)      Material        MPHMeterMaterial;
var(Speed)      float           MPHMeterPosX;
var(Speed)      float           MPHMeterPosY;
var(Speed)      float           MPHMeterScale;
var(Speed)      float           MPHMeterSizeY;

/** UU to ft. */
static final postoperator float Feet(float UU)
{
    return UU / 16.0;
}

/** UU to m. */
static final postoperator float Meters(float UU)
{
    return UU Feet / (100.0 / 30.48);
}

/** UU to km. */
static final postoperator float Kilometers(float UU)
{
    return UU Meters / 1000.0;
}

/** UU to km/h. */
static final postoperator float KPH(float UU)
{
    return UU Kilometers * 3600.0;
}

/** Speed: Calculate KPH based on Velocity. */
function float KPHVelocity()
{
    return VSize(Velocity) KPH;
}

event PostNetBeginPlay()
{
    SetFriction(false);
    SpawnTicker();

    super.PostNetBeginPlay();
}

event KDriverEnter(Pawn P)
{
    super.KDriverEnter(P);

    SetFreelanceStart();
}

event DrivingStatusChanged()
{
    local bool bIsEmpty;

    super.DrivingStatusChanged();

    bIsEmpty = IsVehicleEmpty();

    ActivateThrusters(!bIsEmpty);
    SetFriction(!bIsEmpty);
}

event Destroyed()
{
    ActivateThrusters(false);
    DestroyTicker();

    super.Destroyed();
}

event Died(Controller Killer, class<DamageType> damageType, vector HitLocation)
{
    SetFriction(false);
    ActivateThrusters(false);

    super.Died(Killer, damageType, HitLocation);
}

event Tick(float DeltaTime)
{
    super.Tick(DeltaTime);

    // This does not appear to work.
    //if(!IsVehicleEmpty())
    //{
    //    Enable('tick');
    //}

    SetThrottle();
    UpdateEffects(DeltaTime);
}

simulated event KApplyForce(out vector Force, out vector Torque)
{
    ApplyPitchCorrection(Torque);

    super.KApplyForce(Force, Torque);
}

event DrawHUD(Canvas C)
{
    super.DrawHUD(C);

    if(bDebugSpeed)
    {
        DisplayDebugSpeed(C);
    }
}

/** Speed: Display various speed calculations. */
function DisplayDebugSpeed(Canvas C)
{
    local Color WhiteColor;
    local float XL, YL;
    local float myKPHVelocity;
    local string Message;

    myKPHVelocity = KPHVelocity();

    WhiteColor = class'Canvas'.static.MakeColor(255,255,255);
    C.DrawColor = WhiteColor;
    C.Style = ERenderStyle.STY_Normal;
    C.StrLen("TEST", XL, YL);

    // Draw speeds.
    Message = "KPH Velocity " $ myKPHVelocity;
    C.SetPos(MPHMeterPosX * C.ClipX, (MPHMeterSizeY + MPHMeterPosY) * C.ClipY + YL);
    C.Font = class'HUD'.static.GetConsoleFont(C);
    C.DrawText(Message);

    // Draw rev meter.
    C.SetPos(MPHMeterPosX * C.ClipX, MPHMeterPosY * C.ClipY);
    C.DrawTileStretched(MPHMeterMaterial, myKPHVelocity / MPHMeterScale * C.ClipX, MPHMeterSizeY * C.ClipY);
}

/** Tick: Spawn a ticker to keep the vehicle ticking while it is not driven but
          not empty either. */
function SpawnTicker()
{
    if(bUseTicker)
    {
        Ticker = Spawn(class'XS_Chopper_Ticker', self);

        if(Ticker != none)
        {
            Ticker.ChopperBase = self;
        }
    }
}

/** Ticker: Destroy the ticker. */
function DestroyTicker()
{
    if(Ticker != none)
    {
        Ticker.Destroy();
    }
}

/** Tick: Update tick related effects.  This code must be separated from Tick
          so it can be called from turrets as well. */
function UpdateEffects(float DeltaTime)
{
    SetEngineSoundPitch();
    UpdateExhausts();
    UpdateStreamers(DeltaTime);
}

function ActivateThrusters(bool bActivate)
{
    if(Level.NetMode != NM_DedicatedServer)
    {
        ActivateExhausts(bActivate);
        ActivateStreamers(bActivate);
    }
}

/** Exhausts: Activate or deactivate exhausts. */
simulated function ActivateExhausts(bool bActivate)
{
    if(!bDropDetail)
    {
        if(bActivate)
        {
            SpawnExhausts();
        }
        else
        {
            DestroyExhausts();
        }
    }
}

/** Exhausts: Activate exhausts. */
simulated function SpawnExhausts()
{
    local vector RotX, RotY, RotZ;
    local int i;
    local int j;

    GetAxes(Rotation, RotX,RotY,RotZ);

    if(myExhausts.length == 0)
    {
        // Spawn exhausts.
        myExhausts.length = Exhausts.length;

        for(i = 0; i < Exhausts.length; i++)
        {
            if(myExhausts[i] == none)
            {
                // Use base class if none is provided.
                if(Exhausts[i].ExhaustClass == none)
                {
                    Exhausts[i].ExhaustClass = ExhaustClass;
                }

                myExhausts[i] = Spawn(Exhausts[i].ExhaustClass, self,, Location + (Exhausts[i].Offset >> Rotation));
                myExhausts[i].SetSize(Exhausts[i].Size);

                if(Exhausts[i].Bone != '')
                {
                    // Bone location and rotation override the offset and
                    // direction.
                    AttachToBone(myExhausts[i], Exhausts[i].Bone);

                    // Offset the exhausts.
                    // SetRelativeLocation will cause the emitter to vanish.
                    for(j = 0; j < myExhausts[i].Emitters.length; j++)
                    {
                        myExhausts[i].Emitters[j].StartLocationRange += Exhausts[i].Offset;
                    }
                }
                else
                {
                    myExhausts[i].SetBase(self);
                }

                myExhausts[i].SetRelativeRotation(Exhausts[i].Dir);
            }
        }
    }
}

/** Exhausts: Deactivate exhausts. */
function DestroyExhausts()
{
    local int i;

    // Despawn exhausts.
    for(i = 0; i < myExhausts.length; i++)
    {
        myExhausts[i].Kill();
    }

    myExhausts.length = 0;
}

/** Exhausts: Match exhausts to velocity. */
function UpdateExhausts()
{
    local float Thrust;
    local int i;

    if(Level.NetMode != NM_DedicatedServer)
    {
        // Adjust Engine FX depending on velocity.
        for(i = 0; i < myExhausts.Length; i++)
        {
            // Is this thruster aligned to the current input direction?
            Thrust = GetThrust(i);
            myExhausts[i].SetThrust(Thrust);
        }
    }
}

/** Exhaust: Get the amount of thrust for exhaust i corresponding to  its
             direction and the thrust input direction.

             Back/Forward: OutputThrust, Throttle
             Strafe Right/Left: OutputStrafe, Steering
             Down/Up: OutputRise, Rise
             Yaw Right/Left: DriverView
             Pitch Down/Up: Not required.*/
function float GetThrust(int i)
{
    local float Thrust;
    local float LinearThrust;
    local float AngularThrust;
    local vector ExhaustDirection;

    ExhaustDirection = GetExhaustDirection(i);

    LinearThrust = GetLinearThrust(ExhaustDirection);
    AngularThrust = GetAngularThrust(ExhaustDirection, i);
    Thrust = LinearThrust + AngularThrust;

    // Apply minimum thrust.
    Thrust = FClamp(Thrust, Exhausts[i].MinThrust, 1);

    return Thrust;
}

/** Exhaust: Get exhaust i's relative direction. */
function vector GetExhaustDirection(int i)
{
    local vector ExhaustDirection;
    local rotator BoneDirection;

    ExhaustDirection = vector(Exhausts[i].Dir);

    if(Exhausts[i].Bone != '')
    {
        BoneDirection = GetRelativeBoneRotation(Exhausts[i].Bone);

        // Rotate the emitter's direction by the bone's rotation.
        ExhaustDirection = ExhaustDirection >> BoneDirection;
    }

    return ExhaustDirection;
}

/** Exhaust: Get Bone's relative rotation. */
function rotator GetRelativeBoneRotation(name Bone)
{
    local rotator BoneRotation;

    // GetBoneRotation() returns absolute rotation.  Subtract the craft's
    // rotation from the bone's rotation to arrive at the bone's relative
    // rotation.
    BoneRotation = GetBoneRotation(Bone) - Rotation;

    return BoneRotation;
}

/** Exhaust: Get the linear direction of output for ExhaustDirection. */
function float GetLinearThrust(vector ExhaustDirection)
{
    local vector LinearThrustInput;
    local float LinearThrust;

    LinearThrustInput = GetLinearThrustInput();

    if(LinearThrustInput == vect(0,0,0))
    {
        // There is no input.
    }
    else
    {
        // Calculate the amount of correspondence between the thrust and the
        // exhaust's direction.
        LinearThrust = LinearThrustInput dot ExhaustDirection;
    }

    return LinearThrust;
}

/** Exhaust: Get the linear direction of thrust from the input  Thrust is
             applied in the opposite direction of the input (going right means
             firing the left thrusters). */
function vector GetLinearThrustInput()
{
    local vector LinearThrustInput;
    local float ThrustThrottle;

    // If reversing, use input throttle so reverse thrusters can flare normally.
    if(Throttle < 0)
    {
        ThrustThrottle = InputThrottle;
    }
    else
    {
        ThrustThrottle = Throttle;
    }

    LinearThrustInput.X = ThrustThrottle * -1;
    // Steering is already inverted.
    LinearThrustInput.Y = Steering;
    LinearThrustInput.Z = Rise * -1;

    return LinearThrustInput;
}

/** Exhaust: Get the angular direction of output for ExhaustDirection. */
function float GetAngularThrust(vector ExhaustDirection, int i)
{
    local vector AngularThrustInput;
    local vector ExhaustOffset;
    local float AngularThrust;

    AngularThrustInput = GetAngularThrustInput();

    // If the exhaust is behind the center of mass, reverse its output.
    ExhaustOffset = GetExhaustOffset(i);

    if(ExhaustOffset.X < 0)
    {
        AngularThrustInput.Y *= -1;
    }

    // Get the correspondence between input and the direction the exhaust is pushing.
    AngularThrust = AngularThrustInput dot -ExhaustDirection;
    // Scale output to input.
    AngularThrust *= Abs(AngularThrustInput.Y);

    return AngularThrust;
}

/** Exhaust: Get the angular direction of input, in this case only yaw. */
function vector GetAngularThrustInput()
{
    local vector AngularThrustInput;

    // Return the difference between the driver's view yaw and the craft's yaw.
    // Positive: Turning right.
    // Negative: Turning left.
    AngularThrustInput.Y = NormalRotValue(DriverViewYaw - Rotation.Yaw);

    // Normalize (to a value between -1 and 1).
    // A full left or right is 16384 URU.
    AngularThrustInput.Y /= 16384;
    // Clamp to normal values (in case MaxYawRate < 1).
    AngularThrustInput.Y = FClamp(AngularThrustInput.Y, -1, 1);

    return AngularThrustInput;
}

/** Exhaust: Get exhaust i's actual offset, relative to the craft, not its bone. */
function vector GetExhaustOffset(int i)
{
    local vector ExhaustOffset;
    local vector BoneOffset;
    local vector EmitterOffset;

    if(Exhausts[i].Bone != '')
    {
        // Get the bone's offset from the craft.
        BoneOffset = GetBoneOffset(Exhausts[i].Bone);
        // Get the emitter's particle spawn offset from the bone.
        EmitterOffset = Exhausts[i].Offset >> rotator(vector(GetBoneRotation(Exhausts[i].Bone)) << Rotation);
        // Add both offsets together.
        ExhaustOffset = BoneOffset + EmitterOffset;
    }
    else
    {
        ExhaustOffset = Exhausts[i].Offset;
    }

    return ExhaustOffset;
}

/** Exhaust: Get Bone's offset relative to the craft. */
function vector GetBoneOffset(name Bone)
{
    local vector BoneLocation;
    local vector BoneOffset;

    // Get the bone's absolute location.
    BoneLocation = GetBoneCoords(Bone).Origin;
    // Get the bone's offset and de-rotate it by the craft's rotation to arrive
    // at the bone's offset relative to the craft.
    BoneOffset = (BoneLocation - Location) << Rotation;

    return BoneOffset;
}

/** Exhaust: Get emitter i's offset relative to its bone. */
function vector GetEmitterOffset(int i)
{
    local vector EmitterOffset;
    local rotator BoneRotation;

    // Get the emitter's attachment bone's (if any) rotation relative to that
    // of the craft.
    BoneRotation = GetRelativeBoneRotation(Exhausts[i].Bone);
    // Rotate the emitter's spawn offset by the bone's relative rotation.
    EmitterOffset = Exhausts[i].Offset >> BoneRotation;

    return EmitterOffset;
}

/** Normalize rotational value (-32768 <= Value < 32768). */
function int NormalRotValue(int Value)
{
    while(Value < -32768)
    {
        Value += 65536;
    }

    while(32768 < Value)
    {
        Value -= 65536;
    }

    return Value;
}

/*
function int GetRotDiff(int A, int B)
{
    local int comp;

    comp = (A - B) & 65535;
    if(comp > 32768)
        comp -= 65536;

    return comp;
}
*/
/*
function RawInput(float DeltaTime,
    float aBaseX, float aBaseY, float aBaseZ,
    float aMouseX, float aMouseY,
    float aForward, float aTurn, float aStrafe, float aUp, float aLookUp)
{
Level.Game.Broadcast(Instigator.Controller,
    // Base gives no results.
        "Linear"
    $ ": Base " $ aBaseX $ "," $ aBaseY $ "," $ aBaseZ
    // Input ranges from -6000 to 6000.
    $ ", Input " $ aForward $ "," $ aStrafe $ "," $ aUp
    // Output ranges from -1 to 1.
    $ ", Output " $ OutputThrust $ "," $ OutputStrafe $ "," $ OutputRise
    // Vehicle controls range from =1 to 1.
    $ ", Vehicle " $ Throttle $ "," $ Steering $ "," $ Rise
    , 'Say');
Level.Game.Broadcast(Instigator.Controller,
        "Angular"
    // Mouse direction only measures delta.
    $ ": Mouse " $ aMouseY $ "," $ aMouseX
    $ ", Turn " $ aLookUp $ "," $ aTurn
    // Target is... funny.
    $ ", Target " $ TargetPitch $ "," $ TargetHeading
    // DriverView uses absolute coordinates.
    $ ", DriverView " $ DriverViewPitch $ "," $ DriverViewYaw
    , 'Say');
}
*/

// Streamers: Activate or deactivate streamers.
function ActivateStreamers(bool bActive)
{
    local int i;

    if(!bDropDetail)
    {
        if(bActive)
        {
            if(Streamers.length == 0)
            {
                // Spawn Streamers.
                Streamers.length = StreamerOffsets.length;

                for(i = 0; i < Streamers.length; i++)
                {
                    if(Streamers[i] == none)
                    {
                        Streamers[i] = Spawn(StreamerClass, self,, Location + (StreamerOffsets[i] >> Rotation));
                        Streamers[i].SetBase(self);
                    }
                }
            }
        }
        else
        {
            // Despawn streamers.
            for(i = 0; i < Streamers.length; i++)
            {
                Streamers[i].Destroy();
            }

            Streamers.length = 0;
        }
    }
}

/** Landing: Use high friction while grounded, low while flying. */
function SetFriction(bool bFlight)
{
    if(bFlight
    && KParams.KFriction > FlightFriction)
    {
        // Flight friction.
        KParams.KFriction = FlightFriction;
    }
    else if(KParams.KFriction < default.KParams.KFriction)
    {
        // Ground friction.
        KParams.KFriction = default.KParams.KFriction;
    }
}

/** Movement: Apply minimum throttle. */
function SetThrottle()
{
    // Save actual input throttle for exhaust calculations.
    InputThrottle = Throttle;
    Throttle = FClamp(Throttle, MinThrottle, 1);
}

/** Sound: Match engine sound pitch to velocity. */
function SetEngineSoundPitch()
{
    local float EnginePitch;

    if(Level.NetMode != NM_DedicatedServer)
    {
        EnginePitch = EnginePitchRange.Min + VSize(Velocity) / MaxPitchSpeed * (EnginePitchRange.Max - EnginePitchRange.Min);
        SoundPitch = FClamp(EnginePitch, EnginePitchRange.Min, EnginePitchRange.Max);
    }
}

// Get the vehicle's velocity along the direction it is travelling.
function vector GetRelativeVelocity()
{
    return Velocity << Rotation;
}

// Streamers: Match streamers to velocity.
function UpdateStreamers(float DeltaTime)
{
    local float DesiredOpacity, DeltaOpacity, MaxOpacityChange;
    local int i;
    local bool NewStreamerActive;
    local vector RelativeVelocity;
    local TrailEmitter T;

    if(Level.NetMode != NM_DedicatedServer)
    {
        RelativeVelocity = GetRelativeVelocity();

        // Update streamer opacity (limit max change speed)
        DesiredOpacity = (RelativeVelocity.X - StreamerOpacityRamp.Min) / (StreamerOpacityRamp.Max - StreamerOpacityRamp.Min);
        DesiredOpacity = FClamp(DesiredOpacity, 0.0, StreamerOpacityMax);

        MaxOpacityChange = DeltaTime * StreamerOpacityChangeRate;

        DeltaOpacity = DesiredOpacity - StreamerCurrentOpacity;
        DeltaOpacity = FClamp(DeltaOpacity, -MaxOpacityChange, MaxOpacityChange);

        if(!IsBehindView())
        {
            StreamerCurrentOpacity = 0.0;
        }
        else
        {
            StreamerCurrentOpacity += DeltaOpacity;
        }

        if(StreamerCurrentOpacity < 0.01)
        {
            NewStreamerActive = false;
        }
        else
        {
            NewStreamerActive = true;
        }

        for(i = 0; i < Streamers.length; i++)
        {
            if(NewStreamerActive)
            {
                if(!StreamerActive)
                {
                    T = TrailEmitter(Streamers[i].Emitters[0]);
                    T.ResetTrail();
                }

                Streamers[i].Emitters[0].Disabled = false;
                Streamers[i].Emitters[0].Opacity = StreamerCurrentOpacity;
            }
            else
            {
                Streamers[i].Emitters[0].Disabled = true;
                Streamers[i].Emitters[0].Opacity = 0.0;
            }
        }

        StreamerActive = NewStreamerActive;
    }
}

// Check for 3rd person view.
function bool IsBehindView()
{
    local PlayerController PC;

    PC = Level.GetLocalPlayerController();

    if(PC != none
    && PC.ViewTarget == self)
    {
        return PC.bBehindView;
    }
    else
    {
        return True;
    }
}

/** Landing: Lessen (but not prevent) damage sustained while landing. */
function float ImpactDamageModifier()
{
    local float Multiplier;
    local vector X, Y, Z;

    GetAxes(Rotation, X, Y, Z);

    Multiplier = super.ImpactDamageModifier();

    // Landing?
    if(ImpactInfo.ImpactNorm Dot Z > 0)
    {
        // Lessen the damage.
        Multiplier *= 1 - ImpactInfo.ImpactNorm dot Z * LandingImpactDamageMultiplier;
    }

    return super.ImpactDamageModifier() * Multiplier;
}

// AI: When trying to dodge, vary height as well?
function bool Dodge(eDoubleClickDir DoubleClickMove)
{
    if(FRand() < 0.7)
    {
        VehicleMovingTime = Level.TimeSeconds + 1;
        Rise = 1;
    }

    return false;
}

// AI: Keep distance?
function bool RecommendLongRangedAttack()
{
    return true;
}

/** Entry: Don't mess around trying to find an exit position, just use the damn
    position given. */
function bool PlaceExitingDriver()
{
    local vector ExitPosition, ZOffset;

    ZOffset = Driver.default.CollisionHeight * vect(0,0,0.5);
    ExitPosition = Location + (ExitPositions[0] >> Rotation) + ZOffset;
    Driver.SetLocation(ExitPosition);

    return true;
}

/** Hover: Check if the vehicle is empty, ignoring Turret. */
function bool IsVehicleEmptyOtherwise(ONSWeaponPawn Turret)
{
    local int i;

    if(Driver != none)
    {
        return false;
    }
    else
    {
        for(i = 0; i < WeaponPawns.length; i++)
        {
            if(WeaponPawns[i].Driver != none
            && WeaponPawns[i] != Turret)
            {
                return false;
            }
        }
    }

    return true;
}

/** Movement: Prevent the nose from dipping.  Use this if turning isn't supposed
              to pitch the craft. */
function ApplyPitchCorrection(out vector Torque)
{
    local vector PitchCorrectionTorque;

    if(!IsVehicleEmpty()
    && PitchCorrection != 0)
    {
        PitchCorrectionTorque.Y = Rotation.Pitch * PitchCorrection;
        PitchCorrectionTorque = PitchCorrectionTorque >> Rotation;
        Torque += PitchCorrectionTorque;
    }
}
/* // mewocat edit - this wobbliness is really bonkers
simulated event bool SpecialCalcView(out Actor ViewActor, out vector CameraLocation, out rotator CameraRotation)
{
    super.SpecialCalcView(ViewActor, CameraLocation, CameraRotation);

    WobbleCamera(CameraLocation, CameraRotation);

    return true;
}

simulated function SpecialCalcBehindView(PlayerController PC, out Actor ViewActor, out vector CameraLocation, out rotator CameraRotation)
{
    super.SpecialCalcBehindView(PC, ViewActor, CameraLocation, CameraRotation);

    AdjustCameraDistance(CameraLocation, CameraRotation);
    AdjustCameraFOV();
}
*/

/** Camera: Add some roll to the camera. */
/*
function WobbleCamera(out vector CameraLocation, out rotator CameraRotation)
{
    if(CameraWobbliness != 0
    // Keep camera steady while crashing!
    && Health > 0)
    {
        // Add a little of the craft's rotation to the camera.
        CameraRotation.Pitch += Rotation.Pitch * CameraWobbliness;
        CameraRotation.Roll += Rotation.Roll * CameraWobbliness;
    }
}
*/

/** Camera: Adjust camera location based on speed. */
function AdjustCameraDistance(out vector CameraLocation, out rotator CameraRotation)
{
    local float CameraDistanceFactor;
    local vector CameraDistance;

    if(CameraDistanceAdjustment != 0)
    {
        CameraDistanceFactor = GetRelativeVelocity().X / KarmaParams(KParams).KMaxSpeed;
        CameraDistance.X = CameraDistanceFactor * CameraDistanceAdjustment;
        CameraLocation -= (CameraDistance >> CameraRotation);
    }
}

/** Camera: Adjust FOV based on speed. */
function AdjustCameraFOV()
{
    local float FOV;
    local float CameraDistanceFactor;
    local PlayerController PC;

    if(CameraFOVAdjustment != 0)
    {
        CameraDistanceFactor = GetRelativeVelocity().X / KarmaParams(KParams).KMaxSpeed;
        PC = PlayerController(Controller);
    	FOV = PC.DefaultFOV + CameraDistanceFactor * CameraFOVAdjustment;
        PC.SetFOV(FOV);
    }
}

/** AI: The more, the merrier. */
function bool ImportantVehicle()
{
    local bool bFreelance;

    if(!bCheckFreelanceStart
    || bFreelanceStart)
    {
        bFreelance = true;
    }

    return !bFreelance;
}

/** AI: The more, the merrier. */
function SetFreelanceStart()
{
    local bool bFreelance;

    if(Bot(Controller) != none
    && Bot(Controller).Squad != none
    && Bot(Controller).Squad.bFreelance
    && !ONSTeamAI(Bot(Controller).Squad.Team.AI).bAllNodesTaken)
    {
        bFreelance = true;
    }

    bFreelanceStart = bFreelance;
}

// Keep the destroyed vehicle around longer.
state VehicleDestroyed
{
Begin:
    DestroyAppearance();
    VehicleExplosion(vect(0,0,1), 1.0);

    // Rather than always 9 seconds...
    Sleep(Rand(60));

    CallDestroy();
}

/** Message: Generic HUD message wrapper. */
function HUDMessage(string Message, optional float NewPosY, optional float NewLifeTime, optional color NewDrawColor)
{
    if(Message != ""
    && Controller != none)
    {
        class'XS_Message'.static.HUDMessage(Controller, Message, NewPosY, NewLifeTime, NewDrawColor);
    }
}

/** Adds Value to r's values. */
static final operator(20) range += (out range r, float Value)
{
    r.Min += Value;
    r.Max += Value;

    return r;
}

/** Adds Value to r's values. */
static final operator(20) range + (range r, float Value)
{
    r += Value;

    return r;
}

/** Adds Value to rv's values. */
static final operator(20) rangevector += (out rangevector rv, float Value)
{
    rv.X += Value;
    rv.Y += Value;
    rv.Z += Value;

    return rv;
}

/** Adds Value to rv's values. */
static final operator(20) rangevector + (rangevector rv, float Value)
{
    rv += Value;

    return rv;
}

/** Adds v to rv's values. */
static final operator(20) rangevector += (out rangevector rv, vector v)
{
    rv.X += v.X;
    rv.Y += v.Y;
    rv.Z += v.Z;

    return rv;
}

/** Adds v to rv's values. */
static final operator(20) rangevector + (rangevector rv, vector v)
{
    rv += v;

    return rv;
}

defaultproperties
{
     ExhaustClass=Class'SciFiPelican.XS_Exhaust'
     StreamerClass=Class'Onslaught.ONSAttackCraftStreamer'
     StreamerOpacityRamp=(Min=1200.000000,Max=1600.000000)
     StreamerOpacityChangeRate=1.000000
     StreamerOpacityMax=0.700000
     LandingImpactDamageMultiplier=0.500000
     FlightFriction=0.000001
     MaxPitchSpeed=1000.000000
     EnginePitchRange=(Min=64.000000,Max=96.000000)
     MinThrottle=-1.000000
     PitchCorrection=1.000000
     CameraWobbliness=0.500000
     MPHMeterMaterial=Texture'InterfaceContent.Menu.BorderBoxD'
     MPHMeterPosX=0.010000
     MPHMeterPosY=0.800000
     MPHMeterScale=300.000000
     MPHMeterSizeY=0.050000
     TurnTorqueMax=10000.000000
     bTurnInPlace=True
     bDrawMeshInFP=True
     bDriverHoldsFlag=False
     bCanCarryFlag=False
     FlagRotation=(Yaw=32768)
}
