Quake DeveLS - DOUBLE JEOPARDY!

Author: SumFuka
Difficulty: Medium

Up to tutorial 5 we have been mainly working with hacks on the client code. Lets try and write something that is based on server code and changes the gameplay of quake2 completely. This is a rather lengthy tutorial, but is not really that hard if you look at it step by step.

Ok, our mod is going to be really simple, lets say that every now and again the server will decide to play 'double jeopardy' or 'triple jeopardy'. We'll get the server to pick a random type of weapon, and if you kill someone with that particular weapon during triple jeopardy, you'll get 2 or 3 frags instead of 1 ! Sounds fun ? Let's try it out !

Let's add some server variables

Ok, let's think about what variables the server needs. Well obviously it needs a 'jeopardy state', i.e. whether jeopardy is ON or OFF. If it is on, we also need the server to remember what weapon we are playing triple jeopardy with. Lastly, we need to remember at what time the jeopardy will end (make it last for 30 seconds, say).

Create a new file, jeopardy.c in the same directory as all the other .c files. Start off with this :

#include "g_local.h"
#include "jeopardy.h"

int jeopardy_state;
char jeopardy_weapon[50];
float jeopardy_end;
The #include directive allows us to use functions from other .c files, by including a header file (ending in .h). The g_local.h file that we include describes (amongst other things) the quake2 game interface functions. The jeopardy.h file will be a header file we will write - it will describe the functions used for JEOPARDY !

In fact, lets enter in a new file called jeopardy.h right now :

// JEOPARDY.H
// these variables are defined in jeopardy.c
// (the 'extern's say that they exist in another .c file)
extern int jeopardy_state;
extern char jeopardy_weapon[50];
extern float jeopardy_end;

// Jeopardy states :
#define JEOPARDY_OFF 0
#define JEOPARDY_DOUBLE 1
#define JEOPARDY_TRIPLE 2

// Jeopardy data :
#define JEOPARDY_DELAYTIME 30
#define JEOPARDY_PLAYTIME 30

// jeopardy function prototypes, available to the server to call :
void JeopardyInit(float timenow);
void JeopardyConnect(edict_t *newplayer);
void JeopardyStart(float timenow);
void JeopardyEnd(float timenow);
void JeopardyThink(float timenow);
int JeopardyBonus(char *inflictor_class, edict_t *winner);
The #defines simply define constants that are used in the code. When we compile our program, the compiler substitutes the numbers on the right wherever it comes across the identifier on the left. For example, when we write JEOPARDY_DOUBLE in the code, it is compiled as the integer 1.

After the #defines comes a list of function prototypes. A function prototype simply defines the name of a function, and the parameters it uses. Why are these prototypes needed ? The answer is easy : Assume that we would like to call the JeopardyBonus function from p_client.c. Now p_client.c is compiled independently from jeopardy.c, so we need to tell p_client.c that the jeopardy functions indeed do exist ! We do this by putting a function prototype in jeopardy.h and #including jeopardy.h in p_client.h.

Just think of a .h file as a blueprint for the corresponding .c file. The p_client.c file needs to know the blueprint for the jeopardy functions in order to call them.

Jeopardy jeopardy jeopardy...

Open up jeopardy.c again, and we'll add all the functions we need to run our game of jeopardy :

// initialize the jeopardy server variables.
void JeopardyInit(float timenow)
{
	// we start out with jeopardy OFF
	jeopardy_state = JEOPARDY_OFF;

	// the next jeopardy will start in 30 seconds.
	jeopardy_end = timenow + JEOPARDY_DELAYTIME;
}
The above function is used to initialize our jeopardy game, and should be called when the server is started. It sets jeopardy_end to 30 seconds from the time the server is started, which will trigger the next jeopardy to start then.

// tell people they are playing jeopardy, when they connect.
void JeopardyConnect(edict_t *newplayer)
{
	// welcome message
	gi.cprintf (newplayer, PRINT_HIGH, "Welcome to JEOPARDY !\nWhen jeopardy comes on, kill people with\nthe right weapon and get double or triple frags !\n");
}
When a new player connects, we want to tell them that they are playing jeopardy. This function should be called whenever a new player joins a game !

// start playing jeopardy !!!
void JeopardyStart(float timenow)
{
	float rnum;

	// end jeopardy in 30 seconds or so
	jeopardy_end = timenow + JEOPARDY_PLAYTIME;

	// playing double or triple jeopardy ?
	rnum = random();
	if (rnum < 0.67) {
		jeopardy_state = JEOPARDY_DOUBLE;
		gi.bprintf (PRINT_HIGH, "It's DOUBLE JEOPARDY time... 2 frags for ");
	} else {
		jeopardy_state = JEOPARDY_TRIPLE;
		gi.bprintf (PRINT_HIGH, "It's TRIPLE JEOPARDY time... 3 frags for ");
	}

	// what weapon is our jeopardy weapon ?
	rnum = random();
	if (rnum < 0.2) {
		// must kill by blaster or hyperblaster
		strcpy (jeopardy_weapon, "bolt");
		gi.bprintf (PRINT_HIGH, "killing with blaster weapons !\n");
	} else if (rnum < 0.4) {
		// must kill by either shotty or the chaingun or the railgun
		strcpy (jeopardy_weapon, "player");
		gi.bprintf (PRINT_HIGH, "giving out slugs of love.\n");
	} else if (rnum < 0.6) {
		// must kill by grenades
		strcpy (jeopardy_weapon, "grenade");
		gi.bprintf (PRINT_HIGH, "grenade-induced-gibs.\n");
	} else if (rnum < 0.8) {
		// must kill by rockets !
		strcpy (jeopardy_weapon, "rocket");
		gi.bprintf (PRINT_HIGH, "giving a rocket enema !\n");
	} else {
		// must kill by bfg
		strcpy (jeopardy_weapon, "bfg");
		gi.bprintf (PRINT_HIGH, "BFG carnage !\n");
 	}
}
Now the function above starts jeopardy and sets it to last for 30 seconds (or whatever JEOPARDY_PLAYTIME is set to). Then we randomly select DOUBLE or TRIPLE jeopardy... triple jeopardy happening 33% of the time. Then we randomly select a type of weapon which will score bonus jeopardy. We let all players know this stuff by using the bprintf function. Notice how I have used the bprintf function in two stages... by leaving the first part without a newline (\n), the second string is attached to the end of the last line.

// end the show !
void JeopardyEnd(float timenow)
{
	// tell everyone "It's Over."
	gi.bprintf (PRINT_HIGH, "JEOPARDY is over...\n");
	
	// turn jeopardy OFF
	jeopardy_state = JEOPARDY_OFF;

	// the next jeopardy will start in 30 seconds.
	jeopardy_end = timenow + JEOPARDY_DELAYTIME;
}
This function tells everyone that jeopardy is over, and sets the next jeopardy to start in 30 seconds (or whatever JEOPARDY_DELAYTIME is).

// think, to check if jeopardy has ended.
void JeopardyThink(float timenow)
{
	// is it time to start or end jeopardy ?
	if (timenow >= jeopardy_end)
	{
		// if jeopardy is OFF, turn it ON, and vice versa
		if (jeopardy_state == JEOPARDY_OFF) {
			JeopardyStart(timenow);
		} else {
			JeopardyEnd(timenow);

		}
	}
}
The JeopardyThink function should be called every second or so, and it's job is to check if the time jeopardy_end has been reached (in seconds). If it has, then it turns jeopardy on or off.

// did the killer get a bonus ?
int JeopardyBonus(char *inflictor_class, edict_t *winner)
{
	// return 0 if jeopardy is off.
	if (jeopardy_state == JEOPARDY_OFF)
		return 0;

 	// jeopardy is on, let's check the inflictor class :
	if ( !strncmp (inflictor_class, jeopardy_weapon, strlen(jeopardy_weapon)))
	{
		// return a bonus of 1 (double jeopardy) or 2 (triple jeopardy)
		gi.bprintf (PRINT_MEDIUM, "%s scores JEOPARDY !\n", winner->client->pers.netname);
	 	return jeopardy_state;
	}

	// the weapon didn't match... sorry, but no cigar !
	return 0;
}
This function returns a bonus value based on how someone was killed. If jeopardy is on, and the attacker killed someone with the weapon type specified by jeopardy, then they get extra frags !

