Grappling Hook

    This grappling hook is a modified version of the Expert Quake2 grappling hook.  It uses the push function in Quake2 to prevent jerkiness and bouncing, even when you have moderate lag.  I personally believe that it's the best grappling hook around.  It is offhand, so you don't have to switch weapons to fire it.

Remember to add in the text that is blue, taking out any text that is pink.

First open up g_local.h, we're going to define some variables in the gclient_s structure:

 


    qboolean    grenade_blew_up;
    float        grenade_time;
    int             silencer_shots;
    int             weapon_sound;

    float        pickup_msg_time;

    float        respawn_time;         // can respawn when time > this

    //Grapple Variables
    edict_t        *hook;       
    edict_t        *hook_touch; 
    qboolean    on_hook;     
    int             hook_frame;

 

Next we need to change a function in p_weapon.c, so open it up and remove the "static" part of the P_projectsource like this:

 

static void P_ProjectSource (gclient_t *client, vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result)
{
    vec3_t    _distance;

    VectorCopy (distance, _distance);
    if (client->pers.hand == LEFT_HANDED)
        _distance[1] *= -1;
    else if (client->pers.hand == CENTER_HANDED)
        _distance[1] = 0;
    G_ProjectSource (point, _distance, forward, right, result);
}

 

Next we're going to make two new files: grapple.c and grapple.h.  Make sure you add grapple.c to your project!

In grapple.c, add the following section:

 

#include "g_local.h"
#include "grapple.h"


#define HOOK_TIME        5000   
#define HOOK_SPEED        1200   
#define THINK_TIME        0.3        
#define HOOK_DAMAGE        5        
#define GRAPPLE_REFIRE    2    
#define PULL_SPEED    500 


void P_ProjectSource (gclient_t *client, vec3_t point, vec3_t distance, vec3_t forward, vec3_t right, vec3_t result);

qboolean Ended_Grappling (gclient_t *client)
{
    return (!(client->buttons & BUTTON_USE) && client->oldbuttons & BUTTON_USE);
}

qboolean Is_Grappling (gclient_t *client)
{
    return (client->hook == NULL) ? false : true;
}

void Grapple_Touch(edict_t *hook, edict_t *other, cplane_t *plane, csurface_t *surf)
{
    // Release if hitting its owner
    if (other == hook->owner)
        return;
    if (!Is_Grappling(hook->owner->client) && hook->health == 0) {
        return;
    }

    hook->health = 0;
    if (surf && surf->flags & SURF_SKY)
        {
            Release_Grapple(hook);
            return;
    }

    if (other != g_edicts && other->clipmask == MASK_SHOT)
        return;
    gi.WriteByte (svc_temp_entity);
    gi.WriteByte (TE_BLASTER);
    gi.WritePosition (hook->s.origin);
    gi.WriteDir (plane->normal);
    gi.multicast (hook->s.origin, MULTICAST_PVS);
    //gi.sound(hook, CHAN_ITEM, gi.soundindex("hook/hit.wav"), 1, ATTN_NORM, 0);
//uncomment this line to make it play a sound         //when your hook hits a wall

   if (other != NULL) {
        T_Damage(other, hook, hook->owner, hook->velocity, hook->s.origin, plane->normal, HOOK_DAMAGE, 0, 0, MOD_SUICIDE);
    }
    if (other != g_edicts && other->health && other->solid == SOLID_BBOX) {
        Release_Grapple(hook);
        return;
    }

    if (other != g_edicts && other->inuse &&
        (other->movetype == MOVETYPE_PUSH || other->movetype == MOVETYPE_STOP))
    {
        other->mynoise2 = hook;
        hook->owner->client->hook_touch = other;
        hook->enemy = other;
        hook->groundentity = NULL;
        hook->flags |= FL_TEAMSLAVE;
    }

    VectorClear(hook->velocity);
    VectorClear(hook->avelocity);
    hook->solid = SOLID_NOT;
    hook->touch = NULL;
    hook->movetype = MOVETYPE_NONE;
    hook->delay = level.time + HOOK_TIME;
    hook->owner->client->on_hook = true;
    hook->owner->groundentity = NULL;
    Pull_Grapple(hook->owner);

}

