Quake DeveLS - BFG Grenade Launcher

Author: Mark "Grey" Davies
Difficulty: Easy

Overview:

This tutorial just explains how to add bfg grenade launcher support to your mod. In order to fire the bfg grenades from the launcher you need to get the Disruptor Shield Tech, the grenade launcher and some cells. When the grenade is fired it bounces around like normal but then explodes into a static bfg ball.

The cvar "bfg_grenades" is used to indicate if bfg grenades are on (1) or off (0) and the cvar "bfg_grenade_cost" indicates how many cells are required to fire the bfg grenade.

Instructions:

Open the file g_weapon.c and add the following functions

static void BFG_Grenade_Explode (edict_t *ent)
{
    float	damage_radius = (1000/4);
    vec3_t	forward       = {0};
    int		damage        = (200/4);
    int     timer         = (8000/2.5);    /* next think = 8000/timer (2.5 secs) */

    fire_bfg (ent->owner, ent->s.origin, forward, damage, timer, damage_radius);
    
    G_FreeEdict (ent);
}


void fire_bfg_grenade (edict_t *self, vec3_t start, vec3_t aimdir, int damage, int speed, float timer, float damage_radius)
{
    edict_t	*grenade;
    vec3_t	dir;
    vec3_t	forward, right, up;

    vectoangles (aimdir, dir);
    AngleVectors (dir, forward, right, up);

    grenade = G_Spawn();
    VectorCopy (start, grenade->s.origin);
    VectorScale (aimdir, speed, grenade->velocity);
    VectorMA (grenade->velocity, 200 + crandom() * 10.0, up, grenade->velocity);
    VectorMA (grenade->velocity, crandom() * 10.0, right, grenade->velocity);
    VectorSet (grenade->avelocity, 300, 300, 300);
    grenade->movetype = MOVETYPE_BOUNCE;
    grenade->clipmask = MASK_SHOT;
    grenade->solid = SOLID_BBOX;
    grenade->s.effects |= EF_GRENADE | EF_BFG;
    VectorClear (grenade->mins);
    VectorClear (grenade->maxs);
    grenade->s.modelindex = gi.modelindex ("models/objects/grenade/tris.md2");
    grenade->owner = self;
    grenade->touch = Grenade_Touch;
    grenade->nextthink = level.time + timer;
    grenade->think = BFG_Grenade_Explode;
    grenade->dmg = damage;
    grenade->dmg_radius = damage_radius;
    grenade->classname = "bfggrenade";
    grenade->s.sound = gi.soundindex ("weapons/bfg__l1a.wav");

    gi.sound(self, CHAN_AUTO,  grenade->s.sound, 1, ATTN_NORM, 0);
    
    gi.linkentity (grenade);
}

Replace the fire_bfg() function with

void fire_bfg (edict_t *self, vec3_t start, vec3_t dir, int damage, int speed, float damage_radius)
{
	edict_t	*bfg;

	bfg = G_Spawn();
	VectorCopy (start, bfg->s.origin);
	VectorCopy (dir, bfg->movedir);
	vectoangles (dir, bfg->s.angles);
	VectorScale (dir, speed, bfg->velocity);
	bfg->movetype = MOVETYPE_FLYMISSILE;
	bfg->clipmask = MASK_SHOT;
	bfg->solid = SOLID_BBOX;
	bfg->s.effects |= EF_BFG | EF_ANIM_ALLFAST;
	VectorClear (bfg->mins);
	VectorClear (bfg->maxs);
	bfg->s.modelindex = gi.modelindex ("sprites/s_bfg1.sp2");
	bfg->owner = self;
	bfg->touch = bfg_touch;
	bfg->radius_dmg = damage;
	bfg->dmg_radius = damage_radius;
	bfg->classname = "bfg blast";
	bfg->s.sound = gi.soundindex ("weapons/bfg__l1a.wav");

    
    bfg->nextthink = level.time + 8000/speed;
    bfg->think = G_FreeEdict;

    bfg->think = bfg_think;
	bfg->nextthink = level.time + FRAMETIME;


    bfg->teammaster = bfg;
	bfg->teamchain = NULL;

    bfg->last_move_time = 0;
    if( dir[0] == dir[1] == dir[2] == 0 )
    {
        bfg->last_move_time = level.time + 8000/speed;
        bfg->classname = "bfg greneade blast";
    }
    
	if (self->client)
		check_dodge (self, bfg->s.origin, dir, speed);

	gi.linkentity (bfg);
}

Replace the bfg_think() function with

