Celebrating 12 years of Quake!



Untitled Document



Back
to Booth Main



Tutorial: Adding a new weapon to Quake



Okay, today we shall cover what you need to do by the book to add
a new weapon to quake. Note that there are several ways of doing so, but I will only
cover the supposed "proper" way.



This tutorial shall be broken up into two sections: Item pickup and
Actual weapon code.



Aside from explaining a few things, we are going to make a railgun
weapon, using the already

existant IT_EXTRA_WEAPON Bitflag.



Make sure you download the sprite you will need for the rail trail
effect here and place it

into your moddir/progs directory.



SECTION ONE: Item Pickup



Open up the DEFS.QC File and go down to line #293. Note the following:



float IT_EXTRA_WEAPON = 128;



This gentlemen, is a bitflag declaration. When used in conjuntion
with a entity variable such as .items, you can store a whole bunch of flags
that indicate what you have in your inventory.



Keep in mind that a bitflag has to be declared in multiples of two.
i.e. 1, 2, 4, 8, etc.



Also, keep in mind that .items is currently already at capacity, so
you won't be adding any more weapons bitflags to that variable.



Any more weapons and you'll need to declare a .items2 or .items3,
in which event you can stuff the entire stack of IT_ bitflags into said
variable. (Makes for quite a stack of new weapons.)



Also, if you do so, make sure you change the IT_ prefix to IT2_ or
IT_3, so you won't be having any confusion with your bitflags. They can
hold identical values as the IT_ bitflags just make sure you rename them
with the appropriate prefix.



Note that bitflags are stored, removed and accessed by way of the
following example:



To add:

self.items = self.items | IT_AXE;



Note that you can add multiple items by doing the following:

self.items = self.items | IT_AXE | IT_SHOTGUN;



Just keep in mind the | is what helps make the magic happen.



To remove:

self.items = self.items - (self.items & IT_AXE);



To remove multiple items:

self.items = self.items - (self.items & (IT_AXE
| IT_SHOTGUN));



Finally, to find out if a bitflag is present, do something like this:



if(self.items & IT_AXE)







daemon pointed out to me that you can safely add a bitflag that's already been added. I dunno if you can safely subtract though, so make sure you check before removing anything okay? ;)




You can close DEFS.QC, open ITEMS.QC and go down to weapon_touch().



You can now choose one of two different routes: If you intend to make
a map to place your new

weapon, you should do something like this:



else if (self.classname == "weapon_lightning")

{

if (leave && (other.items & IT_LIGHTNING) )

return;

hadammo = other.ammo_rockets;

new = IT_LIGHTNING;

other.ammo_cells = other.ammo_cells + 15;

}

else if(self.classname == "weapon_railgun")

{

if (leave && (other.items & IT_EXTRA_WEAPON) )

return;

hadammo = other.ammo_rockets;

new = IT_EXTRA_WEAPON;

other.ammo_cells = other.ammo_cells + 15;

}

else

objerror ("weapon_touch: unknown classname");



Then later add something like this for the actual map entity to place:



/*QUAKED weapon_railgun (0 .5 .8) (-16 -16 0)
(16 16 32)

*/



void() weapon_railgun =

{

precache_model ("progs/g_rail.mdl");

setmodel (self, "progs/g_rail.mdl");

self.weapon = IT_EXTRA_WEAPON;

self.netname = "Railgun";

self.touch = weapon_touch;

setsize (self, '-16 -16 0', '16 16 56');

StartItem ();

};



The other route is if you don't intend to make a custom map to place
a custom pickup entity.

In this event, you should do this instead in weapon_touch():



else if (self.classname == "weapon_lightning")

{

if (leave && (other.items & IT_LIGHTNING) )

return;

hadammo = other.ammo_rockets;

new = IT_LIGHTNING | IT_EXTRA_WEAPON;

other.ammo_cells = other.ammo_cells + 15;

}





And later in weapon_lightning():



/*QUAKED weapon_lightning (0 .5 .8) (-16 -16
0) (16 16 32)

*/



void() weapon_lightning =

