Coding a new class (part 1)
Introduction
The examples in this code is based on the dystopian codebase, but
it shouldn't matter much, and what is written here, should hold for
most godwars muds out there. This first part of the class howto
will descripe the basic creation of a new class.
Let us get started
First you need to plan your class, no work is done well if you just start from one
end and work at it till your done. You need to know what the class should do, what
theme it should be based on, and a rough estimate of the powers and skills the class
need, or you'll end up in a world of pain.
When you have the basic idea of your new class, you need to start coding it, I would recommend making two files, a source file and a header file for the class. I've come to the conclussion that this can be explained better by making an example; in the following we will try to code a class called titans.
First we need the files titan.c and titan.h (the source and header files), just create the two new files, and leave them empty for now. Then add the titan file to your Makefile, just add it to the long list of filenames in the Makefile (remember to call it titan.o).
Then you need to make sure your class headerfile is included in your main headerfile, that's merc.h for us. Open it, and somewhere in the beginning it includes alot of headerfiles, ie. player.h, garou.h and some other header files. Just add titan.h to the list, so we can use the things we define in the headerfile.
You need to define alot of things in the .c file, just copy/paste the defines from one of the old class files, it should look something like :
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "merc.h"
Just do it, you probably need the functions that are declared in these headerfiles, and you need to include merc.h since it's your main headerfile, where all the functions for your mud are defined.
Now define your class in the headerfile where you define classes (player.h for dystopia), you will notice that the classes are defined using a bitvector, if you don't know what a bitvector is, I'll try and explain it here. By using powers of two, when defining values, we can add them up, and always be sure whether a certain value is set. ie. should we have defines CLASS_ANGEL as 4 and CLASS_DEMON as 2, then by setting the players class to 6, he is both an angel and a demon, if we had used values that wasn't powers of two, we would have had some problems, ie. should CLASS_ANGEL be 5 and CLASS_DEMON be 4, then by setting the players class to 5 (we only want him to be an angel), we would accidently also give him the class DEMON, since 5 is actually 1+4; the player would also have the class that was defined as 1. This method of storing values is called a bitvector, and we will use alot of them later on. Now let's define CLASS_TITAN so we can use it in the code.
My last CLASS_* define in my player.h file was
#define CLASS_NINJA 128
so I'll double that and add CLASS_TITAN
#define CLASS_TITAN 256
Let's make it possibly to get the class, open the file clan.c and goto the function do_class(), make an addition for the new class.
else if (!str_cmp(arg2,"titan"))
{
victim->class = CLASS_TITAN;
send_to_char("You are now a Titan.\n\r",victim);
}
Let's give the class some glamour, add it to do_who() and when people look in the room, they should also see the class of the player. Open act_info.c, and goto the function do_who(). If you look it through, you will find a place where we define how the class looks on who, ie. for drows it would look something like :
/* first we define how the "thingies" around the class name should look,
* this is how drows look, we should add another for titans.
*/
else if ( IS_CLASS(wch, CLASS_DROW))
{strcpy( openb, "#P.o0"); strcpy(closeb, "#P0o.#n" );}
/* we add this */
else if ( IS_CLASS(wch, CLASS_TITAN))
{strcpy( openb, "(X)"); strcpy(closeb, "(X)" );}
as you can see, we want the symbols around the class name to look like this (X), openb is the thing before the class name, and closeb the one after, in case you want to make them different. (I didn't add any colors for the symbols, the #P is the color pink in the dystopia code, use whatever colorsymbols you use on your code).
A short while down in do_who() we encounter the definition of the class name, here we should add the name Titan for CLASS_TITAN (in dystopia the names differ from the players generation, heres how it looks).
else if ( IS_CLASS(wch, CLASS_DROW))
{
if (wch->generation == 1)
sprintf(kav,"%s#0Matron Mother#n%s ", openb, closeb);
else if (wch->generation == 2)
sprintf(kav,"%s#0Weaponmaster#n%s ", openb, closeb);
else if (wch->generation == 3)
sprintf(kav,"%s#0High Priestess#n%s ", openb, closeb);
else if (wch->generation == 4)
sprintf(kav,"%s#0Favored of Lloth#n%s ", openb, closeb);
else if (wch->generation == 5)
sprintf(kav,"%s#0Black Widow#n%s ", openb, closeb);
else if (wch->generation == 6)
sprintf(kav,"%s#0Drow#n%s ", openb, closeb);
else if (wch->generation == 7)
sprintf(kav,"%s#0Drow Male#n%s ", openb, closeb);
else
sprintf(kav,"%s#0Drider#n%s ", openb, closeb);
}
We want to add something similar for Titans, but being a little lazy, we let them all have the name Titan (I'm not going to sit here and think up 8 different Titan generation names).
else if ( IS_CLASS(wch, CLASS_TITAN))
{
if (wch->generation == 1)
sprintf(kav,"%sTitan leader%s ", openb, closeb);
else
sprintf(kav,"%sTitan%s ", openb, closeb);
}
What we do is simply putting together the symbols around the class name (openb and closeb) and put the class name in the middle (again, no colors, use your own). The one thing that can go wrong here is alignment. We don't want who to get messed up, with class names that doesn't align correct, a bad example would be something like this :
[Lord ] (0.85) ()Ogre() Jobo [Colonel ] (0.60) (X)Titan(X) Vladd [Colonel ] (0.60) [*]Shapeshifter[*] Dracknuur
As you can see, the names of the players are not correctly aligned, because we did something bad when defining the class names, you should always make sure to have the correct amount of spaces after each name, you can either count or simply try till you get it right.
Now let's fix it so other players can see that Titans are Titans when they look in the room. Still in act_info.c we go to show_char_to_char_0() and add two lines of code for the Titan class. There should be some code for the old classes already, just go there and add these lines.
if (!IS_NPC(victim) && !IS_NPC(ch) && IS_CLASS(victim, CLASS_TITAN)
&& IS_HERO(victim) && IS_HERO(ch))
strcat( buf, "(X)Titan(X) ");
Now we can see Titans on who, and we know a player is a Titan by looking in the room where he is, we are know ready to get some coding done.
The class needs to have some sort of system to gain powers and skills, like demons have inparts, warps and disciplines the new class should have an easy system to gain the powers of that class, we make this quick little command for our Titan class called learn, which simply allows the Titan to learn new powers at the cost of primal. Read the code, I will try and explain everything here.
void do_learn(CHAR_DATA *ch, char *argument)
{
/*
* In the beginning of our function we must define any variables we
* need, you may NOT write any code before the variables are
* defined, this is how C works, so let's keep it this way.
*
* we create a buffer of size MAX_STRING_LENGTH to hold temporary
* text and a buffer of size MAX_INPUT_LENGTH to hold the argument
* given by the player.
*/
char buf[MAX_STRING_LENGTH];
char arg[MAX_INPUT_LENGTH];
/*
* Now let's get the argument (whatever the player typed after
* 'learn' so we can figure out if the player can learn this. We
* will use the command one_argument() to make sure we have a single
* word, the rest of the argument is put back into argument, should
* we need to examine it later. We will store the one-word argument
* in the variable arg.
*/
argument = one_argument(argument, arg);
/*
* First we make sure that the person using the command isn't a mob,
* letting monsters use commands is never wise, since we may
* accidently crash the mud (monsters don't have the same structure
* as players).
*/
if (IS_NPC(ch)) return;
/*
* Next let's make sure that the person is a Titan, it wouldn't be
* wise to let someone who isn't a titan use the command. Notice the
* \n\r in the message we send to the player, this is a linebreak,
* if we don't add this each time we send a message to a player, the
* player will get his next message on the same line, making it look
* both silly and ugly.
*/
if (!IS_CLASS(ch, CLASS_TITAN))
{
send_to_char("Huh?\n\r",ch);
return;
}
/*
* If a player doesn't give an argument, we should tell him give one.
*/
if (arg[0] == '\0')
{
send_to_char("Learn what [combat or knowledge]\n\r",ch);
return;
}
/*
* Lets see if the player can learn what it is he tries to learn. We
* will compare the argument 'arg' to a list of possibly things a
* Titan can learn, and should the argument not be on the list, we
* will tell the player to try something else. To compare strings we
* use the command str_cmp, you will notice the lack of logic in the
* way str_cmp is used. We use str_cmp as if it returned a bool (a
* truth value TRUE, FALSE), but actually str_cmp returns an integer
* which is the total difference between the two strings it
* compares. So if we where to compare combat with combul it would
* return 2, since theres two letters that don't match. Should the two
* strings match, str_cmp returns the value 0, which is the same value
* as FALSE, that's why we use the ! infront of the comparison. Should
* they actually match, we would get a FALSE, and the ! inverts it
* (making FALSE TRUE, and the other way around. (it would be more
* correct to use if (str_cmp(arg, "combat") == 0), but this seems
* to be the way most people code...)
*/
if (!str_cmp(arg, "combat"))
{
/*
* First we see if the player has already maxed his skill, we
* don't want him to get more than he should. But first we need to
* actually define where to store how "skilled" the player is. Open
* titan.h (our headerfile for Titans) and add the following two
* lines :
#define TITAN_COMBAT 1
#define TITAN_KNOWLEDGE 2
* We want to use the ch->pcdata->powers[] structure to save our
* players Titan data in. we have about some 40'ish spots in the
* array, so don't define anything beyond 40. You can get the
* precise amount by looking at the definition in merc.h; it is not
* wise to change this number if you don't know what you are doing,
* bad things can happen if you forget to update the rest of the
* code to this change (how a player is saved for instance). Okay,
* back to seeing if the player is already maxed, we don't want a
* titan to have a higher combat rating than 5, so let's do this.
*/
if (ch->pcdata->powers[TITAN_COMBAT] > 4)
{
send_to_char("You already have all 5 levels.\n\r",ch);
return;
}
/*
* So the player _can_ learn more, then let's see if the player
* can pay the price of 1 million exp per. level, (we use
* 'ch->pcdata->powers[TITAN_COMBAT]+1' because we don't want the
* first one to be free (the player starts at level 0, so gaining
* the first level should cost 1 million exp, thus the +1.
*/
if (ch->exp < (ch->pcdata->powers[TITAN_COMBAT]+1)*1000000)
{
send_to_char("You don't have enought exp.\n\r",ch);
return;
}
/*
* So the player had the exp and could train the level, then
* let us take the exp and up the TITAN_COMBAT. Here goes.
*/
ch->pcdata->powers[TITAN_COMBAT]++;
ch->exp -= ch->pcdata->powers[TITAN_COMBAT]*1000000;
send_to_char("Ok.\n\r",ch);
}
/*
* So it wasn't combat, we continue comparring till we have a match,
* or we run out of things to compare with. We use 'else if' instead
* of 'if' not because it's needed, but because it's a cleaner way of
* coding, and it's a tiny bit faster, since we don't need to compare,
* should the previous compare match.
*/
else if (!str_cmp(arg, "knowledge"))
{
// Look at how the code for combat was made,
// and try and fill out this one, it shouldn't be hard.
}
/*
* If none of the above match, we should tell the player that he
* cannot learn this or that ability, and return. Again notice
* the \n\r.
*/
else
{
send_to_char("Sorry, you cannot learn this.\n\r",ch);
return;
}
/*
* And finally, at the end of the function, we return. This really
* isn't needed, it's just a bad habbit I have, do it if you like, I
* think it looks cleaner.
*/
return;
}
Now that we have a way to learn powers/skills we should give them some fighting powers, we don't want our Titans to get killed by a tiny snail.
Open fight.c, let's see what we should edit... heres a quick list of what we need to edit for a new class, we will go into the details later on.
* check_dodge() // obvious dodge. * check_parry() // obvious parry. * update_damcap() // Defines the damcap of a certain class. * cap_dam() // Defines the toughness of a class. * multi_hit() // Takes care of all midround attacks. * one_hit() // Defines the damage dealt with certain attacks. * number_attacks() // Defines how many "normal" attacks a class has.
First lets edit check_dodge(), since we haven't given the Titans much to work with, we will use TITAN_COMBAT to decide how good a player is at dodging (if he is a Titan, that is).
You will notice that both check_dodge() and check_parry() goes through the classes two times, first we figure out what's the chance of dodging, then we cap the chance (depending on codebase, I'm currenly using < 20 and > 80 - meaning that should the chance be below 20% we will set it to 20% and if it's above 80% we will set it to 80%) and go through all the classes again. Obvious it's better to have any modifiers after the cap, since we could then get a 100% chance of dodging if we had a good level. Therefore I suggest setting the best modifier before the cap, and then give the class a small modifier after the cap. Take a look at what modifiers the other classes are getting to get an idea on how big a modifier you should give your class.
We will allow the Titans to gain a 8% chance of dodging per level in TITAN_COMBAT the player has, this making it top at 40% when the player reaches level 5 in TITAN_COMBAT. You will notice that a player both get's positive modifiers (from his skills or levels) and negative modifiers (from the attackers skills or levels), we should define a set of modifiers for both an attacking Titan and a defending Titan.
/*
* In the code, 'victim' is the player trying to dodge, and 'ch' is
* the attacker. We will give the Titans the same attack modifier as
* they have defence, it could easily be different, feel free to try
* different values. Notice that we check to see if the player is a
* monster; this is done to avoid crashes, since monsters don't have
* the pcdata structure.
*/
if (!IS_NPC(victim) && IS_CLASS(victim, CLASS_TITAN))
chance += victim->pcdata->powers[TITAN_COMBAT]*8;
if (!IS_NPC(ch) && IS_CLASS(ch, CLASS_TITAN))
chance -= ch->pcdata->powers[TITAN_COMBAT]*8;
/*
* Then below the cap, we make another little modifier, this time
* only giving 2% per level. Allowing a Titan an extra 10% when maxed.
*/
if (!IS_NPC(victim) && IS_CLASS(victim, CLASS_TITAN))
chance += victim->pcdata->powers[TITAN_COMBAT]*2;
if (!IS_NPC(ch) && IS_CLASS(ch, CLASS_TITAN))
chance -= ch->pcdata->powers[TITAN_COMBAT]*2;
The same should be done to check_parry, if you have defined other combat oriented powers for your class, you should let them give a small modifier as well, just remember not to make the total to big, or your class will be a bit rigged :)
In update_damcap() we must add modifiers for the damcap, the base is 1000, so let's just stack up from there, again we will use TITAN_COMBAT (it's all we have to work with for this class).
Find where the other classes are given bonusses and add something for your class, we will give our Titans an extra 300 damcap per level he has. (That's a total of 1500 extra damcap, giving him 2500 with the 1000 base).
if (IS_CLASS(ch, CLASS_TITAN))
{
max_dam += ch->pcdata->powers[TITAN_COMBAT];
}
Now we should give our Titans some toughness, letting them resist some damage. As before we will use TITAN_COMBAT (we will use the TITAN_KNOWLEGDE later to decide what powers the Titan can use)
In cap_dam add something for your class, we will give a 10% reduction in the damage for each level the Titan has in TITAN_COMBAT.
/*
* Notice that we again check to see if the players
* is a monster, we don't want to crash the
* mud because monsters don't have pcdata.
*/
if (!IS_NPC(victim) && IS_CLASS(victim, CLASS_TITAN))
{
dam *= 100 - victim->pcdata->powers[TITAN_COMBAT] * 10;
dam /= 100;
}
Now our Titans have damcap, the ability to dodge and parry better, and can resist damage, now we just need to give the class some attacks, first let's give the class one extra normal attack per TITAN_COMBAT he has (normal attacks are weapon attacks, ie. slice, slash, etc). Edit the function number_attacks(), the amount of attacks a player gains depends on what he is fighting, first we give him some extra attacks against mobs (You will notice the line 'if (IS_NPC(victim))' this is where we add the attacks vs. mobs).
/*
* Remember to check for IS_NPC each time you use pcdata.
*/
if (!IS_NPC(ch) && IS_CLASS(ch, CLASS_TITAN))
count += ch->pcdata->powers[TITAN_COMBAT];
Then below, when we come to the attacks gained against other players, we add the same line, since we want Titans to gain the same amount of attacks vs players as well as monsters.
Now that the class has normal attacks, we give the class some special "free" attacks, that hit each round (like werewolves have quills and drows have darktendrils...), this all happens in multi_hit(). If you don't want to use some of the attack types that already exists, but want to make some unique attacks for your class, we need to define them first... Let's make a 'sweeping blow' attack for Titans.
open merc.h (our main headerfile) and search for one of the old attack types (they are all called something like gsn_*, ie. gsn_quills (the werewolf attack), and add a define for our new attack.
extern sh_int gsn_sweepblow;
now you most likely need to up the variable MAX_SKILL (also located in merc.h), since we are adding a new "skill" (the attack counts as a skill), just let MAX_SKILL be one higher.
now open db.c and define your attack again (put it together with all the old attack types)
sh_int gsn_bladespin;
Now that we have the skill, or at least have the name so we can use it in the code, we should define the messages for the attack, this is done in const.c, so open that file.
If you search for gsn_quills in const.c, you will find it located in a table (of skills), and it should look something like this :
{
"quills", 13,
spell_null, TAR_IGNORE, POS_STANDING,
&gsn_quills, SLOT( 0), 0, 24,
"razor quills", "!quills!"
},
The important things here is the strings "quills", "razor quills" and the number 13. The strings are the messages used when sending messages to players and the number is the level needed to use the skill (we set this to 13, since we don't want players to use it like a skill, and since the highest level a player can be is 12, noone will be able to use it, which is good). The rest isn't really important for us, so we just copy/paste it changing what we need.
{
"sweeping blow", 13,
spell_null, TAR_IGNORE, POS_STANDING,
&gsn_sweepblow, SLOT( 0), 0, 24,
"sweeping blow", "!sweep!"
},
Make sure to add this at the BOTTOM of the table, do not put it somewhere in the middle, you will cause serious problems if you do this, since the skills are each assigned a number upon boot, and this number is saved in the players files. ie. should someone have a wand of magic missiles, and someone where to add a new skill right before magic missile, the wand would suddenly cast spells of that type instead of magic missile.... so make sure to add the skill in the bottom of the table ( you can let paradox stay as the last one if you want, it's not important/critical).
Now that we have our new attack, we should edit multi_hit() in fight.c so our Titans can use it in combat. Add the following in multi_hit() somewhere with all the other classes attacks.
if (!IS_NPC(ch) && IS_CLASS(ch, CLASS_TITAN))
{
/*
* each time we call one_hit() the player does one attack against
* the victim of the specific type. The number following the
* attacktype decides whether the attack is a weaponattack (and in
* that case which hand the attack is made with), if we set the
* value to 0, the attack will not be a weaponattack, but count as
* an unarmed attack. The real difference is that weaponattacks
* counts as training the weapon the player is wielding in that
* hand, and any modifiers to the damage that the weapon gives is
* also added to the attack. Unarmed attacks will train the
* players unarmed skill.
*/
/* one attack with the right hand weapon */
one_hit(ch, victim, gsn_sweepblow, 1);
/* one attack that counts as unarmed. */
one_hit(ch, victim, gsn_sweepblow, 0);
}
Now let's take a look at one_hit(), we want the Titan to deal some hefty damage, since he's so big and mean. Start reading the code, there should be a place where different classes modify the damage, if there isn't one, you could add it yourself, but most likely there is, add a line for your class, something much like this.
if (!IS_NPC(ch) && IS_CLASS(ch, CLASS_TITAN))
{
/* the sweeping blow does double damage when a Titan does it */
if (dt == gsn_sweepblow) dam *= 2;
/* 10% extra damage per level in combat */
dam *= 100 + ch->pcdata->powers[TITAN_COMBAT] * 10;
dam /= 100;
}
This should make sure Titans deals a little extra damage, 'dt' is the attacktype, and we want sweeping blows to deal alot of damage, so we double the damage done by these attacks.
Our class is now ready to play... they don't have any powers yet, but at least they can fight and become larger and more powerful in combat.
When giving a class powers, it's always smart to check for mob status, and if the player is the class we want it to be, to avoid problems. Think before you code, and you'll avoid many strange bugs.
/*
* always.. always I tell you.
*/
if (IS_NPC(ch)) return;
if (!IS_CLASS(ch, CLASS_TITAN))
{
send_to_char("Huh?\n\r",ch);
return;
}
When making a new command/power for a class, you must define it in merc.h and add it to the list of commands in interp.c before the player can use it (this also goes for the do_learn command we coded before), also remember that commands that players are supposed to use, should always be defined with these arguments (CHAR_DATA *ch, char *argument), since we need to know what argument the player gave with the command and we need to know what player used the command.
in merc.h we should add do_learn as :
DECLARE_DO_FUN( do_learn );
and in interp.c we should add the function to the commands that players can use like this :
{ "learn", do_learn, POS_STANDING, 3, LOG_NORMAL, CLASS_TITAN, 0 ,0 },
The first string is the name of the command, and the next is the coded function that will be used when a player types the command. Followed by the position the player must be in to use it (ie. use POS_FIGHTING if the command should be usable when fighting), the number 3 is the minimum level required to use the command, 3 is the level of avatars, and we should always use 3 for class commands, since we don't want them to use their commands when they are mortal (level 1 is newbies and level 2 is mortal). Then we have the logging of the function, if you want to log every use of the command, use LOG_ALWAYS. The last three numbers should be the name of the class that can use it, and then two zeros (the last two numbers are used for discplines, but since we don't want the class to use disciplines, we will just set these to 0).
Remember that practice makes a master, you most likely won't get it right the first time, and time and energy must be put into testing the class before letting players use it.