Chapter 2: Medic! - Part II
Requirements
Hello again! OK, if you've gotten this far, you should now have a mod that displays a menu when you start deathmatch, and allows you to choose an MOS. hen you choose and MOS, you should get a sound for feedback. Also, you should get a message and a sound if you try to change MOS's mid game.
In part II, we're going to put that menu and the choice you make to good use, creating a player medic that heals other players by touching them. We'll also explore the secrets of adding new icons and functionality to the player status bar in order to be able to view the amount of healing ability our medic has.
Before we continue, I'd like to take a moment to express my gratitude to two colleagues who made significant contributions to this tutorial. Rohan and Tar. Thanks for your insights, guys...
GreyBear's Quake 2 programming philosophy
My philosophy about making code changes is this:
Make your changes in your own code files whenever possible. That means for our mods, we'll be adding NEW files to the Quake 2 DLL, not merely inserting our mods into the original files. Why? Well, two reasons. One, it's much easier to find your mods if you keep them in your own files, named by you with descriptive names that you can remember. Two, it maintains the integrity of the original code. As programmers we should respect the work of others. Rather than just hack it up, we should add to it gracefully, and clearly mark our additions as our own.
When we can't follow the above, as in modifying global include files, or modifying global structs, we will segregate our mods in the code, and clearly mark them as our additions. I also highly recommend you use a consistent marker, like your initials. That way, you can search the code for your initials and easily find every mod you make in id code. Again, it makes for easy maintenance.
In the body of the tutorial text, code modifications made by us will have a + sign at the end of the line in a comment field to clue you in that the line was added to the original surrounding code.
Housekeeping from Part I
You probably should re-examine Part I quickly if you haven't looked at it recently. I made some minor changes to it in order to make our tasks here in part II a little easier. I also have one more change in part I code that you need to make now, before you continue. This change will make your code more readable, and will help you keep straight on MOS variable values in the code. We're going to add #declare statements to the b_playermos.h code file to allow us to use names instead of numbers to represent MOS's in the game code. So, add the following to the end of the b_playermos.h code:
//+ BD - 1/15 This file forward declares our function prototypes for inclusion into Q2 source
//+ BD - 1/15 Defines to make our code easier to read
#define GRUNT 1 //+ BD
#define MEDIC 2 //+ BD
Save the file. Now, go through the b_playermos.c file, replacing the number in the MOS tests with the appropriate defines (either GRUNT or MEDIC). Now, for the rest of our code, we'll use these declares to make our code easier to understand.
Creating a heal_health variable
Now that we have the ability to choose our Medic MOS, we need a way for him to collect and then distribute healing health points to other players. The way to do this is to create a variable that holds an indicator of how many healing health points the payer possesses. In order to allow our Medic to carry these points across levels like regular health points, we should create our variable within the persistant struct inside the edict->client struct that represents the player. Add the following code to the client_persistant_t struct in g_local.h file:
int max_health;
int heal_health; //+BD - 1/15 - The amount of health points to heal others with
int selected_item;
Save the file. Now we have the variable to hold our healing health points.
Writing code to save heal_health points
Now, let's add a test to determine how our Medic picks up Health items. If a health item is encountered, and he himself is healthy, those health points get added to his heal_health variable. We'll write a new function in the b_playermos.c file, after doing the test in g_items.c. First, the altered code in g_items.c. Add to the Pickup_Health() function like so:
qboolean Pickup_Health (edict_t *ent, edict_t *other)
{
if (!(ent->style & HEALTH_IGNORE_MAX))
{ //+BD - 1/15 Added braces to contain both sub if's
//+ BD - 1/15 If we're in deathmatch AND we're a medic, AND we're healthy...
if( deathmatch->value && other->client->resp.mos == MEDIC && other->health >= 100)
{
Set_Heal_Health(ent,other); //+ BD 1/15 ...then grab the health and add to healing health instead of regular health
return true; //+ BD - 1/15 We took it, so return true...
}
//+BD - 1/15 Original code begins with the if below. End NEW CODE
if (other->health >= other->max_health)
return false;
} //+ BD - 1/15 added brace
other->health += ent->count;
Save the file. Now, open up b_playermos.c. Once again, the actual working code has been placed in our own file to keep it separate. Next, we're going to write the function Set_Heal_Health(). This function will determine what type of health item was picked up, and what it's health value is. It'll also determine the sound to play, and add the health points to our heal_health variable for later use. Here's the code. Add it to the end of the file:
//+BD - 1/15 - If we're a medic, health above 100 goes to healing
//+BD - 1/15 THIS ENTIRE CODE BLOCK IS NEW!
void Set_Heal_Health(edict_t *ent, edict_t *other)
{
if (ent->count == 2) //BD ent->count is the number of health points
ent->item->pickup_sound = "items/s_health.wav"; //BD and the sound associated w/ it.
else if (ent->count == 10)
ent->item->pickup_sound = "items/n_health.wav";
else if (ent->count == 25)
ent->item->pickup_sound = "items/l_health.wav";
else // (ent->count == 100)
ent->item->pickup_sound = "items/m_health.wav";
other->client->pers.heal_health += ent->count; //BD - 1/15 - Up the heal_health by the size of the health pkg
//BD We'll be returning HERE later to add a status bar display for user feedback
if (!(ent->spawnflags & DROPPED_ITEM))
SetRespawn (ent, 30);
}
Finally, we need to add the forward declaration of our new function to the header file, so it's visible to the other code files that may execute it. Add it to playermos.h like so:
//Functions we use
void CheckMOS(edict_t *ent);
void Cmd_MOS_f(edict_t *ent, char *cmd);
void Set_Heal_Health(edict_t *ent, edict_t *other); //+BD 1/15 - Our new player function
Save the file. So far, so good...
Modifying the ClientThink()function
Now we're going to have a little discourse on Quake 2 game logic, and why things work the way they do. In Quake 2, entity collision, or touching, happens in a manner opposite of what you might think would be normal at first. If your player character touches something else, that player takes no special note, other than to catalog the touch in an array. Then, when it's time to move in the game, the code loops through this array and performs the action appropriate for each touch. What's backwards is that the player isn't the one to decide what to do. The other object determines what happens. Upon examination, this is pretty smart. With this method, each object only needs to remember one function for touching occurrences, namely its own.
This makes life a little bit difficult for us, however, for two reasons, one general, and one specific:
if (!other->touch)
{ //+ BD -1/16 Here's where we heal the other player if needed
if(other->client && ent->client->resp.mos == MEDIC) //+BD - 1/16 If the other ent is player, and we're a medic...
{
Player_Heal(ent,other); //+BD - 1/16 Check and heal if conditions met
}
continue;
}
Save the file. Yet again, we're going to segment the actual working code in our own code file, b_playermos.c.
The next step is to create the code that actually contains the healing logic, so let's do a little conceptual thinking. Before we heal the player, several things have to be true. Remember from above tough, we've already tested two conditions to get to our new code execution, namely that we touched another player, and we are a medic. So now, we need to list the remaining conditions our new code needs to test before healing occurs:
//+BD - 1/16 -THIS ENTIRE CODE BLOCK IS NEW!
//+BD 1/16 - Player_Heal() - Check and heal if appropriate
void Player_Heal(edict_t *ent, edict_t *other)
{
//BD local vars
int heal_needed; //How much healing our patient needs
int heal_left; //How much our medic has to give
//+BD First, figure out how far below the max health our patient is
//If we're at max health, or dead (at the other extreme) exit quietly...
if( (heal_needed = other->max_health - other->health) <=0 || ent->health <=0)
return;
//If not, lets' figure out our current supply situation
if( (heal_left = ent->client->pers.heal_health) <=0)
{ //No healing points. Exit quietly...
return;
}
//OK, now we figure out whether we have enough points to completely heal the pt.
if(heal_left >= heal_needed)
{ //Enough to completely heal the patient
other->health += heal_needed; //Add back the points we need to be healed
heal_left -= heal_needed; //...and subtract them from the medic's points
gi.centerprintf(ent,"%s completely healed!\n",other->client->pers.netname);
gi.centerprintf(other,"%s has healed your battle wounds!\n",ent->client->pers.netname);
//Play a sound for the player.
gi.sound(ent, CHAN_VOICE, gi.soundindex("items/m_health.wav"), 1, ATTN_NORM, 0);
}
else
{ //Not enough. Use what we have.
other->health += heal_left; //Take all the points remaining
heal_left = 0;
gi.centerprintf(ent,"%s partially healed.\n",other->client->pers.netname);
gi.centerprintf(other,"%s has treated your battle wounds\nas best as possible.\n",ent->client->pers.netname);
//Play a sound for the player.
gi.sound(ent, CHAN_VOICE, gi.soundindex("items/n_health.wav"), 1, ATTN_NORM, 0);
}
//Now, set the variables for the medic based on what we actually used...
ent->client->pers.heal_health = heal_left;
}
OK, now save the file.
The Explanation: Hey, you guys are getting good by this time, judging from the feedback and questions I get, so I figure you've followed what's come through up to now. However that last bit was all new code, and it deserves explaining.
First, note the functions arguments. We're passing two edict structs, ent and other. Ent is our medic. Other is the player to heal. Next, we declare some local variables. These variables never get seen outside this function. Their sole purpose is to provide us a place to store values we calculate, and make it easier to refer to variables buried in structures. Saves on typing. Next, we perform a check to make sure that the other player isn't on either end of the health spectrum. That is, the other player isn't fully healthy, and he's not dead. If either applies, we return, because we can't help him. Next, we look to see if our medic has any healing points to spare. If not, again we go away. If we get past this point, we roll up our sleeves and get to work. We now need to determine whether we can fully or partially heal the player with the healing points our medic has, and take the appropriate action. We then print a message to both players, and play a sound for feedback purposes. Finally, we set the new value of the medic's heal points to either the remainder left over, or zero.
Next, add a function prototype to the b_playermos.h file. It should look like this:
void Player_Heal(edict_t *ent, edict_t *other); //+ - BD - 1/16 - Declaration of the new function
Save the header file. We're almost home...
Displaying healing health points on the status bar
The last thing we need to do is to give the medic a visual gauge to tell him how many healing points he possesses. This way, he knows when to go hunting for MedKits. This subject could take a tutorial all by itself to explain fully (and might if I get around to it), but for now I'll give you the quick 'n' dirty explanation of how to display stuff on the statusbar.
First, you need to understand that there are three parts to creating the statusbar.
OK, first we need to add a declaration for our heal_health variable in the STAT_* list within q_shared.h. This serves as an array index to allow the statusbar code to get at the actual numeric value of our heal_health points. Here's the code: !-Code block-->
#define STAT_FLASHES 15 // cleared each frame, 1 = health, 2 = armor
#define STAT_HEAL 16 //+ BD 1/20 - Added for healing point display above health
#define MAX_STATS 32
Note that the maximum number of STAT variables is 32... That means we've got room to spare for further additions down the road.
Next, we need to insert a new macro into the code within g_spawn.c. This is tricky to find, since it's not commented at all.... Go to line 590, and scroll down from there. You'll see a list of grouped lines enclosed in quotes. This is actually two macro sets, one for a single player statusbar, and another for a deathmatch statusbar. Look for the code below and make the modifications indicated:
char *dm_statusbar =
"yb -24 "
// health
"xv 0 "
"hnum "
"xv 50 "
"pic 0 "
//+ BD 1/20 Healing Points
"if 16 " //+BD 1/20 The index of our STAT value defined in q_shared.h
" xv 75 " //+BD 1/20 The X value from left of screen for the numeric display
//" yb -48" //+BD 1/20 Y value from bottom of statusbar for numeric display.
" num 3 16" //+BD 1/20 Display a number - 3 digits use the value of STAT index #16
" xv 125 " //+BD 1/20 X value from left screen of icon
//" yb -48" //+BD 1/20 Y value from bottom of statusbar for icon
" pic 0 " //+BD 1/20 index of picture array to show as icon
"endif " //+BD 1/20 Close the if statement
We want to change only the deathmatch statusbar (hence we change below *dm_statusbar). The comments in our added code are pretty straightforward. We're adding a new macro to tell the statusbar code where and how to display our heal_health value, along with a health icon.
Finally, we need to make an addition to the p_hud.c file. This file is where the code lives to draw the statusbar or heads up display (hud, get it?). First, add the declaration for our functions by including the header file:
//+ BD 1/20 Add include for declare statements
#include "b_playermos.h"
Then, insert code to make our heal_health value display on the statusbar:
ent->client->ps.stats[STAT_HEALTH] = ent->health;
//+ BD 1/20 Add Healing points to display. They get displayed directly above
//+ BD the health points if greater than zero...
if(ent->client->resp.mos == MEDIC) //+BD 1/20 If we're a medic...
{ //+BD 1/20
ent->client->ps.stats[STAT_HEAL] = ent->client->pers.heal_health; //+BD 1/20 associate our value with the STAT_HEAL index pointer
} //+BD 1/20
//
// ammo
The result
We're done! Compile and run your new DLL as usual. You now have a fully functional medic player class. With a little elbow grease and a good idea, it shouldn't' be hard to extend this tutorial to cover multiple classes.
Extra Credit: Implement a max_heal variable that caps the number of healing points a medic may accumulate.
Contacting the Author
Questions? Problems? Write me, and I'll try to answer your question or help you with debugging. Send your queries to me here.
Tutorial written by GreyBear