{

precache_model ("progs/g_light.mdl");

setmodel (self, "progs/g_light.mdl");

self.weapon = 3;

self.netname = "Thunderbolt and Railgun";

self.touch = weapon_touch;

setsize (self, '-16 -16 0', '16 16 56');

StartItem ();

};



Note with route 1 you will also need a custom model set for this.
Since I don't have time right now to come up with a fancypants new v_ and
g_ models for this railgun, I shall instead assume that you're following
the second route.



Now, that you've done the route 2 modifications, save and close your
ITEMS.QC and open up your CLIENT.QC file.



SECTION TWO - Actual Weapons Code



Okay, now we are moving on to the actual weapons code. But first,
I want you to go down to ClientObituary() in CLIENT.QC.



Add this right after the if(rnum == IT_LIGHTNING) code block:



if (rnum == IT_EXTRA_WEAPON)

{

deathstring = " was railed by ";

deathstring2 = "\n";

if (targ.health < -40)

{

deathstring = " splattered by ";

deathstring2 = "'s railgun\n" ;

}

}



What we have just done is ensure that quake knows what your weapon
is in deathmatch.



Save and close, open DEFS.QC, and at the bottom put this:



.float aninexttim;

.float frmend;

.vector oriorg;

.vector oldvel;



The first two entity variables are for controlling the effect sprite's
animation.



The last two are entity vector variables for determining where the
effect sprites are supposed

to go.



Save and close, then open up WEAPONS.QC. Place this at the end of
W_Precache:



precache_model("progs/s_puls.spr");



All sprites and sounds must be precached before use. After you've
precached them you don't

need to do so again.



Now after W_FireBullets but before W_FireShotgun put:



void() BoomThink =

{

self.frame = self.frame + 1;

self.think = BoomThink;

self.nextthink = time + self.aninexttim;



if(self.frame >= self.frmend)

{

self.frame = self.frmend;

self.think = SUB_Remove;

self.nextthink = time + self.aninexttim;

}

};



// SpawnBoom - Spawns an explosion sprite at
org.

void(vector org, vector dir, string emdl, float beginf, float endfrm,float
anitm, float skn, vector vel) SpawnBoom =

{

newmis = spawn();

newmis.solid = SOLID_NOT;

newmis.movetype = MOVETYPE_FLY; // DRS: todo - change so it can move

newmis.owner = world;

newmis.angles = vectoangles(dir);

newmis.velocity = vel;



setmodel(newmis, emdl);

setsize(newmis, '0 0 0', '0 0 0');

setorigin(newmis, org);

newmis.skin = skn;

newmis.frame = beginf;

newmis.aninexttim = anitm;

newmis.frmend = endfrm;



newmis.think = BoomThink;

newmis.nextthink = time + anitm;

};



void() TracerThink =

{

local vector org;

local float dis;



dis = vlen(self.oriorg-self.oldorigin)/5;



org = self.origin + self.oldvel*dis;

setorigin(self, org);



SpawnBoom(self.origin, self.oldvel, "progs/s_puls.spr",
8, 14, 0.06, 0, '0 0 0');

// Fixes improperly aligned sprite beamrings.

newmis.angles_x = newmis.angles_x - newmis.angles_x*2;

SpawnBoom(self.origin, (self.oldvel-self.oldvel*2), "progs/s_puls.spr",
8, 14, 0.06, 0, '0 0 0');

// Fixes improperly aligned sprite beamrings.

newmis.angles_x = newmis.angles_x - newmis.angles_x*2;



if(self.cnt == 5)

{

self.think = SUB_Remove;

self.nextthink = time + 0.1;

}

else

{

self.think = TracerThink;

self.nextthink = time + 0.1;

self.cnt = self.cnt + 1;

}

};



void(float damage) W_FireRail =

