Quake 2 is another classic from id software.While not modded quite as much as it's predecessor or Q3A, it is still a blast to play and the mod's on offer are bundles of fun. Just step into Quake 2 Rocket Arena, armed with the new and almighty rocket and rail gun and you will be gibbing for hours on end.If you dig fast paced action, then load up the good ole Quake 2, fire up a mod or two and have some fun!

Post tutorial Report RSS Adding Reloading to Mods

Paril here! Down at Paril's Projects, we try alot of stuff to put into our mods. Our main focus is realism. Anyways, you may have seen this tutorial before, but this one explains how to add more than one weapon to reload, and the frame explainations.

Posted by on - Advanced Client Side Coding

[page=Introduction/Contents]

Welcome to my fourth tutorial!

Now, this tutorial may seem vaguely familiar to some people. When I found this tutorial, I noticed that there are LOTS of things that he didn't (but should have of) explain(ed)! This includes:

Changing the mag limit
Adding more than one weapon to the Weapon_Generic function
Notes

Now just to note.. This isn't very easy. This tutorial IS cut and paste, but there are things you must do yourself. I am taking this from a Mini-Project I am making, called ReloadQ2, so it will include the normal Quake2 names and such.

Alrighty then, if you're ready, let's get started on this!

[page=Adding the Definitions]

Alright, you're here now.

find:

float       respawn_time;     // can respawn when time > this

Beneath it, above the end of the struct, add

//PP - Capacities and how many rounds left
    int     shotg_max;         
    int     shotg_rds;
    int     sshotg_max;         
    int     sshotg_rds;
    //PP end code

In the same file, find this:

typedef enum 
{
        WEAPON_READY, 
        WEAPON_ACTIVATING,
        WEAPON_DROPPING,
        WEAPON_FIRING

First, add a comma after WEAPON_FIRING.. so, the entire block like that would look like:

typedef enum 
{
        WEAPON_READY, 
        WEAPON_ACTIVATING,
        WEAPON_DROPPING,
        WEAPON_FIRING,
 
        //PP added to animate weapon reload and last round
        WEAPON_END_MAG,
        WEAPON_RELOADING
        //PP end add
} weaponstate_t;

Now, we're heading to p_client, and find PutClientInServer

Now, at the beginning with the definitions, add:

//PP added new declaration to give ammo
        gitem_t *item;

Same code, go lower to find:

gi.linkentity (ent);

Under that, and above the:

client->newweapon = client->pers.weapon;

add:

//PP   set the max clip size, then fill the current mag...
        client->shotg_max = 12;
        client->shotg_rds = client->shotg_max;
        client->sshotg_max = 3;
        client->sshotg_rds = client->sshotg_max;
        //PP end add

Alright, you've finished that. Now, let's get on with the Weapons themself..

[page=Adding New Weapon Animations]

find this in p_weapon above Weapon_Generic

#define FRAME_FIRE_FIRST              (FRAME_ACTIVATE_LAST + 1)
        #define FRAME_IDLE_FIRST              (FRAME_FIRE_LAST + 1)
        #define FRAME_DEACTIVATE_FIRST (FRAME_IDLE_LAST + 1)

and add this right beneath it:

//PP - To use the reload and last round animations
        #define FRAME_RELOAD_FIRST            (FRAME_DEACTIVATE_LAST +1)
        #define FRAME_LASTRD_FIRST   (FRAME_RELOAD_LAST +1)
        //PP end add

Now, the next part is BIG.. so, just replace your entire Weapon_Generic function with this:

//PP Create a local define to make changing clip size easy
        #define SHOTGMAG 7
        #define SSHOTGMAG 3
        void Weapon_Generic (edict_t *ent, int FRAME_ACTIVATE_LAST, int FRAME_FIRE_LAST, 
                                int FRAME_IDLE_LAST, int FRAME_DEACTIVATE_LAST, 
                                /*PP added*/
                                int FRAME_RELOAD_LAST, int FRAME_LASTRD_LAST,
                                /*PP end add*/
                                int *pause_frames, int *fire_frames, void (*fire)(edict_t *ent))
        {
               int            n;
        
               //PP - Added Reloading weapon, done manually via a cmd
               if( ent->client->weaponstate == WEAPON_RELOADING)
               {
                       if(ent->client->ps.gunframe < FRAME_RELOAD_FIRST || ent->client->ps.gunframe > FRAME_RELOAD_LAST)
                               ent->client->ps.gunframe = FRAME_RELOAD_FIRST;
                       else if(ent->client->ps.gunframe < FRAME_RELOAD_LAST)
                       {
                               ent->client->ps.gunframe++;           
                               //PP - Check weapon to find out when to play reload sounds
                               if(stricmp(ent->client->pers.weapon->pickup_name, "Shotgun") == 0)
                               {
                                      if(ent->client->ps.gunframe == 42)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1out.wav"), 1, ATTN_NORM, 0);
									  if(ent->client->ps.gunframe == 56)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1in.wav"), 1, ATTN_NORM, 0);                             
									  if(ent->client->ps.gunframe == 63)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/shotgr1b.wav"), 1, ATTN_NORM, 0);                                                           
							   }
                               else if(stricmp(ent->client->pers.weapon->pickup_name, "Super Shotgun") == 0)
                               {
                                      if(ent->client->ps.gunframe == 42)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1out.wav"), 1, ATTN_NORM, 0);
									  if(ent->client->ps.gunframe == 56)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1in.wav"), 1, ATTN_NORM, 0);                             
									  if(ent->client->ps.gunframe == 63)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/shotgr1b.wav"), 1, ATTN_NORM, 0);                                                           
							   }
                       }
                       else
                       {
                               ent->client->ps.gunframe = FRAME_IDLE_FIRST;
                               ent->client->weaponstate = WEAPON_READY;
                               if(stricmp(ent->client->pers.weapon->pickup_name, "Shotgun") == 0)
                               {
                                      if(ent->client->pers.inventory[ent->client->ammo_index] >=  ent->client->shotg_max)
                                              ent->client->shotg_rds = ent->client->shotg_max;
							   else
                                      ent->client->shotg_rds = ent->client->pers.inventory[ent->client->ammo_index];
							   }
							   else if(stricmp(ent->client->pers.weapon->pickup_name, "Super Shotgun") == 0)
                               {
                                      if(ent->client->pers.inventory[ent->client->ammo_index] >=  ent->client->sshotg_max)
                                              ent->client->sshotg_rds = ent->client->sshotg_max;
                                
							   else
                                      ent->client->sshotg_rds = ent->client->pers.inventory[ent->client->ammo_index];
                               }
                       
                       }
               }
               //PP - Empty or unloaded weapon
               if( ent->client->weaponstate == WEAPON_END_MAG)
               {
                       if(ent->client->ps.gunframe < FRAME_LASTRD_LAST)
                               ent->client->ps.gunframe++;
                       else
                               ent->client->ps.gunframe = FRAME_LASTRD_LAST;
               }
               
 
               if (ent->client->weaponstate == WEAPON_DROPPING)
               {
                       if (ent->client->ps.gunframe == FRAME_DEACTIVATE_LAST)
                       {
                               ChangeWeapon (ent);
                               return;
                       }
 
                       ent->client->ps.gunframe++;
                       return;
               }
 
               if (ent->client->weaponstate == WEAPON_ACTIVATING)
               {
                       if (ent->client->ps.gunframe == FRAME_ACTIVATE_LAST)
                       {
                              ent->client->weaponstate = WEAPON_READY;
                               ent->client->ps.gunframe = FRAME_IDLE_FIRST;
                               return;
                       }
               
                       //PP - Check the current weapon to find out when to play takeout sounds
                       if(stricmp(ent->client->pers.weapon->pickup_name, "Shotgun") == 0)
                       {
                               if(ent->client->ps.gunframe == 2)
                                      gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/shotgr1b.wav"), 1, ATTN_NORM, 0);
                                      ent->client->shotg_max = SHOTGMAG;        //set mag rounds                                             ent->client->Mk23_rds = MK23MAG;   //fill the mag...
                       }
                       ent->client->ps.gunframe++;
                       return;
               }
 
               if ((ent->client->newweapon) && (ent->client->weaponstate != WEAPON_FIRING))
               {
                       ent->client->weaponstate = WEAPON_DROPPING;
                       ent->client->ps.gunframe = FRAME_DEACTIVATE_FIRST;
                       return;
               }
 
               if (ent->client->weaponstate == WEAPON_READY)
               {
                       if (((ent->client->latched_buttons|ent->client->buttons) & BUTTON_ATTACK))
                       {
                               ent->client->latched_buttons &= ~BUTTON_ATTACK;
                               if ((!ent->client->ammo_index) ||  ( ent->client->pers.inventory[ent->client->ammo_index] >= ent->client->pers.weapon->quantity))
                               {
                                      ent->client->ps.gunframe = FRAME_FIRE_FIRST;
                                      ent->client->weaponstate = WEAPON_FIRING;
 
                                      // start the animation
                                      ent->client->anim_priority = ANIM_ATTACK;
                                      if (ent->client->ps.pmove.pm_flags & PMF_DUCKED)
                                      {
                                              ent->s.frame = FRAME_crattak1-1;
                                              ent->client->anim_end = FRAME_crattak9;
                                      }
                                      else
                                      {
                                              ent->s.frame = FRAME_attack1-1;
                                              ent->client->anim_end = FRAME_attack8;
                                      }
                               }
                               else
                               {
                                      if (level.time >= ent->pain_debounce_time)
                                      {
                                              gi.sound(ent, CHAN_VOICE, gi.soundindex("weapons/noammo.wav"), 1, ATTN_NORM, 0);
                                              ent->pain_debounce_time = level.time + 1;
                                      }
                                      //+BD - Disabled for manual weapon change
                                      //NoAmmoWeaponChange (ent);
                               }
                       }
                       else
                       {
                               if (ent->client->ps.gunframe == FRAME_IDLE_LAST)
                               {
                                      ent->client->ps.gunframe = FRAME_IDLE_FIRST;
                                      return;
                               }
 
                               if (pause_frames)
                               {
                                      for (n = 0; pause_frames[n]; n++)
                                      {
                                              if (ent->client->ps.gunframe == pause_frames[n])
                                              {
                                                     if (rand()&15)
                                                     return;
                                              }
                                      }
                               }
 
                               ent->client->ps.gunframe++;
                               return;
                       }
               }
 
               if (ent->client->weaponstate == WEAPON_FIRING)
               {
                       for (n = 0; fire_frames[n]; n++)
                       {
                               if (ent->client->ps.gunframe == fire_frames[n])
                               {
                                      if (ent->client->quad_framenum > level.framenum)                                                      gi.sound(ent, CHAN_ITEM, gi.soundindex("items/damage3.wav"), 1, ATTN_NORM, 0);
 
                                      fire (ent);
                                      break;
                               }
                       }
 
                       if (!fire_frames[n])
                               ent->client->ps.gunframe++;
 
                       if (ent->client->ps.gunframe == FRAME_IDLE_FIRST+1)
                               ent->client->weaponstate = WEAPON_READY;
               }
}

Now that was big! That includes the code for both the Super Shotgun and Shotgun.

Now, in the weapons which you need reloading (in this case, Shotgun and Super Shotgun), look for this:

// get start / end positions
               VectorAdd (ent->client->v_angle, ent->client->kick_angles, angles);
               AngleVectors (angles, forward, right, NULL);
               VectorSet(offset, 0, 8, ent->viewheight-8);
               P_ProjectSource (ent->client, ent->s.origin, offset, forward, right, start);

or something around that, and this:

//Display the yellow muzzleflash light effect
               gi.WriteByte (svc_muzzleflash);
               gi.WriteShort (ent-g_edicts);
               //If not silenced, play a shot sound for everyone else
               gi.WriteByte (MZ_SHOTGUN | is_silenced);
               gi.multicast (ent->s.origin, MULTICAST_PVS);

First, comment your firing function. Now, between both those, put this in:

//PP - Added to animate last round firing...
 
               if (ent->client->pers.inventory[ent->client->ammo_index] == 1 || (ent->client->shotg_rds == 1))
               {
                       //Hard coded for reload only.
                       ent->client->ps.gunframe=64;
                       ent->client->weaponstate = WEAPON_END_MAG;
                       fire_shotgun (ent, start, forward, damage, kick, 500, 500, DEFAULT_SHOTGUN_COUNT, MOD_SHOTGUN);
                       ent->client->shotg_rds--;
               }
               else
               {
                       //If no reload, fire normally.
                       fire_shotgun (ent, start, forward, damage, kick, 500, 500, DEFAULT_SHOTGUN_COUNT, MOD_SHOTGUN);
 
                       ent->client->shotg_rds--;
               }

[page=Command]
Alright! You're 70/100 way there!

Now, head over to g_cmds, and above say.. Cmd_God_f, add this:

//PP ENTIRE CODE BLOCK NEW
        // Cmd_Reload_f()
        // Handles weapon reload requests
        void Cmd_Reload_f (edict_t *ent)
        {
               int rds_left;           //PP - Variable to handle rounds left
 
                //PP - If the player is dead, don't bother
                if(ent->deadflag == DEAD_DEAD)
               {
                       gi.centerprintf(ent, "I know you're not smart,\nBUT YOU'RE DEAD!!\n");
                       return;
                }
 
               //First, grab the current magazine max count...
               if(stricmp(ent->client->pers.weapon->pickup_name, "Shotgun") == 0)
                       rds_left = ent->client->shotg_max;
               else    //We should never get here, but...
                        //BD 5/26 - Actually we get here quite often right now. Just exit for weaps that we
                       //          don't want reloaded or that never reload (grenades)
                {
                       gi.centerprintf(ent,"Where'd you train?\nYou can't reload that!\n");
                       return;
               }
 
               if(ent->client->pers.inventory[ent->client->ammo_index])
               {       
                       if((ent->client->weaponstate != WEAPON_END_MAG) && (ent->client->pers.inventory[ent->client->ammo_index] < rds_left))
                       {
                                      gi.centerprintf(ent,"Buy a clue-\nYou're on your last magazine!\n");
                       }
                       else
                               //Set the weaponstate...
                               ent->client->weaponstate = WEAPON_RELOADING;
                }
               else
                       gi.centerprintf(ent,"Pull your head out-\nYou've got NO AMMO!\n");
        }
        //PP END CODE BLOCK

Now, find:

else if (Q_stricmp (cmd, "invdrop") == 0)
               Cmd_InvDrop_f (ent);

add beneath:

//PP - for handling reload commands
        else if (Q_stricmp (cmd, "reload") == 0)
                Cmd_Reload_f (ent);

You should be good there.

[page=Frames]
This couldn't be simpler, even easier if you have models.

void Weapon_Shotgun (edict_t *ent)
{
	static int	pause_frames[]	= {22, 28, 34, 0};
	static int	fire_frames[]	= {8, 9, 0};

	Weapon_Generic (ent, 7, 18, 36, 39, 68, 69, pause_frames, fire_frames, weapon_shotgun_fire);
}

You're adding two new numbers. One after the fourth for handling the last frame for your reload, and one after that for handling the Last Round animation. I'll explain those after the New Weapons.

[page=Adding New Weapons to the List]

This isn't too hard, but can be a bit freaky.

First, you must make definitions just the same with the other weapons. And then, look at what I did in Weapon_Generic with the names. ALWAYS, and I mean ALWAYS use Else If when adding a new weapon through Weapon_Generic, just like I did for the Super Shotgun.

Like so:

if(stricmp(ent->client->pers.weapon->pickup_name, "Shotgun") == 0)
                               {
                                      if(ent->client->ps.gunframe == 42)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1out.wav"), 1, ATTN_NORM, 0);
									  if(ent->client->ps.gunframe == 56)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1in.wav"), 1, ATTN_NORM, 0);                             
									  if(ent->client->ps.gunframe == 63)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/shotgr1b.wav"), 1, ATTN_NORM, 0);                                                           
							   }
                               else if(stricmp(ent->client->pers.weapon->pickup_name, "Super Shotgun") == 0)
                               {
                                      if(ent->client->ps.gunframe == 42)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1out.wav"), 1, ATTN_NORM, 0);
									  if(ent->client->ps.gunframe == 56)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/m4a1in.wav"), 1, ATTN_NORM, 0);                             
									  if(ent->client->ps.gunframe == 63)
                                              gi.sound(ent, CHAN_WEAPON, gi.soundindex("weapons/shotgr1b.wav"), 1, ATTN_NORM, 0);                                                           
							   }

It's always an Else If (but change the sounds :))

[page=Frames Explaination]

Alright, here's another thing.

Ok, here's how your model is set up (or should be):

activate01 - activatexx (Activation)
fire01 - firexx (Fire)
idle01 - idlexx (Idle frames/ready frames)
deactivate01 - deactivatexx (Putaway)
reload01 - reloadxx (Reloading)
lastrnd01 - lastrndxx (Last round)

The last round is a bit hard to understand. You should take the frames from fire, and copy + paste them into the Last Frame, but for weapons like the Mk23, you might want a signal to know that the weapon now needs reloading! Such as in Action Quake, the slider goes back, indicating you have no ammo.

Reload frames is obvious.

[page=Extra]

Now, alot of you may need help on this one. May be a problem, errors, anything.. Contact me please. This is a new tutorial, so the email isn't worn out, don't worry ;)

jonno.5000@gmail.com
OR
check at Paril101.planetquake.gamespy.com for a sample code (if available)

Thank you for listening and reading. Hopefully, you'll have some good realism in mods.
If you use this tutorial, no need to thank me. I am just someone who wishes to help at no cost to ANYONE.

Thank you.
Paril

Post a comment

Your comment will be anonymous unless you join the community. Or sign in with your social account: