Note: this lesson is only compatible with your bot if he picks up items from the item's touch function in items.qc. I did write a tutorial covering this. The native Tutor Bot bot_grab_item() routine will not work properly.
Man, that title is a mouthful. I guess there's no shorter way of explaining today's lesson, though. Here's an even longer way of stating it: your bot will learn the splendid skill of moving towards and picking up important items during battle while he is facing you and shooting.
Yeah, I know, I'm excited too. It's one of the cooler things I've done with my bot. In fact, when I saw my little guy strafe into a rocket launcher and then blast me with it, I thought I was fighting a Reaper. Hehe, wait until you see it.
This tutorial utilizes my hybrid velocity code of course, but therein lies the challenge: if we "push" him in the direction which he is facing, then how are we now to make him face one direction and "push" him in another?
Well, I'm glad you asked, because I have an answer. Ironically enough, we're going to use id software for our solution. They created a variable called self.ideal_yaw, which is supposedly the ideal direction a monster should face. They use it in a dumb way, but we'll put it to smart use.
You see, the bot is always facing the direction of his self.angles_y. The bot model itself is facing that direction. But in order for use to push him in a separate direction, we need another variable to store this direction. And that is self.ideal_yaw. Oh, how fitting.
First things first. For this lesson, we will need to define only one new variable. Open defs.qc, and at the very bottom, add this:
.entity second_goal;M-kay. You should know that during a fight, the bot's self.goalentity is always his enemy. And such, we cannot use that variable. Therefore, this new entity will store the item that the bot wants to strafe into during the battle. Next, open up tutor.qc and move down to the subroutine respawn_bot(), where you should see a section looking something like so:
// finishing his characteristics self.items = IT_SHOTGUN; self.currentammo = self.ammo_shells = self.ammo_shells + 25; self.weapon = IT_SHOTGUN; self.health = 100; self.max_health = 100; self.classname = "bot"; self.air_finished = time + 12; self.enemy = world; self.goalentity = world; self.search_time = 0; self.pausetime = 0;After that beautiful code, add this line:
self.second_goal = world;This merely resets his second goal entity when he dies (just like his self.goalentity or self.enemy). No big deal there.
// ---------------------- void() bot_strafe = // ---------------------- { scan_for_fight_items(); if (self.second_goal != world) { strafe_to_item(); return; }Good. This is the brain of his new skill. Allow me to state this in English: "I will check around for good items that I can pick up. If I see one, let me mark it as my second goal. So then, if I have a second goal, I will move toward it and leave the subroutine."
// ------------------------------------------------ void() scan_for_fight_items = // ------------------------------------------------ { local entity item; local float dist; if (self.second_goal.model == string_null) self.second_goal = world; if (!visible(self.second_goal)) self.second_goal = world; if (self.second_goal != world) return; // checks a radius around him for items dist = 1000; item = findradius(self.origin, dist); while(item) { if ( (item.classname == "weapon_supershotgun" || item.classname == "weapon_nailgun" || item.classname == "weapon_supernailgun" || item.classname == "weapon_grenadelauncher" || item.classname == "weapon_rocketlauncher" || item.classname == "weapon_lightning" || item.classname == "item_health") && visible(item) && item.model != string_null && vlen (item.origin - self.origin) < dist) { dist = vlen (item.origin - self.origin); self.search_time = time + 10; self.second_goal = item; } item = item.chain; } };This is a simple find-the-item-in-a-certain-radius routine. You've seen them, even written them before. If his second goal is invisible or has been picked up, he resets it. If he has a second goal already, he leaves this subroutine. If not, he looks 1000 units around him for one of the items listed there (weapons or health). You can expand this; I just figured these are the key items to get mid-battle. He then remembers the item if he sees one.
// ----------------------------------------- void() jump_random = // ----------------------------------------- { if (!(self.flags & FL_ONGROUND)) return; self.flags = self.flags - (self.flags & FL_ONGROUND); if (random() > 0.5) self.velocity_x = (random() * 100) + 100; else self.velocity_y = (random() * 100) + 100; self.velocity_z = self.velocity_z + 200; sound (self, CHAN_BODY, "player/plyrjmp8.wav", 1, ATTN_NORM); }; // ---------------------- void() walk_forward = // ---------------------- { walkmove(self.angles_y, 20); makevectors(self.angles); self.flags = self.flags - (self.flags & FL_ONGROUND); self.velocity = v_forward * self.speed; }; // ----------------------------------- void() strafe_to_item = // ----------------------------------- { local vector dir; self.ideal_yaw = vectoyaw(self.second_goal.origin - self.origin); if (walkmove(self.ideal_yaw, 20) == TRUE) { self.flags = self.flags - (self.flags & FL_ONGROUND); dir = normalize(self.second_goal.origin - self.origin); self.velocity = dir * self.speed; return; } if (!(self.flags & FL_ONGROUND)) return; if (random() > 0.8) walk_forward(); else jump_random(); };Okay now, don't be a'scared. Along with our new strafe_to_item() method, I threw in two of my navigational support routines. They are called walk_forward() and jump_random(). They are self-explanatory. I hope.