Tutorial: My First Quake III Arena Mod
So now that the players (re)spawn with the initial weapon and unlimited ammo it's time to look at progressing the player to the next weapon when he/she makes a kill. For this, we'll be opening up the g_combat.c file. Search for a function called player_die. This function is triggered when a player dies. You'll find the following bit of code halfway through this function:
if ( attacker == self || OnSameTeam (self, attacker ) ) { AddScore( attacker, self->r.currentOrigin, -1 ); } else { AddScore( attacker, self->r.currentOrigin, 1 ); ... }
This piece of code substracts one point from the player's score if he killed himself or a teammate and adds 1 to his score if he made a valid kill. The attacker variable contains the data for the person that made the kill. We're going to expand the second AddScore call with some additional code to do a few new things. What we want to achieve is that the attacker progresses to the next weapon and if he's got the highest weapon in the weapon order (by default the BFG), he's awarded 10 points instead of 1. So the new code looks like this:
if ( attacker == self || OnSameTeam (self, attacker ) ) { AddScore( attacker, self->r.currentOrigin, -1 ); } else { if (attacker->client->currentWeapon != GetFinalWeapon()) { AddScore( attacker, self->r.currentOrigin, 1); ProgressWeapon( attacker, qfalse ); } else { AddScore( attacker, self->r.currentOrigin, 10); ProgressWeapon( attacker, qtrue ); } ... }
As before, if the player killed himself, he's awarded -1 points. Our spawn code will make sure he respawns with the initial weapon, so in this case, we don't have to make any changes to the code. In our new code set up, if the player killed a team mate, he's awarded -1 point but stays at the same weapon. Ofcourse you could call ProgressWeapon and pass qtrue for the reset argument. Then the player would be reset to the initial weapon. At a later stage, you might even decide to make that configurable.
Now if the player was killed by another player, we're checking if that other player is holding the final weapon. For this we've implemented a new function called GetFinalWeapon(). I'll get to that in a bit. In case the attacker was holding the final weapon, we award 10 points and reset him back to the initial weapon (wouldn't want him to keep racking up those points that easily)! If the attacker wasn't holding the final weapon, we award a single point and move him on to the next weapon. As an added challenge for yourself, you could try and figure out how to make that amount of 10 points configurable by the server admin. Shouldn't be too hard, simply follow the same instructions as how the g_weaponOrder cvars were implemented.
So now that that's out of the way, we just need to implement the GetFinalWeapon() function. I've placed it inside the g_combat.c file as well.
int GetFinalWeapon() { if(g_weaponOrder9.string != "" && g_weaponOrder9.integer > 0 && g_weaponOrder9.integer < WP_NUM_WEAPONS && g_weaponOrder9.integer != WP_GRAPPLING_HOOK) return 9; if(g_weaponOrder8.string != "" && g_weaponOrder8.integer > 0 && g_weaponOrder8.integer < WP_NUM_WEAPONS && g_weaponOrder8.integer != WP_GRAPPLING_HOOK) return 8; if(g_weaponOrder7.string != "" && g_weaponOrder7.integer > 0 && g_weaponOrder7.integer < WP_NUM_WEAPONS && g_weaponOrder7.integer != WP_GRAPPLING_HOOK) return 7; if(g_weaponOrder6.string != "" && g_weaponOrder6.integer > 0 && g_weaponOrder6.integer < WP_NUM_WEAPONS && g_weaponOrder6.integer != WP_GRAPPLING_HOOK) return 6; if(g_weaponOrder5.string != "" && g_weaponOrder5.integer > 0 && g_weaponOrder5.integer < WP_NUM_WEAPONS && g_weaponOrder5.integer != WP_GRAPPLING_HOOK) return 5; if(g_weaponOrder4.string != "" && g_weaponOrder4.integer > 0 && g_weaponOrder4.integer < WP_NUM_WEAPONS && g_weaponOrder4.integer != WP_GRAPPLING_HOOK) return 4; if(g_weaponOrder3.string != "" && g_weaponOrder3.integer > 0 && g_weaponOrder3.integer < WP_NUM_WEAPONS && g_weaponOrder3.integer != WP_GRAPPLING_HOOK) return 3; if(g_weaponOrder2.string != "" && g_weaponOrder2.integer > 0 && g_weaponOrder2.integer < WP_NUM_WEAPONS && g_weaponOrder2.integer != WP_GRAPPLING_HOOK) return 2; if(g_weaponOrder1.string != "" && g_weaponOrder1.integer > 0 && g_weaponOrder1.integer < WP_NUM_WEAPONS && g_weaponOrder1.integer != WP_GRAPPLING_HOOK) return 1; return 1; }
I reckon that's pretty straightforward. It checks for each weaponOrder cvar if it's not empty, bigger than 0, smaller than the number of weapons in the game and if it's not the grappling hook. It obviously starts with checking g_weaponOrder9 and moves down to g_weaponOrder1. The first valid value it encounters is your final weapon. Now build the code, copy the resulting dll's to your mod directory and create a game. Throw in a bot and play your first game of your own mod. Unfortunately, you'll quickly notice three problems. First of all, the most obvious one. The game continually displays a "low ammo warning" on the screen. The second problem is a gameplay issue. Whenever a player is fragged he drops the currently held weapon. This could seriously throw the whole balance out of the game, since someone could simply frag a player holding the BFG, pick up the BFG himself and make a frag with the stolen weapon and be awarded 10 points. We do not want this, so we're going to have to remove weapon drops. Another problem that you'll notice is that when playing against bots, the console is spammed with "ERROR: Weapon number out of range" messages. This is a problem we need to fix as well, but let's focus on the low ammo warning first. Find the function CG_DrawAmmoWarning() in cg_draw.c (cgame project). This function is responsible for drawing the ammo warning. The quick and dirty way is to clear the contents of this function and leave an empty function. The other, more elegant solution is to find the CG_Draw2D() function in the same file and remove the call to CG_DrawAmmoWarning();. When you do this, you can also remove the whole CG_DrawAmmoWarning function itself if you wish.
Stopping weapons from being dropped when a player is fragged is simple as well. Find the TossClientItems function in g_combat.c. The whole block of code listed below can be removed from this function:
// drop the weapon if not a gauntlet or machinegun weapon = self->s.weapon; // make a special check to see if they are changing to a new // weapon that isn't the mg or gauntlet. Without this, a client // can pick up a weapon, be killed, and not drop the weapon because // their weapon change hasn't completed yet and they are still holding the MG. if ( weapon == WP_MACHINEGUN || weapon == WP_GRAPPLING_HOOK ) { if ( self->client->ps.weaponstate == WEAPON_DROPPING ) { weapon = self->client->pers.cmd.weapon; } if ( !( self->client->ps.stats[STAT_WEAPONS] & ( 1 << weapon ) ) ) { weapon = WP_NONE; } } if ( weapon > WP_MACHINEGUN && weapon != WP_GRAPPLING_HOOK && self->client->ps.ammo[ weapon ] ) { // find the item type for this weapon item = BG_FindItemForWeapon( weapon ); // spawn the item Drop_Item( self, item, 0 ); }
As you can probably see, this whole piece of code simply takes care of dropping the weapon the player is currently holding (it does some other checks that are irrelevant to us right now). Simply removing this code makes sure now weapons are dropped when players die. Don't forget that the int weapon; declaration can be removed now as well.