{

local entity tempen;

local vector orgen, orgo, dir, tdir, dorg;

local float donerail;



makevectors(self.v_angle);



sound (self, CHAN_WEAPON, "weapons/sgun1.wav", 1, ATTN_NORM);

self.ammo_cells = self.ammo_cells - 5;

self.currentammo = floor(self.ammo_cells / 5);



orgo = self.origin + self.view_ofs + v_forward*8;

dir = v_forward;

traceline (orgo, orgo + v_forward*2048, FALSE, self);



while(!donerail)

{

if(trace_ent.takedamage)

{

orgen = trace_endpos + dir*4;

dorg = trace_endpos - dir*4;

SpawnBlood (dorg, '0 0 0', damage);

T_Damage (trace_ent, self, self, damage);



if(trace_ent.movetype != MOVETYPE_PUSH
&& trace_ent.classname != "monster_oldone" &&
trace_ent.classname != "monster_boss")

{

tdir = trace_endpos - orgo;

tdir = normalize(tdir);

trace_ent.flags = trace_ent.flags - (trace_ent.flags & FL_ONGROUND);

trace_ent.velocity = trace_ent.velocity + tdir*(170+damage);

if(trace_ent.velocity_z < 270)

trace_ent.velocity_z = trace_ent.velocity_z + 270;

}

}

else

{

dorg = trace_endpos - dir*4;



donerail = TRUE;

}

tempen = self;

self = trace_ent;

traceline(orgen, orgen + dir*2048, FALSE, self);

self = tempen;

}

newmis = spawn();

newmis.solid = SOLID_NOT;

setsize(newmis, '-1 -1 -1', '1 1 1');

setorigin(newmis, orgo);

newmis.classname = "railtrail";

newmis.nextthink = time + 0.1;

newmis.think = TracerThink;

newmis.oldorigin = dorg;

newmis.oriorg = orgo;

newmis.oldvel = dir;

newmis.owner = self;

};





Okay, SpawnBoom and BoomThink are the stuff that will spawn and animate
the railgun beam rings.



TracerThink is the function that makes the beam rings appear at the
proper distances.



Finally W_FireRail is the function that fires the railgun. I'm not
going to get into a

breakdown of what is going on inside these functions because it's not the
focus of the tutorial.



Now go down to W_SetCurrentAmmo and add right between the else if
(self.weapon == IT_LIGHTNING) and else code blocks so that it looks like
this:



else if (self.weapon == IT_LIGHTNING)

{

self.currentammo = self.ammo_cells;

self.weaponmodel = "progs/v_light.mdl";

self.weaponframe = 0;

self.items = self.items | IT_CELLS;

}

else if (self.weapon == IT_EXTRA_WEAPON)

{

self.currentammo = floor(self.ammo_cells / 5);

self.weaponmodel = "progs/v_rock.mdl";

self.weaponframe = 0;

self.items = self.items | IT_CELLS;

}

else

{

self.currentammo = 0;

self.weaponmodel = "";

self.weaponframe = 0;

}



We have just added support for our new weapon in W_SetCurrentAmmo,
which basically sets our

current ammo type, view model, etc. We are going to skip W_BestWeapon because
the railgun

is an ammo hog.



Go down to W_Attack and add right under the else if (self.weapon ==
IT_LIGHTNING) code block so that it looks like this:



else if (self.weapon == IT_LIGHTNING)

{

player_light1();

self.attack_finished = time + 0.1;

sound (self, CHAN_AUTO, "weapons/lstart.wav", 1, ATTN_NORM);

}

else if (self.weapon == IT_EXTRA_WEAPON)

{

player_rocket1();

W_FireRail(80);

self.attack_finished = time + 0.8;

}



Note that if you want your weapon to have different firing frames
you'll need to define them

in PLAYER.QC. I won't get into that now, as again it isn't the focus of
this tutorial.



Yes I am lazy. =P



Now go down to W_ChangeWeapon().



There are two routes to go here. If you want to make it so that your
weapon can be selected by pressing a given key twice, you should do this:



else if (self.impulse == 8)

{

if(self.weapon == IT_LIGHTNING)

{

fl = IT_EXTRA_WEAPON;

if (self.ammo_cells < 5)

am = 1;

}

else

{

fl = IT_LIGHTNING;

if (self.ammo_cells < 1)

am = 1;

}

}