void bfg_think (edict_t *self)
{
	edict_t	*ent;
	edict_t	*ignore;
	vec3_t	point;
	vec3_t	dir;
	vec3_t	start;
	vec3_t	end;
	int		dmg;
	trace_t	tr;

    /* MarkD */
    if( (0 != self->last_move_time) && (self->last_move_time < level.time) )
    {
        self->think = bfg_explode;
        self->nextthink = level.time + FRAMETIME;
        return;
    }
    
	if (deathmatch->value)
		dmg = 5;
	else
		dmg = 10;

	ent = NULL;
	while ((ent = findradius(ent, self->s.origin, 256)) != NULL)
	{
		if (ent == self)
			continue;

		if (ent == self->owner)
			continue;

		if (!ent->takedamage)
			continue;

		if (!(ent->svflags & SVF_MONSTER) && (!ent->client) && (strcmp(ent->classname, "misc_explobox") != 0))
			continue;

		VectorMA (ent->absmin, 0.5, ent->size, point);

		VectorSubtract (point, self->s.origin, dir);
		VectorNormalize (dir);

		ignore = self;
		VectorCopy (self->s.origin, start);
		VectorMA (start, 2048, dir, end);
		while(1)
		{
			tr = gi.trace (start, NULL, NULL, end, ignore, CONTENTS_SOLID|CONTENTS_MONSTER|CONTENTS_DEADMONSTER);

			if (!tr.ent)
				break;

			// hurt it if we can
			if ((tr.ent->takedamage) && !(tr.ent->flags & FL_IMMUNE_LASER) && (tr.ent != self->owner))
				T_Damage (tr.ent, self, self->owner, dir, tr.endpos, vec3_origin, dmg, 1, DAMAGE_ENERGY, MOD_BFG_LASER);

			// if we hit something that's not a monster or player we're done
			if (!(tr.ent->svflags & SVF_MONSTER) && (!tr.ent->client))
			{
				gi.WriteByte (svc_temp_entity);
				gi.WriteByte (TE_LASER_SPARKS);
				gi.WriteByte (4);
				gi.WritePosition (tr.endpos);
				gi.WriteDir (tr.plane.normal);
				gi.WriteByte (self->s.skinnum);
				gi.multicast (tr.endpos, MULTICAST_PVS);
				break;
			}

			ignore = tr.ent;
			VectorCopy (tr.endpos, start);
		}

		gi.WriteByte (svc_temp_entity);
		gi.WriteByte (TE_BFG_LASER);
		gi.WritePosition (self->s.origin);
		gi.WritePosition (tr.endpos);
		gi.multicast (self->s.origin, MULTICAST_PHS);
	}

	self->nextthink = level.time + FRAMETIME;
}

Open the file p_weapon.c and replace the weapon_grenadelauncher_fire() function with

void weapon_grenadelauncher_fire (edict_t *ent)
{
	vec3_t	offset;
	vec3_t	forward, right;
	vec3_t	start;
	int		damage = 120;
	float	radius;

	radius = damage+40;
	if (is_quad)
		damage *= 4;

	VectorSet(offset, 8, 8, ent->viewheight-8);
	AngleVectors (ent->client->v_angle, forward, right, NULL);
	P_ProjectSource (ent->client, ent->s.origin, offset, forward, right, start);

	VectorScale (forward, -2, ent->client->kick_origin);
	ent->client->kick_angles[0] = -1;

    /* Fire Grenade */
    {
        static gitem_t *tech = NULL;
        cvar_t *bfgg = gi.cvar( "bfg_grenades", "0", CVAR_SERVERINFO );
        cvar_t *bfgc = gi.cvar( "bfg_grenade_cost", "10", CVAR_SERVERINFO );

        if (!tech)
            tech = FindItemByClassname("item_tech1");

        if( (bfgg && bfgg->value) &&
            (ent->client->pers.inventory[ITEM_INDEX(tech)]) )
        {
            if( ent->client->pers.inventory[ITEM_INDEX(FindItem("cells"))] < (int)bfgc->value )
            {
                /* Normal Fire */
                fire_grenade (ent, start, forward, damage, 600, 2.5, radius);

                gi.cprintf (ent, PRINT_HIGH, "Not enough cells for bfg grenades (need %d).\n", (int)bfgc->value);
                gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/noammo.wav"), 1, ATTN_NORM, 0);
            }
            else
            {
                /* BFG ball instead of explosion */
                fire_bfg_grenade (ent, start, forward, damage, 600, 2.0, radius);
                ent->client->pers.inventory[ITEM_INDEX(FindItem("cells"))] -= (int)bfgc->value;
            }
        }
        else
        {
            /* Normal Fire */
            fire_grenade (ent, start, forward, damage, 600, 2.5, radius);
        }
    }
    
	gi.WriteByte (svc_muzzleflash);
	gi.WriteShort (ent-g_edicts);
	gi.WriteByte (MZ_GRENADE | is_silenced);
	gi.multicast (ent->s.origin, MULTICAST_PVS);

	ent->client->ps.gunframe++;

	PlayerNoise(ent, start, PNOISE_WEAPON);

	if (! ( (int)dmflags->value & DF_INFINITE_AMMO ) )
		ent->client->pers.inventory[ent->client->ammo_index]--;
}

Mark "Grey" Davies