void Think_Grapple(edict_t *hook)
{
    if (level.time > hook->delay)
        hook->prethink = Release_Grapple;
    else
    {
        if (hook->owner->client->hook_touch) {
            edict_t *obj = hook->owner->client->hook_touch;

            if (obj == g_edicts)
            {
                Release_Grapple(hook);
                return;
            }

            if (obj->inuse == false) {
                Release_Grapple(hook);
                return;
            }

            if (obj->deadflag == DEAD_DEAD)
            {
                Release_Grapple(hook);
                return;
            }

            // Movement code is handled with the MOVETYPE_PUSH stuff in g_phys.c

            T_Damage(obj, hook, hook->owner, hook->velocity, hook->s.origin, vec3_origin, HOOK_DAMAGE, 0, 0, MOD_SUICIDE);
        }

        hook->nextthink += THINK_TIME;
    }
}

static void DrawBeam (edict_t *ent)
{
    gi.WriteByte (svc_temp_entity);
    gi.WriteByte (TE_BFG_LASER);
    gi.WritePosition (ent->owner->s.origin);
    gi.WritePosition (ent->s.origin);
    gi.multicast (ent->s.origin, MULTICAST_PHS);
}
void Make_Hook(edict_t *ent)
{
    edict_t *hook;
    vec3_t forward, right, start, offset;

    hook = G_Spawn();
    AngleVectors(ent->client->v_angle, forward, right, NULL);
    VectorScale(forward, -2, ent->client->kick_origin);
    ent->client->kick_angles[0] = -1;
    VectorSet(offset, 8, 0, ent->viewheight-8);
    P_ProjectSource (ent->client, ent->s.origin, offset, forward, right, start);

    VectorCopy(start, hook->s.origin);
    VectorCopy(forward, hook->movedir);
    vectoangles(forward, hook->s.angles);
    VectorScale(forward, HOOK_SPEED, hook->velocity);
    VectorSet(hook->avelocity, 0, 0, 500);

    hook->classname = "hook";
    hook->movetype = MOVETYPE_FLYMISSILE;
    hook->clipmask = MASK_SHOT;
    hook->solid = SOLID_BBOX;
    hook->svflags |= SVF_DEADMONSTER;
    hook->s.renderfx = RF_FULLBRIGHT;
    VectorClear (hook->mins);
    VectorClear (hook->maxs);
    hook->s.effects |= EF_COLOR_SHELL;
    hook->s.renderfx |= RF_SHELL_GREEN;   
    hook->s.modelindex = gi.modelindex ("models/objects/flash/tris.md2");   
    hook->owner = ent;
    hook->touch = Grapple_Touch;
    hook->delay = level.time + HOOK_TIME;
    hook->nextthink = level.time;

    hook->prethink = DrawBeam;
    hook->think = Think_Grapple;
    hook->health = 100;
    hook->svflags = SVF_MONSTER;
   
    ent->client->hook = hook;
    gi.linkentity(hook);
}

void Throw_Grapple (edict_t *player)
{
   
    if (player->client->hook) {
        return;
    }

    gi.sound(player, CHAN_ITEM, gi.soundindex("medic/medatck2.wav"), 0.5, ATTN_NORM, 0);

    player->client->hook_touch = NULL;
   
    Make_Hook(player);
}

void Release_Grapple (edict_t *hook)
{
    edict_t *owner = hook->owner;
    gclient_t *client = hook->owner->client;
    edict_t *link = hook->teamchain;

    client->on_hook = false;
    client->hook_touch = NULL;

    if (client->hook != NULL) {
        client->hook = NULL;
        VectorClear(client->oldvelocity);

        hook->think = NULL;

        if (hook->enemy) {
            hook->enemy->mynoise2 = NULL;
        }

        G_FreeEdict(hook);
    }
}

void Pull_Grapple (edict_t *player)
{
    vec3_t hookDir;
    vec_t length;

    VectorSubtract(player->client->hook->s.origin, player->s.origin, hookDir);
    length = VectorNormalize(hookDir);

    VectorScale(hookDir, /*player->scale * */ PULL_SPEED, player->velocity);
    VectorCopy(hookDir, player->movedir);

//To move the player off the ground just a bit so he doesn't stay stuck (version 3.17 bug)
    if (player->velocity[2] > 0) {

        vec3_t traceTo;
        trace_t trace;

        // find the point immediately above the player's origin
        VectorCopy(player->s.origin, traceTo);
        traceTo[2] += 1;

        // trace to it
        trace = gi.trace(traceTo, player->mins, player->maxs, traceTo, player, MASK_PLAYERSOLID);

        // if there isn't a solid immediately above the player
        if (!trace.startsolid) {
            player->s.origin[2] += 1;    // make sure player off ground
        }
    }

}

 

