Script:Files:script/tools/repair drone.script

From Mod Wiki
Revision as of 18:14, 2 November 2007 by Wizz (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
/***********************************************************************

repair_drone.script

***********************************************************************/

// blend times
#define TOOL_REPAIR_DRONE_IDLE_TO_IDLE        0
#define TOOL_REPAIR_DRONE_IDLE_TO_LOWER        4
#define TOOL_REPAIR_DRONE_IDLE_TO_FIRE        1
#define    TOOL_REPAIR_DRONE_IDLE_TO_RELOAD    4
#define TOOL_REPAIR_DRONE_RAISE_TO_IDLE        1
#define TOOL_REPAIR_DRONE_FIRE_TO_IDLE        4
#define TOOL_REPAIR_DRONE_RELOAD_TO_IDLE    4
#define    TOOL_REPAIR_DRONE_RELOAD_TO_FIRE    4

#define STATE_SEEK        0
#define STATE_REPAIR    1
#define STATE_RETURN    2

/***********************************************************************

class declarations

***********************************************************************/

object repair_drone {
    void        init();
    void        destroy();

    void        LookTowards( vector targetOrigin );
    void        SeekTarget();
    void        RepairTarget();
    void        Return();

    void        DoMove( vector end );

    void        OnKilled( entity inflictor, entity attacker, float damage, vector direction, float location );
    void        OnPostDamage( entity attacker, float oldHealth, float newHealth );

    void        SetEntities( entity target, entity owner );
    void        SetAction( float action );
    void        ChangeState( float val );

    void        DoAction();

    void        SoundThread();
    void        CheckOwner( entity ownerEntity );
    void        CheckTarget( entity repairTarget );
    void        SelfDestruct();

    float        OnUpdateCrosshairInfo( entity p );

    void        syncFields();

    vector        targetSize;
    float        orbitRadius;

    float        state;
    boolean        forceState;
    float        cachedAction;

    float        repairCount;
    float        repairInterval;
    float        armInterval;
    float        constructInterval;
    float        nextActionTime;
    float        returnTimeout;
}

object tool_repair_drone : weapon_base {

    void            preinit();
    void            init();
    void            destroy();

    void            Lower();
    void            Raise();
    void            Idle();
    void            IdleEmpty();

    void            ClearDrone();
    entity            FindDrone();

    void            Attack();
    boolean            WantsAttack();
    boolean            AttackValid();
    boolean            CheckAttack( float mask );
    boolean            CheckAltAttack();

    void            AltAttack();
    void            DoAttack( string droneType );

    boolean            ShowTargetLock();

    void            Repair();
    void            Arm();
    void            Disarm();
    void            Construct();

    void            ToolTipThread_Raise();
    void            ToolTipThread_Deployed();

    void            OwnerDied();

    float            fireRate;
    float            repairCount;
    float            count;
    float            chargePerDrone;
    float            meleeDistance;

    entity            cachedEntity;
    float            cachedAction;

    float            droneClass;

    boolean            disarmCharge;
    boolean            armCharge;
    boolean            armNormal;
    boolean            canRepair;
    boolean            canConstruct;

    boolean            playingFireSound;
    boolean            lastAltAttack;

    float            nextActionFailMessageTime;

    boolean            deployedTipThreadActive;

    float            returnDelay;
    float            activateDroneTime;
}


/***********************************************************************

repair_drone implementation

***********************************************************************/

void repair_drone::syncFields() {
    syncBroadcast( "cachedAction" );
}

void repair_drone::init() {
    repairCount = getFloatKey( "repair_count" );
    repairInterval = getFloatKey( "repair_interval" );
    armInterval = getFloatKey( "arm_interval" );
    constructInterval = getFloatKey( "construct_interval" );
    returnTimeout = -1;
    nextActionTime = 0;

    if ( !sys.isClient() ) {
        player p = getOwnerEntity();
        if ( p != $null_entity ) {
            p.setRepairDroneState( false );
        }
    }

    // idle until the target has been sent by the server
    entity repairTarget = $null_entity;
    while ( repairTarget == $null_entity ) {
        repairTarget = getRepairTarget();
        sys.waitFrame();
    }

    targetSize = repairTarget.getMaxs() - repairTarget.getMins();
    if ( repairTarget.vCustomOrbitRadius() ) {
        orbitRadius = repairTarget.vGetOrbitRadius();
    } else {
        vector size = repairTarget.getAbsMaxs() - repairTarget.getAbsMins();
        orbitRadius = 0.7 * sys.sqrt( size_x * size_x + size_y * size_y );
    }

    thread SoundThread();

    state = STATE_SEEK;
    while ( true ) {
        forceState = false;
        if ( state == STATE_SEEK ) {
            SeekTarget();
        } else if ( state == STATE_REPAIR ) {
            RepairTarget();
        } else if ( state == STATE_RETURN ) {
            Return();
        }
    }
}

void repair_drone::destroy() {
}

void repair_drone::ChangeState( float val ) {
    if ( state != val ) {
        state = val;
        forceState = true;
    }
}

void repair_drone::SelfDestruct() {
    // TODO: make it explode or something
    if ( !sys.isClient() ) {
        player p = getOwnerEntity();
        if ( p != $null_entity ) {
            p.setRepairDroneState( true );
        }

        remove();
    }
}

void repair_drone::CheckOwner( entity ownerEntity ) {
    // die if the owner is dead
    if ( ownerEntity.getHealth() <= 0 ) {
        SelfDestruct();
    }

    float distance = sys.vecLength( getWorldOrigin() - ownerEntity.getWorldOrigin() );
    if ( distance > getFloatKey( "max_player_die_distance" ) ) {
        // die if the owner gets REALLY far away
        SelfDestruct();
    } else if ( returnTimeout != -1 && sys.getTime() - returnTimeout > 0 ) {
        // die if we haven't returned to the owner by now
        SelfDestruct();
    } else if ( distance > getFloatKey( "max_player_return_distance" ) ) {
        // return to the owner if its getting too far away
        ChangeState( STATE_RETURN );
    }
}

void repair_drone::CheckTarget( entity repairTarget ) {
    // return if the target no longer exists
    if ( repairTarget == $null_entity ) {
        ChangeState( STATE_RETURN );
        return;
    }

    // return if the target is hidden
    if ( repairTarget.isHidden() && !repairTarget.vRepairDroneIgnoreHidden() ) {
        ChangeState( STATE_RETURN );
    }
}

void repair_drone::LookTowards( vector targetOrigin ) {
    vector myOrigin = getWorldOrigin();

    // set the angles to look towards the target
    vector desiredForward = targetOrigin - myOrigin;
    desiredForward = sys.vecNormalize( desiredForward );
    vector myForward = getWorldAxis( 0 );
    vector newForward = myForward * 0.8 + desiredForward * 0.2;
    newForward = sys.vecNormalize( newForward );
    vector newAngles = sys.vecToAngles( newForward );

    vector myAngles = getAngles();
    myAngles_y = newAngles_y;
    setAngles( myAngles );
}

void repair_drone::DoMove( vector end ) {
    setTargetPosition( end, sys.getFrameTime() );

    // calculate how fast it needs to go to be perfect noclip style movement
    vector desiredVelocity = ( end - getWorldOrigin() ) * ( 1.0f / sys.getFrameTime() );
    float velLength = sys.vecLength( desiredVelocity );
    if ( velLength > 400.0f ) {
        vector direction = desiredVelocity * ( 1 / velLength );
        desiredVelocity = direction * 400.0f;
    }

    // blend that with the current velocity (repair drone motion stuff) 
    // to get a slightly bobby noclip type movement :)
    vector myVelocity = getLinearVelocity();
    setLinearVelocity( desiredVelocity * 0.05f + myVelocity * 0.95f );
}


void repair_drone::SeekTarget() {
    float timeToTarget = sys.getTime() + 2;
    float maxMoveSpeed = 320 * sys.getFrameTime();
    entity repairTarget = getRepairTarget();
    entity ownerEntity = getOwnerEntity();

    while ( true ) {
        if ( forceState ) {
            return;
        }

        CheckOwner( ownerEntity );
        CheckTarget( repairTarget );

        float now = sys.getTime();

        vector targetOrigin = repairTarget.getWorldOrigin();
        targetOrigin_z += targetSize_z * 0.5;

        vector myOrigin = getWorldOrigin();

        // find the nearest tangent to the target's sphere
        vector delta = targetOrigin - myOrigin;

        float deltaLength = sys.vecLength( delta );
        vector direction;
        float directionLength;

        // head for a point above the object
        vector destination = targetOrigin;
        destination_z += orbitRadius;
        direction = destination - myOrigin;
        directionLength = sys.vecLength( direction );
        if ( directionLength > 1 ) {
            direction /= directionLength;
        }
        if ( deltaLength < orbitRadius * 1.5 ) {
            ChangeState( STATE_REPAIR );
        }

        float moveSpeed = maxMoveSpeed;
        if ( directionLength < 800.0f ) {
            float fraction = directionLength / 800.0f;
            moveSpeed = moveSpeed * fraction;
        }

        float timeLeft = timeToTarget - now;

        vector desiredOrigin = myOrigin;
        if ( deltaLength < orbitRadius * 1.02 && deltaLength > orbitRadius * 0.98 ) {
            // reached the hemisphere, follow it around to the point above the car
            // fit the new position onto the hemisphere
            vector newOrigin = myOrigin + direction * moveSpeed;
            vector newDelta = targetOrigin - newOrigin;
            float deltaScale = orbitRadius / sys.vecLength( newDelta );
            newDelta_x *= deltaScale;
            newOrigin = targetOrigin - newDelta;

            desiredOrigin = newOrigin;
        } else if ( deltaLength > orbitRadius ) {
            desiredOrigin = myOrigin + direction * moveSpeed;
        } else {
            // inside the hemisphere! move towards the outside
            vector awayDirection = sys.vecNormalize( -delta );
            direction += awayDirection;
            direction = sys.vecNormalize( direction );

            desiredOrigin = myOrigin + direction * moveSpeed;
        }

        LookTowards( targetOrigin );

        DoMove( desiredOrigin );
        sys.waitFrame();
    }
}

void repair_drone::DoAction() {
    float now = sys.getTime();
    if ( nextActionTime > now ) {
        return;
    }

    entity repairTarget = getRepairTarget();
    entity ownerEntity = getOwnerEntity();

    if ( cachedAction == AC_REPAIR ) {
        repairTarget.vRepair( repairCount, ownerEntity );
        nextActionTime = now + repairInterval;
    }

    if ( cachedAction == AC_CONSTRUCT ) {
        repairTarget.vConstruct( ownerEntity );
        nextActionTime = now + constructInterval;
    }

    if ( cachedAction == AC_ARM || cachedAction == AC_DISARM ) {
        repairTarget.vArm( ownerEntity );
        nextActionTime = now + armInterval;
    }
}

void repair_drone::RepairTarget() {
    // it has reached its position above the target, ready to start repairing
    float targetElevation = 30;
    float targetRotation = 57;
    float targetTime = 0;

    float elevationSpeed = 0.5;
    float rotationSpeed = 0.5;
    entity repairTarget = getRepairTarget();
    entity ownerEntity = getOwnerEntity();
    boolean orbitUnderneath = repairTarget.vOrbitUnderneath();

    while( true ) {
        if ( forceState ) {
            return;
        }

        CheckOwner( ownerEntity );
        CheckTarget( repairTarget );

        float now = sys.getTime();

        vector targetOrigin = repairTarget.getWorldOrigin();
        targetOrigin_z += targetSize_z * 0.5;
        vector myOrigin = getWorldOrigin();
        vector delta = myOrigin - targetOrigin;
        float deltaLength = sys.vecLength( delta );
        if ( deltaLength > orbitRadius * getFloatKey( "max_repair_scale" ) ) {
            ChangeState( STATE_SEEK );
            setEffectOrigins( myOrigin, myOrigin, 0 );
            return;
        }
        delta = sys.vecNormalize( delta );

        // calculate the current elevation and rotation
        float elevation = sys.asin( delta_z );
        float rotation = sys.asin( delta_y / sys.cos( elevation ) );

        // find a new place to go to
        if ( now > targetTime + 2 ) {
            // find an initial random point to head to
            targetRotation += sys.random( 20 );
            targetTime = now + 0.5;

            if ( orbitUnderneath ) {
                targetElevation = -85;
            } else {
                targetElevation = sys.random( 60 ) + 25;
            }

            // modify the angles so that they'll interpolate better
            if ( targetElevation > elevation + 180 ) {
                targetElevation -= 360;
            } else if ( targetElevation < elevation - 180 ) {
                elevation -= 360;
            }
            if ( targetRotation > rotation + 180 ) {
                targetRotation -= 360;
            } else if ( targetRotation < rotation - 180 ) {
                rotation -= 360;
            }
        }

        if ( !orbitUnderneath && elevation < 60 ) {
            elevation = 60;
        }

        // move towards the target
        if ( targetElevation - elevation < 0 ) {
            elevation -= elevationSpeed;
        } else {
            elevation += elevationSpeed;
        }
        if ( targetRotation - rotation < 0 ) {
            rotation -= rotationSpeed;
        } else {
            rotation += rotationSpeed;
        }

        // calculate the new position
        vector destination;
        float cosElevation = sys.cos( elevation );
        destination_x = sys.cos( rotation ) * cosElevation;
        destination_y = sys.sin( rotation ) * cosElevation;
        destination_z = sys.sin( elevation );
        destination *= orbitRadius;
        destination += targetOrigin;

        // pull it towards the player so it doesn't hide on the other side all the time
        vector playerDelta = ownerEntity.getWorldOrigin() - myOrigin;
        playerDelta = sys.vecNormalize( playerDelta );
        destination += playerDelta * 5;

        LookTowards( targetOrigin );

        DoMove( destination );

        DoAction();

        // check if it still needs to repair this target
        if ( !repairTarget.vCheckActionCode( ownerEntity, cachedAction ) ) {
            ChangeState( STATE_RETURN );
        }

        if ( cachedAction == AC_REPAIR ) {
            setEffectOrigins( myOrigin, repairTarget.vGetLastRepairOrigin(), 1 );
        } else {
            setEffectOrigins( myOrigin, targetOrigin, 1 );
        }

        sys.waitFrame();
    }
}

void repair_drone::Return() {
    // clear effect
    setEffectOrigins( getOrigin(), getOrigin(), 0 );

    entity repairTarget = getRepairTarget();
    entity ownerEntity = getOwnerEntity();

    // head back to the player
    vector ownerSize = ownerEntity.getMaxs() - ownerEntity.getMins();
    float maxReturnSpeed = 400 * sys.getFrameTime();
    float returnSpeedScale = 0.0f;

    returnTimeout = sys.getTime() + getFloatKey( "return_timeout" );

    while ( true ) {
        if ( forceState ) {
            return;
        }

        CheckOwner( ownerEntity );

        vector ownerOrigin = ownerEntity.getWorldOrigin();
        ownerOrigin_z += ownerSize_z * 1;

        vector targetOrigin = repairTarget.getWorldOrigin();
        targetOrigin_z += targetSize_z * 0.5;

        vector myOrigin = getWorldOrigin();
        vector delta = ownerOrigin - myOrigin;
        float deltaLength = sys.vecLength( delta );
        if ( !sys.isClient() ) {
            player p = ownerEntity;
            if ( deltaLength < 128 ) {
                if ( p != $null_entity ) {
                    p.setRepairDroneState( true );
                }
                remove();
            } else {
                if ( p != $null_entity ) {
                    if ( p.getVehicle() != $null_entity ) {
                        if ( deltaLength < 384 ) {
                            p.setRepairDroneState( true );                        
                            remove();
                        }
                    }
                }
            }
        }
        vector direction = sys.vecNormalize( delta );

        float returnSpeed = maxReturnSpeed * returnSpeedScale;
        returnSpeedScale = returnSpeedScale + 1.0f * sys.getFrameTime();
        if ( returnSpeedScale > 1.0f ) {
            returnSpeedScale = 1.0f;
        }


        vector newOrigin = myOrigin + direction * returnSpeed;

        // make sure it stays on the hemisphere
        vector targetDelta = targetOrigin - newOrigin;
        float targetDeltaLength = sys.vecLength( targetDelta );
        float deltaScale = orbitRadius / targetDeltaLength;
        if ( deltaScale > 1 ) {
            targetDelta *= deltaScale;
            newOrigin = targetOrigin - targetDelta;
        }

        // look towards the owner
        LookTowards( ownerOrigin );

        DoMove( newOrigin );
        sys.waitFrame();
    }
}

void repair_drone::OnPostDamage( entity attacker, float oldHealth, float newHealth ) {
}

void repair_drone::OnKilled( entity inflictor, entity attacker, float damage, vector direction, float location ) {
    SelfDestruct();
}

void repair_drone::SetEntities( entity target, entity owner ) {
    setEntities( target, owner );
}

void repair_drone::SetAction( float action ) {
    cachedAction = action;
}

void repair_drone::SoundThread() {
    float oldState;
    float waitTime;

    float droneSpeed;
    float newPitch;

    boolean playingBeamSound;

    startSound( "snd_idle", SND_WEAPON_IDLE );

    while( true ) {
        sys.waitFrame();

        droneSpeed = sys.fabs( getLinearVelocity() * getWorldAxis( 0 ) );
        newPitch = ( droneSpeed * 0.004f ) + 2.f;
        setChannelPitchShift( SND_WEAPON_IDLE, newPitch );

        if ( state != oldState || sys.getTime() > waitTime ) {
            waitTime = sys.getTime() + 10.f;
            oldState = state;

            if ( state == STATE_SEEK ) {
                startSound( "snd_seek", SND_WEAPON_SIG );
                if ( playingBeamSound ) {
                    playingBeamSound = false;
                    stopSound( SND_WEAPON_MECH );
                }
            } else if ( state == STATE_REPAIR ) {
                startSound( "snd_repair", SND_WEAPON_SIG );
                if ( !playingBeamSound ) {
                    playingBeamSound = true;
                    startSound( "snd_beam", SND_WEAPON_MECH );
                }
            } else if ( state == STATE_RETURN ) {
                startSound( "snd_return", SND_WEAPON_SIG );
                if ( playingBeamSound ) {
                    playingBeamSound = false;
                    stopSound( SND_WEAPON_MECH );
                }
            }
        }
    }
}

float repair_drone::OnUpdateCrosshairInfo( entity p ) {
    if ( sys.getLocalPlayer() == $null_entity ) {
        return 1.f;
    }

    float allegiance = getEntityAllegiance( p );
    vector color = GetAllegianceColor( allegiance );
    float distance = chGetDistance();
    float range = InchesToMetres( distance );
    float health = getHealth();

    chSetNumLines( 0 );
    float index;

    index = chAddLine();
    chSetLineTextIndex( index, g_locStr_Drone );
    chSetLineColor( index, color, 1.f );
    chSetLineType( index, CI_TEXT );
    chSetLineSize( index, 0, 0 );

    if ( health <= 0 ) {
        index = chAddLine();
        chSetLineTextIndex( index, g_locStr_Destroyed );
        chSetLineColor( index, color, 1.f );
        chSetLineType( index, CI_TEXT );
        chSetLineSize( index, 0, 0 );
    } else {
        index = chAddLine();
        chSetLineColor( index, color, 0.5f );
        chSetLineType( index, CI_BAR );
        chSetLineFraction( index, health / getMaxHealth() );
        chSetLineSize( index, 150, CROSSHAIR_INFO_BAR_HEIGHT );

        if ( range <= 100 ) {
            index = chAddLine();
            chSetLineText( index, G_BuildRangeStr( range ) );
            chSetLineColor( index, color, 1.f );
            chSetLineType( index, CI_TEXT );
            chSetLineSize( index, 0, 0 );
        }
    }

    return 1.f;
}


void tool_repair_drone::preinit() {
    meleeDistance    = getFloatKey( "melee_distance" );

    armNormal        = getIntKey( "can_arm_normal" );
    disarmCharge    = getIntKey( "can_disarm_charge" );
    armCharge        = getIntKey( "can_arm_charge" );
    canRepair        = getIntKey( "can_repair" );
    canConstruct    = getIntKey( "can_construct" );

    fireRate         = getFloatKeyWithDefault( "fire_rate", 0.1f );
    repairCount     = getFloatKeyWithDefault( "repair_count", 5.f );

    chargePerDrone    = getFloatKeyWithDefault( "charge_per_drone", 200.f );
    droneClass         = sys.getTypeHandle( "sdRepairDrone" );

    returnDelay        = getFloatKey( "return_delay" );
}

void tool_repair_drone::init() {
    weaponState( "Raise", 0 );
}

void tool_repair_drone::destroy() {
    sys.killThread( "ToolTipThread_Deployed_" + getName() );
    sys.killThread( "ToolTipThread_Raise_" + getName() );
    sys.killThread( "SoundThread_" + getName() );

    stopAllEffects();
    DestroySound();
}

void tool_repair_drone::Raise() {
    weaponRising();

    repair_drone activeDrone = FindDrone();
    if ( activeDrone != $null ) {
        if ( myPlayer.isLocalPlayer() ) {
            thread ToolTipThread_Deployed();
        }
        playAnim( ANIMCHANNEL_ALL, "raise_empty" );
        waitUntil( animDone( ANIMCHANNEL_ALL, PLIERS_RAISE_TO_IDLE ) );
        weaponState( "IdleEmpty", PLIERS_RAISE_TO_IDLE );
    } else {
        if ( myPlayer.isLocalPlayer() ) {
            thread ToolTipThread_Raise();
        }
        playAnim( ANIMCHANNEL_ALL, "raise" );
        waitUntil( animDone( ANIMCHANNEL_ALL, PLIERS_RAISE_TO_IDLE ) );
        weaponState( "Idle", PLIERS_RAISE_TO_IDLE );
    }
}

void tool_repair_drone::Lower() {
    if ( playingFireSound ) {
        playingFireSound = false;
        startSound( "snd_stop", SND_WEAPON_FIRE );
    }

    weaponLowering();

    repair_drone activeDrone = FindDrone();
    if ( activeDrone != $null ) {
        playAnim( ANIMCHANNEL_ALL, "putaway_empty" );
        waitUntil( animDone( ANIMCHANNEL_ALL, 0 ) );
    } else {
        playAnim( ANIMCHANNEL_ALL, "putaway" );
        waitUntil( animDone( ANIMCHANNEL_ALL, 0 ) );
    }

    stopEffect( "fx_loop" );
    weaponHolstered();
    waitUntil( WEAPON_RAISEWEAPON );
    weaponState( "Raise", 0 );
}

boolean tool_repair_drone::WantsAttack() {
    return WEAPON_ATTACK || myPlayer.getButton( PK_ACTIVATE );
}

boolean tool_repair_drone::AttackValid() {
    boolean performAction = CheckAttack( MASK_VEHICLESOLID | CONTENTS_PLAYERCLIP );

    if ( !performAction ) {
        performAction = CheckAttack( CONTENTS_TRIGGER );
    }

    if ( !performAction ) {
        performAction = CheckAttack( CONTENTS_RENDERMODEL | CONTENTS_FORCEFIELD );
    }

    return performAction;
}

void tool_repair_drone::Idle() {
    repair_drone activeDrone;

    weaponReady();

    playCycle( ANIMCHANNEL_ALL, "idle" );

    lastAltAttack = WEAPON_ALTFIRE;

    while ( true ) {
        activeDrone = FindDrone();

        if ( sys.isClient() ) {
            //  if client lags it might have to jump to IdleEmpty from here
            if ( activeDrone != $null ) {
                weaponState( "IdleEmpty", TOOL_REPAIR_DRONE_IDLE_TO_IDLE );
            }
        }

        if ( WEAPON_LOWERWEAPON ) {
            myPlayer.SetProgressBarVisible( false );
            weaponState( "Lower", 4 );
        }

        if ( WantsAttack() ) {
            if ( AttackValid() ) {
                if ( !playingFireSound ) {
                    playingFireSound = true;
                    startSound( "snd_start", SND_WEAPON_FIRE );
                }

                nextActionFailMessageTime = sys.getTime() + 2.f;

                weaponState( "Attack", 4 );
            } else {
                if ( !myPlayer.isToolTipPlaying() ) {
                    if ( sys.getTime() > nextActionFailMessageTime ) {
                        nextActionFailMessageTime = sys.getTime() + 5.f;
                        if ( cachedAction == AC_NONE ) {
                            myPlayer.sendToolTip( GetToolTip( getKey( "tt_action_failed" ) ) );
                        } else if ( cachedAction == AC_ENEMY_REPAIR ) {
                            myPlayer.sendToolTip( GetToolTip( getKey( "tt_enemy_repair" ) ) );
                        }
                    }
                }
            }
        }

        if ( playingFireSound ) {
            playingFireSound = false;
            startSound( "snd_stop", SND_WEAPON_FIRE );
        }

        // check proficiency level ->
        if ( activeDrone != $null_entity ) {
            enableTargetLock( 0 );
        } else {
            // only enable target lock when the player can actually perform the action
            enableTargetLock( ShowTargetLock() );
        }

        if ( WEAPON_ALTFIRE && !lastAltAttack ) {
            if ( activeDrone != $null_entity ) {
                if ( sys.getTime() - activateDroneTime > returnDelay ) {
                    activeDrone.ChangeState( STATE_RETURN );
                }
            } else {
                if ( CheckAltAttack() ) {
                    if ( myPlayer.getAmmo( g_ammoStroyent ) >= chargePerDrone ) {
                        activateDroneTime = sys.getTime();
                        weaponState( "AltAttack", 4 );
                    } else {
                        if ( myPlayer.isLocalPlayer() ) {
                            sys.startSoundDirect( getKey( "snd_no_stroyent" ), SND_WEAPON_FIRE_LOCAL );
                        }
                    }
                }
            }
        }
        // <- check proficiency level

        lastAltAttack = WEAPON_ALTFIRE;

        sys.waitFrame();
    }
}

void tool_repair_drone::IdleEmpty() {
    weaponReady();

    if ( myPlayer.isLocalPlayer() ) {
        deployedTipThreadActive = false;
        sys.killThread( "ToolTipThread_Deployed_" + getName() );
        thread ToolTipThread_Deployed();
    }

    playCycle( ANIMCHANNEL_ALL, "idle_empty" );

    repair_drone activeDrone = FindDrone();

    while ( activeDrone != $null_entity ) {
        activeDrone = FindDrone();

        if ( WEAPON_LOWERWEAPON ) {
            weaponState( "Lower", 4 );
        }

        if ( WEAPON_ALTFIRE && !lastAltAttack ) {
            if ( activeDrone != $null_entity ) {
                if ( sys.getTime() - activateDroneTime > returnDelay ) {
                    activeDrone.ChangeState( STATE_RETURN );
                }
            }
        }

        lastAltAttack = WEAPON_ALTFIRE;
        sys.waitFrame();
    }

    playAnim( ANIMCHANNEL_ALL, "catch" );
    waitUntil( animDone( ANIMCHANNEL_ALL, 4 ) );

    deployedTipThreadActive = false;
    sys.killThread( "ToolTipThread_Deployed_" + getName() );

    weaponState( "Idle", 4 );
}

void tool_repair_drone::ClearDrone() {
    if ( sys.isClient() ) {
        return;
    }

    entity other = FindDrone();
    if ( other != $null_entity ) {
        myPlayer.binRemove( other );
    }
}

entity tool_repair_drone::FindDrone() {
    float i;
    float num = myPlayer.binGetSize();

    for ( i = 0; i < num; i++ ) {
        entity other = myPlayer.binGet( i );
        if ( other == $null_entity ) {
            continue;
        }

        repair_drone drone = other;
        if ( drone != $null_entity ) {
            return other;
        }
    }

    return $null_entity;
}

void tool_repair_drone::Attack() {
    myPlayer.AI_HOLD_WEAPON = true;

    float currentCachedAction = cachedAction;

    playAnim( ANIMCHANNEL_ALL, "fire_start" );
    waitUntil( animDone( ANIMCHANNEL_ALL, 4 ) );

    playEffect( "fx_loop", "repair_drone_joint15", 1 );

    while ( true ) {
        if ( cachedAction == AC_REPAIR ) {
            Repair();
        } else if ( cachedAction == AC_ARM || cachedAction == AC_ARM_CHARGE ) {
            Arm();
        } else if ( cachedAction == AC_DISARM || cachedAction == AC_DISARM_CHARGE ) {
            Disarm();
        } else if ( cachedAction == AC_CONSTRUCT ) {
            Construct();
        }

        float finishTime = sys.getTime() + fireRate;

        while ( sys.getTime() < finishTime ) {
            if ( animDone( ANIMCHANNEL_ALL, 4 ) ) {
                playAnim( ANIMCHANNEL_ALL, "fire_loop" );
            }

            sys.waitFrame();
        }

        if ( WEAPON_LOWERWEAPON ) {
            break;
        }

        if ( !WantsAttack() ) {
            break;
        }

        if ( !AttackValid() ) {
            break;
        }
    }

    myPlayer.AI_HOLD_WEAPON = false;

    stopEffect( "fx_loop" );

    playAnim( ANIMCHANNEL_ALL, "fire_stop" );
    waitUntil( animDone( ANIMCHANNEL_ALL, 4 ) );

    nextActionFailMessageTime = sys.getTime() + 2.f;

    weaponState( "Idle", 4 );
}

void tool_repair_drone::AltAttack() {
    playAnim( ANIMCHANNEL_ALL, "release" );

    DoAttack( "def_drone" );
}

void tool_repair_drone::DoAttack( string droneType ) {
    fired();

    enableTargetLock( 0 );
    waitUntil( animDone( ANIMCHANNEL_ALL, 4 ) );

    myPlayer.setAmmo( g_ammoStroyent, myPlayer.getAmmo( g_ammoStroyent ) - ( chargePerDrone ) );

    // spawn a drone

    if ( !sys.isClient() ) {    
        float droneIndex = GetEntityDef( getKey( droneType ) );
        repair_drone activeDrone = sys.spawnType( droneIndex );

        vector droneOrigin = myPlayer.getViewOrigin();
        vector playerAngles = myPlayer.getViewAngles();
        droneOrigin += sys.angToForward( playerAngles ) * 32;
        activeDrone.setOrigin( droneOrigin );
        activeDrone.setAngles( playerAngles );

        activeDrone.SetEntities( cachedEntity, myPlayer );
        activeDrone.SetAction( cachedAction );
        activeDrone.setGameTeam( myPlayer.getGameTeam() );

        myPlayer.binAdd( activeDrone );
    }

    weaponState( "IdleEmpty", TOOL_REPAIR_DRONE_FIRE_TO_IDLE );
}

boolean tool_repair_drone::CheckAttack( float mask ) {
    cachedEntity = myPlayer.getCrosshairEntity();
    cachedAction = AC_NONE;

    if ( myPlayer.getCrosshairDistance( true ) > meleeDistance ) {
        cachedEntity = $null_entity;
        return false;
    }

    if ( cachedEntity == $null_entity ) {
        // there is no crosshair entity to take priority
        // try doing a melee trace
        melee( mask, meleeDistance, false, false );
        cachedEntity = getMeleeEntity();

        if ( cachedEntity == $null_entity ) {
            return false;
        }
    }

    if ( armCharge ) {
        if ( cachedEntity.vCheckActionCode( myPlayer, AC_ARM_CHARGE ) ) {
            cachedAction = AC_ARM_CHARGE;
            return true;
        }
    }

    if ( disarmCharge ) {
        if ( cachedEntity.vCheckActionCode( myPlayer, AC_DISARM_CHARGE ) ) {
            cachedAction = AC_DISARM_CHARGE;
            return true;
        }
    }

    if ( canRepair ) {
        if ( cachedEntity.vCheckActionCode( myPlayer, AC_REPAIR ) ) {
            cachedAction = AC_REPAIR;
            return true;
        } else if ( cachedEntity.vCheckActionCode( myPlayer, AC_ENEMY_REPAIR ) ) {
            cachedAction = AC_ENEMY_REPAIR;
            return false;
        }
    }

    if ( armNormal ) {
        if ( cachedEntity.vCheckActionCode( myPlayer, AC_ARM ) ) {
            cachedAction = AC_ARM;
            return true;
        }

        if ( cachedEntity.vCheckActionCode( myPlayer, AC_DISARM ) ) {
            cachedAction = AC_DISARM;
            return true;
        }
    }

    if ( canConstruct ) {
        if ( cachedEntity.vCheckActionCode( myPlayer, AC_CONSTRUCT ) ) {
            cachedAction = AC_CONSTRUCT;
            return true;
        }
    }

    return false;
}

boolean tool_repair_drone::CheckAltAttack() {
    if ( myPlayer.getProficiency( g_proficiencyConstructor ) < 2 ) {
        return false;
    }

    repair_drone activeDrone = FindDrone();
    if ( activeDrone != $null_entity ) {
        return false;
    }

    cachedEntity = myPlayer.getEnemy();
    enableTargetLock( 1 );
    if ( cachedEntity == $null_entity ) {
        cachedAction = AC_NONE;
        return false;
    }

    if ( cachedEntity.vCheckActionCode( myPlayer, AC_REPAIR ) ) {
        cachedAction = AC_REPAIR;
        return true;
    }

    return false;
}

boolean tool_repair_drone::ShowTargetLock() {
    return myPlayer.getProficiency( g_proficiencyConstructor ) >= 2;
}

void tool_repair_drone::Construct() {
    cachedEntity.vConstruct( myPlayer );
    myPlayer.ShowProgressBar( cachedEntity, AC_CONSTRUCT );
}

void tool_repair_drone::Repair() {
    cachedEntity.vRepair( repairCount * 0.5f, myPlayer );
    myPlayer.ShowProgressBar( cachedEntity, AC_REPAIR );
}

void tool_repair_drone::Disarm() {
    cachedEntity.vArm( myPlayer );
    myPlayer.ShowProgressBar( cachedEntity, AC_DISARM );
}

void tool_repair_drone::Arm() {
    cachedEntity.vArm( myPlayer );
    myPlayer.ShowProgressBar( cachedEntity, AC_ARM );
}

void tool_repair_drone::ToolTipThread_Raise() {
    myPlayer.cancelToolTips();
    sys.wait( myPlayer.CalcTooltipWait() );

    WAIT_FOR_TOOLTIP;
    myPlayer.sendToolTip( GetToolTip( getKey( "tt_intro_1" ) ) );

    WAIT_FOR_TOOLTIP;
    if ( myPlayer.getProficiency( g_proficiencyConstructor ) < 2 ) {
        myPlayer.sendToolTip( GetToolTip( getKey( "tt_intro_2" ) ) );
    } else {
        myPlayer.sendToolTip( GetToolTip( getKey( "tt_intro_advanced_2" ) ) );
    }
}

void tool_repair_drone::ToolTipThread_Deployed() {
    if ( deployedTipThreadActive ) {
        return;
    }
    deployedTipThreadActive = true;

    WAIT_FOR_TOOLTIP;
    myPlayer.sendToolTip( GetToolTip( getKey( "tt_deployed_intro_1" ) ) );

    //WAIT_FOR_TOOLTIP;
    //myPlayer.sendToolTip( GetToolTip( getKey( "tt_deployed_intro_2" ) ) );

    deployedTipThreadActive = false;
}

void tool_repair_drone::OwnerDied() {
    repair_drone activeDrone = FindDrone();
    if ( activeDrone != $null_entity ) {
        ClearDrone();
    }
}