Tutorial: My First Quake III Arena Mod
Okay, now to get those pesky bots working without any errors. As we had seen before, adding bots to the game causes "weapon index out of range" errors to be displayed in the console. We obviously do not want this to happen. Take a look at BotAimAtEnemy() in ai_dmq3.c (in the game project). Somewhere around line 3295 you'll see this line: trap_BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);. This function will take the bs->weaponnum value and check what accuracy this bot has for the related weapon. For reasons that are still unknown to me, bs->weaponnum is always 0 and obviously 0 is not a valid number for a weapon (these range from 1 to 9). For the sake of testing things (and to inform you about a useful function) enter the following below the trap_BotGetWeaponInfo line:
Com_Printf("bs->weaponnum = %d.\n", bs->weaponnum);
The Com_Printf function prints a string to the in-game console, which is also useful for some simple debugging and testing. After compiling and running, you'll notice it'll say bs->weaponnum = 0. in the console. The cause for this is located in BotChooseWeapon() around line 1550. You'll notice this line: newweaponnum = trap_BotChooseBestFightWeapon(bs->ws, bs->inventory);
Now you'll notice that newweaponnum is always 0, so the problem originates in trap_BotChooseBestFightWeapon. I'm not sure what happends in there, but I do have a quick and dirty workaround for you. Simply replace the trap_BotChooseBestFightWeapon line with the code below.
if (bs->inventory[INVENTORY_GAUNTLET] == 1) newweaponnum = WP_GAUNTLET; if (bs->inventory[INVENTORY_MACHINEGUN] == 1) newweaponnum = WP_MACHINEGUN; if (bs->inventory[INVENTORY_SHOTGUN] == 1) newweaponnum = WP_SHOTGUN; if (bs->inventory[INVENTORY_GRENADELAUNCHER] == 1) newweaponnum = WP_GRENADE_LAUNCHER; if (bs->inventory[INVENTORY_ROCKETLAUNCHER] == 1) newweaponnum = WP_ROCKET_LAUNCHER; if (bs->inventory[INVENTORY_LIGHTNING] == 1) newweaponnum = WP_LIGHTNING; if (bs->inventory[INVENTORY_RAILGUN] == 1) newweaponnum = WP_RAILGUN; if (bs->inventory[INVENTORY_PLASMAGUN] == 1) newweaponnum = WP_PLASMAGUN; if (bs->inventory[INVENTORY_BFG10K] == 1) newweaponnum = WP_BFG;
Since the trap_BotChooseBestFightWeapon() function returns a weapon number that the bot should use, it's easily replaced with the new code. Because players (and bots) in our mod always carry a single weapon, the weapon of choice for the bot is the (only) weapon currently in it's possession. By checking the bs->inventory values, we can see if a bot is in possession of a certain weapon and if so, set newweaponnum to that weapon's number. It's not an elegant fix but it does the job. When we compile the game again, you'll see that the bot errors are gone.
Only a few details remain. In our features listing on page 3 we had an item about displaying a message whenever someone acquired the final weapon. To do this, we must go back to the player_die function in g_combat.c. Remember where we added the code below:
if (attacker->client->currentWeapon != GetFinalWeapon()) { AddScore( attacker, self->r.currentOrigin, 1); ProgressWeapon( attacker, qfalse ); } else { AddScore( attacker, self->r.currentOrigin, 10); ProgressWeapon( attacker, qtrue ); }
We're going to expand that piece of code a bit further. Under the first ProgressWeapon call (where the player doesn't have the final weapon yet, we're going to add a piece of code so it looks like this:
if (attacker->client->currentWeapon != GetFinalWeapon()) { AddScore( attacker, self->r.currentOrigin, 1); ProgressWeapon( attacker, qfalse ); if (attacker->client->currentWeapon == GetFinalWeapon()) { char *finalWeaponName; switch (finalWeapon) { case WP_GAUNTLET : finalWeaponName = "Gauntlet"; break; case WP_MACHINEGUN : finalWeaponName = "Machinegun"; break; case WP_SHOTGUN : finalWeaponName = "Shotgun"; break; case WP_GRENADE_LAUNCHER : finalWeaponName = "Grenade Launcher"; break; case WP_ROCKET_LAUNCHER : finalWeaponName = "Rocket Launcher"; break; case WP_LIGHTNING : finalWeaponName = "Lightninggun"; break; case WP_RAILGUN : finalWeaponName = "Railgun"; break; case WP_PLASMAGUN : finalWeaponName = "Plasmagun"; break; case WP_BFG : finalWeaponName = "BFG"; break; } trap_SendServerCommand( -1, va("cp \"%s" S_COLOR_WHITE " has the %s!\n\"", attacker->client->pers.netname, finalWeaponName) ); } } else { AddScore( attacker, self->r.currentOrigin, (g_lastWeaponPoints.integer > 0 ? g_lastWeaponPoints.integer : 1)); ProgressWeapon( attacker, qtrue ); }
What happends here is that the game progresses the player to the next weapon (like before) but now it checks if the player has the final weapon after progressing the player to the next weapon. So when the attacker is awarded the final weapon, the new code kicks into action. It evaluates the return value of GetFinalWeapon() through the switch statement. This way it can store the name of the weapon in a temporary variable. After that, the trap_SendServerCommand() function sends a server command which will send the message to all clients (the first -1 argument is the client ID, -1 means all clients). va() is a function that's used for string formatting with arguments. It parses attacker->client->pers.netname (the name of the attacking player) into the first %s in the string passed to va(). The second %s is replaced by the weapon name we found with our switch statement. Remember to always follow up a player's name with S_COLOR_WHITE. This is a constant for "^7", which sets the color back to white. If a player ends his name in a different color, the text would continue in that color if S_COLOR_WHITE was omitted. In the same way, the entire text could be made red by starting the string with S_COLOR_RED.
The last thing we need to do is just a minor detail. It's the status bar which still displays an ammo box of the currently held weapon. It's your own choice if you want to remove this or not. You might find it a nice detail, but I'm going to show you how to remove the ammo box from the screen. Locate the function CG_DrawStatusBar() in the file cg_draw.c (cgame project). Around line 535 you'll find the following bit of code:
// draw any 3D icons first, so the changes back to 2D are minimized if ( cent->currentState.weapon && cg_weapons[ cent->currentState.weapon ].ammoModel ) { origin[0] = 70; origin[1] = 0; origin[2] = 0; angles[YAW] = 90 + 20 * sin( cg.time / 1000.0 ); CG_Draw3DModel( CHAR_WIDTH*3 + TEXT_ICON_SPACE, 432, ICON_SIZE, ICON_SIZE, cg_weapons[ cent->currentState.weapon ].ammoModel, 0, origin, angles ); }
This piece of code draws the ammo box in the status bar. Simply remove it or comment it out to stop the game from drawing the ammo box. This basically is all we need to do. You'll see that further down, (after the #endif) it draws the ammo amount and 2D weapon icon. However, this piece of code only draws those things when the currently held weapon has an ammo amount higher than -1. Since all of our weapons have -1 (unlimited) ammo there's no real need to remove this. If you want to be strict about it, you could remove the code that draws the ammo amount (and 2D icon) as well. I did remove it for our release of the mod.