A bot is not tough unless he can kill you.
Most people would not argue with that. Features are cool, and personality is neat. However, the average online player will not take a bot seriously unless that bot can frag him.
A bot will kill you if he picks up the bigger gun or shoots at you more quickly. That's fine. What is really impressive, though, is when the bot can wear you down, chase you when you try to flee, and finish you off when it counts. After all, who doesn't feel like a pasty-faced loser when running away at full speed, only to take a rocket up the ass by a computer-controlled opponent?
Not many artificial deathmatch foes out there can accomplish this. The Reaper does. And maybe the Omicron and Frogbot, but they work with waypoints. In about five minutes, you can add your bot to that list. He will soon learn how to chase enemies through teleporters and around corners.
As you know, I use QuakeC's native navigational methods, walkmove() and movetogoal(), for my bots. Yet in this application they have limitations. Walkmove() only pushes the bot forward; it will not attempt to move around an obstacle. Movetogoal() will walk toward his self.goalentity, yet if his enemy slips through a teleporter, he may end up somewhere entirely different on the map (like far behind or way above the bot). This renders movetogoal() useless.
Another problem we have is that we cannot set a teleporter as the bot's self.goalentity. A teleporter is what we call a "static" entity; it resides in the map, yet it does not use setorigin() to place itself in the world. In other words, it has a middle, yet no entity is resting in that middle.
Alright, so those are our problems. If you are astute, you may already have ascertained the solution: we will create a new invisible entity for the bot, which he can place in the middle of the teleporter while chasing an enemy on the other side of that teleporter. This way, we can utilize movetogoal() anytime.
Did I confuse you there? No worry, it will make sense when you see it all come together. First we will define this new entity. We will call it our "tracker." Open up defs.qc and add this at the bottom:
.entity tracker;Since this is a .entity or "dot entity" (or its real name, field), then other entities can own one of these. In fact, every player and bot in the game will have his own. So, close that, and open client.qc. We will have to spawn one for each player as he enters the game. At the end of PutClientInServer(), add this:
if (time < 3) self.tracker = spawn();When a client enters or re-enters the world, if it is the beginning of the game, we create him a tracker entity. Now close that and open up tutor.qc. Scroll down to bot_spawn(), and at the end of that routine, add this:
bot.tracker = spawn();Every player and every bot now has his own tracker. As we mentioned before, the tracker will be placed at the teleporter when he walks through one. Interestingly enough, we don't have to do any calculations to accomplish this; we can just utilize the teleporter's touch function. Just wait. Open up triggers.qc and scroll to about the middle. You will see the routine teleport_touch(). In the middle of that, you will see this:
// only teleport living creatures if (other.health <= 0 || other.solid != SOLID_SLIDEBOX) return; SUB_UseTargets ();After that, add this line:
setorigin(other.tracker, other.origin);We're already done with that file. Told you it was interesting. You see, we know the true origin of the teleporter here, because the player -- the other -- is touching it in this routine! Thus we just set hs tracker at his current origin. The tracker now marks the teleporter into which the guy was trying to escape. Get a load of that.
if (!visible(self.enemy)) { hunt_enemy(); return; } setorigin(self.enemy.tracker, self.enemy.origin); self.goalentity = self.enemy;Here is where we activate our chasing method. If the enemy is not visible, then the bot knows to chase him. If he is visible, then the bot actually sets the enemy's tracker entity to the enemy's origin. Thus, the tracker will always be resting in the last seen location of the enemy.
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ void () hunt_enemy = // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ { self.goalentity = self.enemy.tracker; if (vlen(self.origin - self.goalentity.origin) <= 30 && time > self.enemy.teleport_time + 4) setorigin(self.enemy.tracker, self.enemy.origin); if (visible(self.enemy.tracker)) { self.angles_y = vectoyaw(self.enemy.tracker.origin - self.origin); if (walkmove(self.angles_y, 20) == TRUE) { makevectors(self.angles); self.flags = self.flags - (self.flags & FL_ONGROUND); self.velocity = v_forward * self.speed; return; } } movetogoal(20); makevectors(self.angles); self.flags = self.flags - (self.flags & FL_ONGROUND); self.velocity = v_forward * self.speed; };Not too long, not too bad. We know in this routine that the enemy is invisible, so we set the bot's goalentity to the tracker. For the next two lines: if the bot is close to the enemy, or the enemy has not teleported in the last four seconds (in other words, the bot got through the teleporter), then we'll set the tracker at the enemy's actual origin.