Otherwise, if you want to select your weapon via a separate impulse
do this:



else if (self.impulse == 13)

{

fl = IT_EXTRA_WEAPON;

if (self.ammo_cells < 5)

am = 1;

}



Next, go down to CheatCommand() and change the second self.items so
that it looks like this:



self.items = self.items | IT_LIGHTNING | IT_EXTRA_WEAPON;



Continue on to CycleWeaponCommand() and change the if else blocks
so that it looks like this:



if (self.weapon == IT_EXTRA_WEAPON)

{

self.weapon = IT_AXE;

}

else if (self.weapon == IT_LIGHTNING)

{

self.weapon = IT_EXTRA_WEAPON;

if (self.ammo_cells < 5)

am = 1;

}

else if (self.weapon == IT_AXE)

{

self.weapon = IT_SHOTGUN;

if (self.ammo_shells < 1)

am = 1;

}

else if (self.weapon == IT_SHOTGUN)

{

self.weapon = IT_SUPER_SHOTGUN;

if (self.ammo_shells < 2)

am = 1;

}

else if (self.weapon == IT_SUPER_SHOTGUN)

{

self.weapon = IT_NAILGUN;

if (self.ammo_nails < 1)

am = 1;

}

else if (self.weapon == IT_NAILGUN)

{

self.weapon = IT_SUPER_NAILGUN;

if (self.ammo_nails < 2)

am = 1;

}

else if (self.weapon == IT_SUPER_NAILGUN)

{

self.weapon = IT_GRENADE_LAUNCHER;

if (self.ammo_rockets < 1)

am = 1;

}

else if (self.weapon == IT_GRENADE_LAUNCHER)

{

self.weapon = IT_ROCKET_LAUNCHER;

if (self.ammo_rockets < 1)

am = 1;

}

else if (self.weapon == IT_ROCKET_LAUNCHER)

{

self.weapon = IT_LIGHTNING;

if (self.ammo_cells < 1)

am = 1;

}



Go on to CycleWeaponReverseCommand() and change the if else blocks
so they look like this:



if (self.weapon == IT_EXTRA_WEAPON)

{

self.weapon = IT_LIGHTNING;

if (self.ammo_cells < 1)

am = 1;

}

else if (self.weapon == IT_LIGHTNING)

{

self.weapon = IT_ROCKET_LAUNCHER;

if (self.ammo_rockets < 1)

am = 1;

}

else if (self.weapon == IT_ROCKET_LAUNCHER)

{

self.weapon = IT_GRENADE_LAUNCHER;

if (self.ammo_rockets < 1)

am = 1;

}

else if (self.weapon == IT_GRENADE_LAUNCHER)

{

self.weapon = IT_SUPER_NAILGUN;

if (self.ammo_nails < 2)

am = 1;

}

else if (self.weapon == IT_SUPER_NAILGUN)

{

self.weapon = IT_NAILGUN;

if (self.ammo_nails < 1)

am = 1;

}

else if (self.weapon == IT_NAILGUN)

{

self.weapon = IT_SUPER_SHOTGUN;

if (self.ammo_shells < 2)

am = 1;

}

else if (self.weapon == IT_SUPER_SHOTGUN)

{

self.weapon = IT_SHOTGUN;

if (self.ammo_shells < 1)

am = 1;

}

else if (self.weapon == IT_SHOTGUN)

{

self.weapon = IT_AXE;

}

else if (self.weapon == IT_AXE)

{

self.weapon = IT_EXTRA_WEAPON;

if (self.ammo_cells < 5)

am = 1;

}



If you opted for the separate impulse selection method, go down to
ImpulseCommands() and change

this:



if (self.impulse >= 1 && self.impulse
<= 8)

W_ChangeWeapon ();



To this:



if (self.impulse >= 1 && self.impulse
<= 8 || self.impulse == 13)

W_ChangeWeapon ();



So completes this section. Save and compile, don't forget to bind
a key if you went separate impulse route, and enjoy your railgun.



Next tutorial should one manifest will cover overhauling the weapons
handling system to be more multi new weapon friendly.