These are all the functions used to create, fire, and use the grappling hook.   Next add this code to grapple.h:

 

qboolean Started_Grappling(gclient_t *client);
qboolean Ended_Grappling(gclient_t *client);
qboolean Is_Grappling(gclient_t *client);
void Throw_Grapple(edict_t *ent);
void Release_Grapple(edict_t *ent);
void Think_Grapple(edict_t *ent);
void Pull_Grapple(edict_t *ent);

Next go to p_client.c, and in the player_die function add these lines like this:

 

/*
==================
player_die
==================
*/
void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point)
{
    int        n;

    if (self->client->hook)
        Release_Grapple(self->client->hook);


    VectorClear (self->avelocity);

    self->takedamage = DAMAGE_YES;
    self->movetype = MOVETYPE_TOSS;

 

This just removes the hook if the player dies.  Next go down to the clientthink function and add in these few lines like this:

 

    if (level.intermissiontime)
    {
        client->ps.pmove.pm_type = PM_FREEZE;
        // can exit intermission after five seconds
        if (level.time > level.intermissiontime + 5.0
            && (ucmd->buttons & BUTTON_ANY) )
            level.exitintermission = true;
        return;
    }

    if (client->on_hook == true)
    {
        Pull_Grapple(ent);
        client->ps.pmove.gravity = 0;
    }
    else
    {
        client->ps.pmove.gravity = sv_gravity->value;
    }

    pm_passent = ent;

    if (ent->client->chase_target) {

        client->resp.cmd_angles[0] = SHORT2ANGLE(ucmd->angles[0]);
        client->resp.cmd_angles[1] = SHORT2ANGLE(ucmd->angles[1]);
        client->resp.cmd_angles[2] = SHORT2ANGLE(ucmd->angles[2]);

    } else {

        // set up for pmove
        memset (&pm, 0, sizeof(pm));

        if (ent->movetype == MOVETYPE_NOCLIP)
            client->ps.pmove.pm_type = PM_SPECTATOR;
        else if (ent->s.modelindex != 255)
            client->ps.pmove.pm_type = PM_GIB;
        else if (ent->deadflag)
            client->ps.pmove.pm_type = PM_DEAD;
        else
            client->ps.pmove.pm_type = PM_NORMAL;
client->ps.pmove.gravity = sv_gravity->value;//REMOVE THIS LINE!
        pm.s = client->ps.pmove;

 

This checks to see if the grappling hook has lodged in a wall and starts the pulling function which would draw you to the hook.  It also turns off the gravity while your are on the hook.  Next go down to the bottom of clientthink and add in these few lines:

 

    // fire weapon from final position if needed
    if (client->latched_buttons & BUTTON_ATTACK)
    {
        if (!client->weapon_thunk)
        {
            client->weapon_thunk = true;
            Think_Weapon (ent);
        }
    }

   // Check to see if player pressing the "use" key
    if (ent->client->buttons & BUTTON_USE && !ent->deadflag && client->hook_frame <= level.framenum)
    {     
    Throw_Grapple (ent);    
    }
    if    (Ended_Grappling (client) && !ent->deadflag && client->hook)
    {
        Release_Grapple (client->hook);
    }


}

 

This was an extremely important part, it set the actual grapple function to the use command, so using the command +use will fire the grapple!   For some unknown reason, Id Software left that command open for use.  Next go back to the top, we need to #include grapple.h because it has the declarations for all the functions:

 

#include "g_local.h"
#include "m_player.h"

#include "grapple.h"

void ClientUserinfoChanged (edict_t *ent, char *userinfo);

void SP_misc_teleporter_dest (edict_t *ent);

Next we need to modify a small section in g_misc.c to set the hook time for setting the refire delay.  Add in the lines shown:


void teleporter_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf)
{
    edict_t        *dest;
    int             i;

    if (!other->client)
        return;
    dest = G_Find (NULL, FOFS(targetname), self->target);
    if (!dest)
    {
        gi.dprintf ("Couldn't find destination\n");
        return;
    }

    // unlink to make sure it can't possibly interfere with KillBox
    gi.unlinkentity (other);

    VectorCopy (dest->s.origin, other->s.origin);
    VectorCopy (dest->s.origin, other->s.old_origin);
    other->s.origin[2] += 10;

    // clear the velocity and hold them in place briefly
    VectorClear (other->velocity);
    other->client->ps.pmove.pm_time = 160>>3;         // hold time
    other->client->ps.pmove.pm_flags |= PMF_TIME_TELEPORT;

    other->client->hook_frame = level.framenum + 1;

    // draw the teleport splash at source and on the player
    self->owner->s.event = EV_PLAYER_TELEPORT;
    other->s.event = EV_PLAYER_TELEPORT;

    // set angles
    for (i=0 ; i<3 ; i++)
        other->client->ps.pmove.delta_angles[i] = ANGLE2SHORT(dest->s.angles[i] - other->client->resp.cmd_angles[i]);

    VectorClear (other->s.angles);
    VectorClear (other->client->ps.viewangles);
    VectorClear (other->client->v_angle);

    // kill anything at the destination
    KillBox (other);

   if (Is_Grappling(other->client)) {
        Release_Grapple(other->client->hook);
    }


    gi.linkentity (other);
}

 

