
Class Based Mod
Starter
Seems like everybody wants to make a class-based mod these days ...
Man, people sure do love to play and make class based mods these days, must be a fad or something.
Anyway, this tutorial will let you add 2 classes, and an observer mode to your mod. Remember that the classes will only work in multiplayer because of the way it assigns them. Make sure you add in only the code which is blue and take out the code that is pink.
First, let's set up the variable around which our classes are based. Open up g_local.h and add this integer into the respawn structure so it looks like this:
// client data
that stays across deathmatch respawns
typedef struct
{
client_persistant_t coop_respawn;
// what to set client->pers to on a respawn
int
enterframe;
// level.framenum the client
entered the game
int
score;
// frags, etc
vec3_t cmd_angles;
// angles sent over in the last command
int
game_helpchanged;
int
helpchanged;
int
class; //
added for the class variable
} client_respawn_t;
Next open up g_cmds.c and go down to the client command section, and add these two commands near the bottom, right after the wave command:
else if (Q_stricmp (cmd, "wave") == 0)
Cmd_Wave_f (ent);
else if (Q_stricmp (cmd,
"class1") == 0)
{
ent->client->resp.class = 1;
EndObserverMode(ent);
}
else if (Q_stricmp (cmd, "class2") ==
0)
{
ent->client->resp.class = 2;
EndObserverMode(ent);
}
else if (Q_stricmp (cmd, "class") ==
0)
{
if
(ent->client->resp.class == 1)
gi.cprintf(ent, PRINT_HIGH, "You are Class 1.\n");
else if
(ent->client->resp.class == 2)
gi.cprintf(ent, PRINT_HIGH, "You are Class 2.\n");
else
gi.cprintf(ent, PRINT_HIGH, "You are an OBSERVER.\n");
}
else // anything
that doesn't match a command will be a chat
Cmd_Say_f (ent, false,
true);
Those will be the commands that clients enter to switch to a class from observer mode, or the switch classes midgame. It also adds a "check" command so a player can just type "class" and it will tell him/her what class they are and if they're an observer.
Next open up p_client.c and add this whole new section to the InitClientPersistant function. It defines what weapons your classes will have.
/*
==============
InitClientPersistant
This is only called when the game first initializes in single player,
but is called after each death and level change in deathmatch
==============
*/
void InitClientPersistant (gclient_t *client)
{
if (client->resp.class == 1)
{
//Class 1
gitem_t
*item;
memset
(&client->pers, 0, sizeof(client->pers));
item = FindItem("Blaster");
client->pers.selected_item = ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item] = 1;
item = FindItem("Jacket
Armor");
client->pers.selected_item = ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item]
= 25;
item =
FindItem("Rockets");
client->pers.selected_item = ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item] = 30;
item = FindItem("Rocket
Launcher");
client->pers.selected_item = ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item]
= 1;
client->pers.weapon =
item;
}
else if (client->resp.class == 2)
{
//Class 2
gitem_t
*item;
memset (&client->pers, 0,
sizeof(client->pers));
item = FindItem("Blaster");
client->pers.selected_item
= ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item] = 1;
item = FindItem("Combat Armor");
client->pers.selected_item =
ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item]
= 50;
item = FindItem("Slugs");
client->pers.selected_item
= ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item] = 30;
item =
FindItem("Railgun");
client->pers.selected_item =
ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item]
= 1;
client->pers.weapon = item;
}
else
{
//Observer mode, doesn't really matter what they
have
gitem_t
*item;
memset
(&client->pers, 0, sizeof(client->pers));
item = FindItem("Combat
Armor");
client->pers.selected_item = ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item]
= 1;
client->pers.weapon =
item;
}
gitem_t *item;
memset (&client->pers, 0,
sizeof(client->pers));
item = FindItem("Blaster");
client->pers.selected_item = ITEM_INDEX(item);
client->pers.inventory[client->pers.selected_item] = 1;
client->pers.weapon = item;
client->pers.health
= 100;
client->pers.max_health = 100;
client->pers.max_bullets = 200;
client->pers.max_shells = 100;
client->pers.max_rockets = 50;
client->pers.max_grenades = 50;
client->pers.max_cells = 200;
client->pers.max_slugs = 50;
client->pers.connected = true;
}
Ok, that was huge. All it does is check to see which class they are based on the class variable, then it assigns the weapon, armor, items, or ammo when they spawn or respawn. You can change what weapons a class starts with easily. Just change the actual names of the items! Make sure you have the correct spelling, else Quake2 will crash when you try to pick your class. Next go back up to the player_die function, still in p_client.c, and add in one line like this:
/*
==================
player_die
==================
*/
void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker,
int damage, vec3_t point)
{
int n;
self->svflags &=
~SVF_NOCLIENT;
VectorClear (self->avelocity);
self->takedamage = DAMAGE_YES;
self->movetype = MOVETYPE_TOSS;
self->s.modelindex2 = 0; //
remove linked weapon model
self->s.angles[0] = 0;
self->s.angles[2] = 0;
self->s.sound = 0;
self->client->weapon_sound = 0;
self->maxs[2] = -8;
...
All that does is make sure
that a player isn't invisible when they switch to a class from observer
mode. You don't want invisible players running around, do you?
Next we need to do some serious work with ClientBeginDeathmatch. Change
it so it looks like this:
/*
=====================
ClientBeginDeathmatch
A client has just connected to the server in
deathmatch mode, so clear everything out before starting them.
=====================
*/
void ClientBeginDeathmatch (edict_t *ent)
{
G_InitEdict (ent);
InitClientResp (ent->client);
// locate ent at a spawn point
PutClientInServer (ent);
ent->client->ps.gunindex
= 0;
gi.linkentity (ent);
if (level.intermissiontime)
{
MoveClientToIntermission
(ent);
}
else
{
// send effect
gi.WriteByte
(svc_muzzleflash);
gi.WriteShort
(ent-g_edicts);
gi.WriteByte (MZ_LOGIN);
gi.multicast
(ent->s.origin, MULTICAST_PVS);
}
gi.bprintf (PRINT_HIGH, "%s entered the
game\n", ent->client->pers.netname);
// make sure all view stuff is valid
ClientEndServerFrame (ent);
}
Next add this function directly above ClientBeginDeathmatch:
void
EndObserverMode(edict_t* ent)
{
ent->movetype &= ~MOVETYPE_NOCLIP;
ent->solid &= ~SOLID_NOT;
ent->svflags &= ~SVF_NOCLIENT;
PutClientInServer (ent);
if (level.intermissiontime)
{
MoveClientToIntermission
(ent);
}
else
{
// send effect
gi.WriteByte
(svc_muzzleflash);
gi.WriteShort
(ent-g_edicts);
gi.WriteByte (MZ_LOGIN);
gi.multicast
(ent->s.origin, MULTICAST_PVS);
}
if (ent->client->resp.class == 1)
gi.bprintf (PRINT_HIGH,
"%s is Class 2\n", ent->client->pers.netname);
else if (ent->client->resp.class == 2)
gi.bprintf (PRINT_HIGH,
"%s is Class 1\n", ent->client->pers.netname);
}
This is basically the same as the original ClientBeginDeathmatch. When a player enters the game it now says nothing until they pick a class. It's a more reliable way of doing this, and works much better than the original way of killing yourself and losing frags. This is how the *good* mods do it.
Ok, just one more section needs to be changed. Go to the client think function, also in p_client.c. Add in the section as shown:
/*
==============
ClientThink
This will be called once for each client frame, which will
usually be a couple times for each server frame.
==============
*/
void ClientThink (edict_t *ent, usercmd_t *ucmd)
{
gclient_t *client;
edict_t *other;
int i, j;
pmove_t pm;
level.current_entity = ent;
client = ent->client;
if (level.intermissiontime)
{
client->ps.pmove.pm_type
= PM_FREEZE;
// can exit intermission
after five seconds
if (level.time >
level.intermissiontime + 5.0
&& (ucmd->buttons & BUTTON_ANY) )
level.exitintermission = true;
return;
}
pm_passent = ent;
// set up for pmove
memset (&pm, 0, sizeof(pm));
if (ent->movetype == MOVETYPE_NOCLIP)
client->ps.pmove.pm_type
= PM_SPECTATOR;
else if (ent->s.modelindex != 255)
client->ps.pmove.pm_type
= PM_GIB;
else if (ent->deadflag)
client->ps.pmove.pm_type
= PM_DEAD;
else
client->ps.pmove.pm_type
= PM_NORMAL;
client->ps.pmove.gravity = sv_gravity->value;
if (ent->client->resp.class < 1)
{
ent->solid = SOLID_NOT;
ent->movetype =
MOVETYPE_NOCLIP;
ent->svflags |=
SVF_NOCLIENT;
}
pm.s = client->ps.pmove;
for (i=0 ; i<3 ; i++)
{
pm.s.origin[i] =
ent->s.origin[i]*8;
pm.s.velocity[i] =
ent->velocity[i]*8;
}
if (memcmp(&client->old_pmove, &pm.s,
sizeof(pm.s)))
{
pm.snapinitial = true;
// gi.dprintf ("pmove changed!\n");
}
...
You're done! This last section added in some checks to see if they are an observer. If they are, then they can move freely around the map, are invisble, and are invulnerable. A player is in observer mode automatically upon entering the game, and must type either class1 or class2 in the console to switch to a class. It's very easy to add more classes this way, just extend the else if statements in g_cmds.c and p_client.c.
Now, if you want to make it so a class can't pick up other weapons, and only has what it starts with, you can prevent any weapons from spawning on the maps. This is very easy to do, it just consists of telling the game to spawn ammo in the place of the weapons. Just open up g_items.c and go to the spawnitem function. Add in this big chunk of code:
/*
============
SpawnItem
Sets the clipping size and plants the object on the floor.
Items can't be immediately dropped to floor, because they might
be on an entity that hasn't spawned yet.
============
*/
void SpawnItem (edict_t *ent, gitem_t *item)
{
if
(deathmatch->value)
{
if (strcmp(ent->classname,
"weapon_shotgun") == 0)
{
ent->classname =
"ammo_shells";
item = FindItemByClassname
("ammo_shells");
}
if
(strcmp(ent->classname, "weapon_supershotgun") == 0)
{
ent->classname =
"ammo_shells";
item =
FindItemByClassname ("ammo_shells");
}
if (strcmp(ent->classname,
"weapon_machinegun") == 0)
{
ent->classname =
"ammo_bullets";
item =
FindItemByClassname ("ammo_bullets");
}
if
(strcmp(ent->classname, "weapon_chaingun") == 0)
{
ent->classname =
"ammo_bullets";
item =
FindItemByClassname ("ammo_bullets");
}
if
(strcmp(ent->classname, "weapon_grenadelauncher") == 0)
{
ent->classname =
"ammo_grenades";
item = FindItemByClassname
("ammo_grenades");
}
if
(strcmp(ent->classname, "weapon_rocketlauncher") == 0)
{
ent->classname =
"ammo_rockets";
item =
FindItemByClassname ("ammo_rockets");
}
if (strcmp(ent->classname,
"weapon_railgun") == 0)
{
ent->classname =
"ammo_slugs";
item =
FindItemByClassname ("ammo_slugs");
}
if
(strcmp(ent->classname, "weapon_hyperblaster") == 0)
{
ent->classname =
"ammo_cells";
item =
FindItemByClassname ("ammo_cells");
}
if
(strcmp(ent->classname, "weapon_bfg") == 0)
{
ent->classname =
"ammo_cells";
item =
FindItemByClassname ("ammo_cells");
}
}
PrecacheItem (item);
if (ent->spawnflags)
{
if
(strcmp(ent->classname, "key_power_cube") != 0)
{
ent->spawnflags = 0;
gi.dprintf("%s at %s has invalid spawnflags set\n", ent->classname,
vtos(ent->s.origin));
}
}
...
All this large section does is change what spawns on the maps. If the map has a spawn point for the weapon, the code tells it to place the appropriate ammo there instead. This will work for all items, so if you don't want quads spawning or something else, just change it making sure you have the correct names from the item list further down in the g_items.c file.
Now you need to make it so when a player dies, he doesn't drop his weapon. This one is really easy, just open up p_client.c and go down to the player_die function. Remove the one marked line as shown:
/*
==================
player_die
==================
*/
void player_die (edict_t *self, edict_t *inflictor, edict_t *attacker,
int damage, vec3_t point)
{
int n;
VectorClear (self->avelocity);
self->takedamage = DAMAGE_YES;
self->movetype = MOVETYPE_TOSS;
self->s.modelindex2 = 0; //
remove linked weapon model
self->s.angles[0] = 0;
self->s.angles[2] = 0;
self->s.sound = 0;
self->client->weapon_sound = 0;
self->maxs[2] = -8;
// self->solid = SOLID_NOT;
self->svflags |= SVF_DEADMONSTER;
if (!self->deadflag)
{
self->client->respawn_time = level.time + 1.0;
LookAtKiller (self,
inflictor, attacker);
self->client->ps.pmove.pm_type = PM_DEAD;
ClientObituary (self,
inflictor, attacker);
TossClientWeapon
(self);
if (deathmatch->value)
Cmd_Help_f (self); // show scores
}
// remove powerups
self->client->quad_framenum = 0;
self->client->invincible_framenum = 0;
self->client->breather_framenum = 0;
self->client->enviro_framenum = 0;
...
That's it. Now no weapons spawn and no players drop weapons, your class based mod is complete.
Tutorial by Willi
Back to Quake Style -
Tutorials