I feel like writing a tutorial, so what if I'm at work? No one will see. My boss won't mind. Believe it or not, I've done most of these lessons at work.
The problem is, I can't test this. In fact, I don't even know how it will work. We'll have to find out together.
But my idea is just so cool, I can't help myself. I want to create a Paintball game for our bot. Honestly. Yes, it may be challenging, but we've done a lot of learning by now; we can do it.
The rules of our game will be as follows: you can kill players with normal weapons, but you only get one frag. If you find a paintgun and cover an opponent with paint, you earn ten frags. Yeah, that's right.
This will be quite a multi-faceted lesson, so let's take a quick peek at what we need, and what we need to do:
- do player skins with paint on them - create a small model for a paint pellet - invent a paint gun - make it look like you splat paint onto an enemyThat's not to bad, right? I'll make you a deal: I'll create the skins and models we need, and you forgive me for my infamous shortcuts. Deal? Good.
C:\QUAKE\TUTOR\PROGSIf yours differs, just remember: wherever your Tutor Bot PROGS.DAT file is, make a \PROGS subdirectory there, and put the two models there. Naturally, PLAYER.MDL is always precached, but we need to do PELLET.MDL. Also, we want to precache another sound, though. Open up weapons.qc and look at W_Precache(), the first subroutine. Right after its first bracket, add this:
precache_model ("progs/pellet.mdl"); precache_sound ("zombie/z_miss.wav");
/*QUAKED weapon_rocketlauncher (0 .5 .8) (-16 -16 0) (16 16 32) */ void() weapon_rocketlauncher = { precache_model ("progs/g_rock2.mdl"); setmodel (self, "progs/g_rock2.mdl"); self.weapon = 3; self.netname = "Rocket Launcher";Okay, change that last line to this:
if (cvar("deathmatch") == 6) self.netname = "Paintgun"; else self.netname = "Rocket Launcher";Woohoo. Save and close.
vector() bot_aim_at_enemy; // ----------------------------- void() fire_paint_pellet = // ----------------------------- { local entity missile; sound (self, CHAN_WEAPON, "weapons/grenade.wav", 1, ATTN_NORM); missile = spawn (); missile.owner = self; missile.movetype = MOVETYPE_FLYMISSILE; missile.solid = SOLID_BBOX; missile.classname = "pellet"; if (self.classname == "player") missile.velocity = aim() * 1500; else missile.velocity = bot_aim_at_enemy() * 1500; missile.angles = vectoangles(missile.velocity); missile.touch = paint_pellet_touch; missile.nextthink = time + 5; missile.think = SUB_Remove; setmodel (missile, "progs/pellet.mdl"); setsize (missile, '-1 -1 -1', '1 1 1'); setorigin (missile, self.origin + v_forward*8 + '0 0 16'); };Hmm, a little bit of trickery here. But not much. Basically I copied this from W_FireRocket(). Then I edited the missile direction part of it. Now both the player and the bot can use the same subroutine. Neat.
// ----------------------------- void() paint_pellet_touch = // ----------------------------- { if (other == world) { self.velocity = '0 0 0'; self.movetype = MOVETYPE_NONE; self.solid = SOLID_NOT; return; } if (other.classname != "player" && other.classname != "bot") return; if (other.health <= 0) return; if (other == self.owner) return; sound (self, CHAN_WEAPON, "zombie/z_miss.wav", 1, ATTN_NORM); particle (other.origin, '20 20 20', 148, 25); if (other.skin < 2) { other.skin = other.skin + 1; T_Damage (other, self, self.owner, 1); } else { other.deathtype = "paint"; T_Damage (other, self, self.owner, 500); } remove(self); };Oh my God. Do you see what I just did? Would you look at that. Whoa.
else if (self.weapon == IT_ROCKET_LAUNCHER) { player_rocket1(); W_FireRocket(); self.attack_finished = time + 0.8; }Paste the following over the previous:
else if (self.weapon == IT_ROCKET_LAUNCHER) { player_rocket1(); if (cvar("deathmatch") == 6) fire_paint_pellet(); else W_FireRocket(); self.attack_finished = time + 0.8; }When the player has the rocket launcher readied and deathmatch set to six, he will fire a pellet instead of a missile. I think that will work. I hope it will work. Remember, I am at work.
// ============================= void() bot_fire_rocket = // ----------------------------- { local entity missile; if (self.weapon == IT_GRENADE_LAUNCHER) { bot_fire_grenade(); return; }After that stuff, paste this:
if (cvar("deathmatch") == 6) { fire_paint_pellet(); return; }This is what we call a hack. We interrupt his rocket-firing code to fire a pellet. Oh well. You can seperate it or clean it up later.
if (cvar("deathmatch") == 6) self.skin = 0;
if (cvar("deathmatch") == 6) self.skin = 0;While we're in here, we might as well add a relevant message of the day, right? Go down to the routine PlayerPostThink(). At the end, paste this:
if (cvar("deathmatch") == 6 && time < 5) centerprint(self, "Now playing PaintBall!\n");};Good. Earlier we gave the paint victim a weird "deathtype" thingie, so I guess we must deal with that. Glide down to ClientObituary(), one of my favorite subroutines. It shall look like so:
/* =========== ClientObituary called when a player dies ============ */ void(entity targ, entity attacker) ClientObituary = { local float rnum; local string deathstring, deathstring2; rnum = random();Right after that, add this:
if (targ.deathtype == "paint") { attacker.frags = attacker.frags + 10; bprint(targ.netname); bprint(" was painted by "); bprint(attacker.netname); bprint("!\n"); return; }Yummy. We interrupt the normally scheduled subroutine to check for a paint victim. We award Van Gogh ten shiny new frags. We print an eloquent message. We exit the routine.