Now go back to the top and do another #include like so:

 

// g_misc.c

#include "g_local.h"

#include "grapple.h"

/*QUAKED func_group (0 0 0) ?
Used to group brushes together just for editor convenience.
*/

 

Next open up g_phys.c and add the #include to the top like this:

// g_phys.c

#include "g_local.h"

#include "grapple.h"

And now go down to the sv_physics_pusher function and add in the indicated lines like so:

 

void SV_Physics_Pusher (edict_t *ent)
{
    vec3_t        move, amove;
    edict_t        *part, *mv;

    // if not a team captain, so movement will be handled elsewhere
    if ( ent->flags & FL_TEAMSLAVE)
        return;

    // make sure all team slaves can move before commiting
    // any moves or calling any think functions
    // if the move is blocked, all moved objects will be backed out
//retry:
    pushed_p = pushed;
    for (part = ent ; part ; part=part->teamchain)
    {
        qboolean blocked = false;
        if (part->mynoise2 && part->mynoise2->think == Think_Grapple)
        {
            edict_t *hook = part->mynoise2;
            vec3_t org, org2, forward, right, up, move2;

            if (hook->enemy == NULL || hook->inuse == false)
                continue;

            VectorScale (part->velocity, FRAMETIME, move);
            VectorScale (part->avelocity, FRAMETIME, amove);            

            VectorAdd(hook->s.origin, move, hook->s.origin);
            VectorAdd(hook->s.angles, amove, hook->s.angles);

            // figure movement due to the pusher's amove
            VectorSubtract (vec3_origin, amove, org);
            AngleVectors (org, forward, right, up);

            VectorSubtract (hook->s.origin, part->s.origin, org);
            org2[0] = DotProduct (org, forward);
            org2[1] = -DotProduct (org, right);
            org2[2] = DotProduct (org, up);
            VectorSubtract (org2, org, move2);
            VectorAdd (hook->s.origin, move2, hook->s.origin);
        }

        if (part->velocity[0] || part->velocity[1] || part->velocity[2] ||
            part->avelocity[0] || part->avelocity[1] || part->avelocity[2]
            )
        {    // object is moving
            VectorScale (part->velocity, FRAMETIME, move);
            VectorScale (part->avelocity, FRAMETIME, amove);

            if (!SV_Push (part, move, amove))
                break;     // move was blocked
        }
    }

 

All this does is give the hook the same velocity as the entity it's stuck in.  That's it!  You now have an offhand grappling hook that is used with the command "+use".  If you want, you can alias this command to +hook by using the stuffcmd() tutorial also on the tutorial page.  It would look like this:

    stuffcmd(ent, "alias +hook \"+use\"\n");
    stuffcmd(ent, "alias -hook \"-use\"\n");

I hope this helps!  If there are any problems with this grapple, please let me know, I understand how coders hate it when a tutorial doesn't work.

Tutorial by Willi