The bprintf line tells everyone that someone scored on jeopardy ! I have been a little tricky on the next line, by returning jeopardy_state as the bonus... jeopardy_state will be 1 for JEOPARDY_DOUBLE and 2 for JEOPARDY_TRIPLE (check the #defines in the header file). Remember that this is a BONUS number we are returning (in addition to the normal frag you get for killing someone), so 1 and 2 are correct bonus return values. Zero is the return value if no bonus is applicable.

Time to SCORE, Baby !

Open up p_client.c and go to line 84, in the client obituary function.

 		attacker->client->resp.score++;
This is the magic line of code that gives players a frag ! This line of code is written from the perspective of the victim. The line first uses the attacker entity to reference the client object (using the -> thingy), then it references the 'resp' part of the client object (using the -> thingy again). 'resp' is the 'respawnable data' that gets erased when the player respawns in the next level... data that only lasts the lifetime of the level, in other words. Finally, the (.) accesses the attacker's score, which is part of the 'respawnable data'. ++ is a C operator that increments the score by one.

Add this code directly below that line, so that it looks like this :

		attacker->client->resp.score++;

		// STEVE added the line below, to give out jeopardy bonuses !
		attacker->client->resp.score += JeopardyBonus (inflictor->classname, attacker);
We are simply saying here that if jeopardy is on, and the victim was killed with the correct type of weapon, then they will get the bonus frags !

Think think think...

Now we need to call the functions to initialize jeopardy and think for it from appropriate points in the quake2 code. A good place to initialize jeopardy is in the quake2 InitGame function. Open up g_save.c and go to line 188. Add the lines below so it looks like this :

	globals.num_edicts = game.maxclients+1;

	// STEVE Init Jeopardy !
	JeopardyInit(level.time);
Now we need to find a place to call JeopardyConnect... lets open p_client.c and go to line 757, at the very end of the ClientBeginDeathmatch function. Add lines so that it looks like this :
	ClientEndServerFrame (ent);

	// Connecting to a JEOPARDY server
	JeopardyConnect (ent);
Now we need the game to think every 0.1 seconds, lets put these lines at the very end of file g_main.c, at the end of the G_RunFrame function.
	ClientEndServerFrames ();

	// STEVE check if jeopardy should start or end
	JeopardyThink (level.time);
What have we forgotten ?!?

If you try to compile p_client.c or g_save.c or g_main.c the compiler will go mental and complain about not knowing that the jeopardy functions exist ! How can we tell these other source files that the jeopardy functions do indeed exist in jeopardy.c ???

... YES ! We supply our header file (jeopardy.h) to those functions ! Add a #include "jeopardy.h" line directly AFTER the #include "g_local.h" line for EACH of p_client.c, g_save.c and g_main.c. The first couple of lines in those files should look like this :

#include "g_local.h"
#include "jeopardy.h"	// STEVE included this header file here
Classnames for bolts and rockets...

Id (because they were so busy getting quake2 to the awesome standard of quality that it is!) forgot to name the 'bolt' entities that are fired from your hyperblaster. Lets open up g_weapon.c and go to line 323 (in the fire_blaster function). Add a line which gives the bolt a classname of "bolt", like this :

	bolt = G_Spawn();

	// STEVE added the bit below
	bolt->classname = "bolt";

	VectorCopy (start, bolt->s.origin);
Ditto for the fire_rocket function. Go to line 563 and insert a similar line like this :
	rocket = G_Spawn();
	
	// STEVE named the rocket entity below
	rocket->classname = "rocket";

	VectorCopy (start, rocket->s.origin);

We're done ! Phew.

Compile this mod and play away, you will need more than one person in the server to see the effects... round up your friends and play JEOPARDY !

Next week... Lets do different player classes... (I promise !)

Tutorial by SumFuka


This site, and all content and graphics displayed on it,
are ©opyrighted to the Quake DeveLS team. All rights received.
Got a suggestion? Comment? Question? Hate mail? Send it to us!
Oh yeah, this site is best viewed in 16 Bit or higher, with the resolution on 800*600.
Thanks to Planet Quake for there great help and support with hosting.
Best viewed with Netscape 4 or IE 3