]> icculus.org git repositories - divverent/nexuiz.git/blob - qcsrc/gamec/bot_ai.c
additional stuffcmd for player name change
[divverent/nexuiz.git] / qcsrc / gamec / bot_ai.c
1 /***********************************************
2 *                                              *
3 *            FrikBot General AI                *
4 *     "The I'd rather be playing Quake AI"     *
5 *                                              *
6 ***********************************************/
7
8 /*
9
10 This program is in the Public Domain. My crack legal
11 team would like to add:
12
13 RYAN "FRIKAC" SMITH IS PROVIDING THIS SOFTWARE "AS IS"
14 AND MAKES NO WARRANTY, EXPRESS OR IMPLIED, AS TO THE
15 ACCURACY, CAPABILITY, EFFICIENCY, MERCHANTABILITY, OR
16 FUNCTIONING OF THIS SOFTWARE AND/OR DOCUMENTATION. IN
17 NO EVENT WILL RYAN "FRIKAC" SMITH BE LIABLE FOR ANY
18 GENERAL, CONSEQUENTIAL, INDIRECT, INCIDENTAL,
19 EXEMPLARY, OR SPECIAL DAMAGES, EVEN IF RYAN "FRIKAC"
20 SMITH HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
21 DAMAGES, IRRESPECTIVE OF THE CAUSE OF SUCH DAMAGES.
22
23 You accept this software on the condition that you
24 indemnify and hold harmless Ryan "FrikaC" Smith from
25 any and all liability or damages to third parties,
26 including attorney fees, court costs, and other
27 related costs and expenses, arising out of your use
28 of this software irrespective of the cause of said
29 liability.
30
31 The export from the United States or the subsequent
32 reexport of this software is subject to compliance
33 with United States export control and munitions
34 control restrictions. You agree that in the event you
35 seek to export this software, you assume full
36 responsibility for obtaining all necessary export
37 licenses and approvals and for assuring compliance
38 with applicable reexport restrictions.
39
40 Any reproduction of this software must contain
41 this notice in its entirety.
42
43 */
44
45 /*
46 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
47
48 target_onstack
49
50 checks to see if an entity is on the bot's stack
51
52 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
53 */
54
55 float(entity scot) target_onstack =
56 {
57         if (scot == world)
58                 return FALSE;
59         else if (self.target1 == scot)
60                 return 1;
61         else if (self.target2 == scot)
62                 return 2;
63         else if (self.target3 == scot)
64                 return 3;
65         else if (self.target4 == scot)
66                 return 4;
67         else
68                 return FALSE;
69 };
70
71 /*
72 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
73
74 target_add
75
76 adds a new entity to the stack, since it's a
77 LIFO stack, this will be the bot's new target1
78
79 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
80 */
81
82 void(entity ent) target_add =
83 {
84         if (ent == world)
85                 return;
86         if (target_onstack(ent))
87                 return;
88         self.target4 = self.target3;
89         self.target3 = self.target2;
90         self.target2 = self.target1;
91         self.target1 = ent;
92         self.search_time = time + 5;
93 };
94
95
96 /*
97 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
98
99 target_drop
100
101 Removes an entity from the bot's target stack.
102 The stack will empty everything up to the object
103 So if you have target2 item_health, target1
104 waypoint, and you drop the health, the waypoint
105 is gone too.
106
107 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
108 */
109
110 void(entity ent) target_drop =
111 {
112         local float tg;
113
114         tg = target_onstack(ent);
115         if (tg == 1)
116         {
117                 self.target1 = self.target2;
118                 self.target2 = self.target3;
119                 self.target3 = self.target4;
120                 self.target4 = world;
121         }
122         else if (tg == 2)
123         {
124                 self.target1 = self.target3;
125                 self.target2 = self.target4;
126                 self.target3 = self.target4 = world;
127         }
128         else if (tg == 3)
129         {
130                 self.target1 = self.target4;
131                 self.target2 = self.target3 = self.target4 = world;
132         }
133         else if (tg == 4)
134                 self.target1 = self.target2 = self.target3 = self.target4 = world;
135         self.search_time = time + 5;
136 };
137
138 /*
139 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
140
141 bot_lost
142
143 Bot has lost its target.
144
145 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
146 */
147
148 void(entity targ, float success) bot_lost =
149 {
150         if (!targ)
151                 return;
152
153         target_drop(targ);
154         if (targ.classname == "waypoint")
155                 targ.b_sound = targ.b_sound - (targ.b_sound & ClientBitFlag(self.b_clientno));
156
157         // find a new route
158         if (!success)
159         {
160                 self.target1 = self.target2 = self.target3 = self.target4 = world;
161                 self.last_way = FindWayPoint(self.current_way);
162                 ClearMyRoute();
163                 self.b_aiflags = 0;
164         }
165         else
166         {
167                 if (targ.classname == "item_artifact_invisibility")
168                         if (self.items & 524288)
169                                 bot_start_topic(3);
170
171                 if (targ.flags & FL_ITEM)
172                 {
173                         if (targ.model == string_null)
174                                 targ._last = world;
175                         else
176                                 targ._last = self;
177                 }
178         }
179
180
181         if (targ.classname != "player")
182                 targ.search_time = time + 5;
183 };
184
185 /*
186 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
187
188 bot_check_lost
189
190 decide if my most immediate target should be
191 removed.
192
193 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
194 */
195 void(entity targ) bot_check_lost =
196 {
197         local vector dist;
198         dist = realorigin(targ) - self.origin;
199         dist_z = 0;
200         if (targ == world)
201                 return;
202
203         // waypoints and items are lost if you get close enough to them
204
205         else if (targ.flags & FL_ITEM)
206         {
207                 if (vlen(targ.origin - self.origin) < 32)
208                         bot_lost(targ, TRUE);
209                 else if (targ.model == string_null)
210                         bot_lost(targ, TRUE);
211         }
212         else if (targ.classname == "waypoint")
213         {
214                 if (!(self.b_aiflags & (AI_SNIPER | AI_AMBUSH)))
215                 {
216                         if (self.b_aiflags & AI_RIDE_TRAIN)
217                         {
218                                 if (vlen(targ.origin - self.origin) < 48)
219                                         bot_lost(targ, TRUE);
220                         }
221                         else if (self.b_aiflags & AI_PRECISION)
222                         {
223                                 if (vlen(targ.origin - self.origin) < 24)
224                                         bot_lost(targ, TRUE);
225                         }
226                         else if (vlen(targ.origin - self.origin) < 32)
227                                 bot_lost(targ, TRUE);
228                         else if (self.b_aiflags & AI_CARELESS) // Electro - better for jumppads
229                         {
230                                 if (vlen(targ.origin - self.origin) < 128)
231                                         bot_lost(targ, TRUE);
232                         }
233                 }
234         }
235         else if (targ.classname == "temp_waypoint")
236         {
237                 if (vlen(targ.origin - self.origin) < 32)
238                         bot_lost(targ, TRUE);
239         }
240         else if (targ.classname == "player")
241         {
242                 if (targ.health <= 0)
243                         bot_lost(targ, TRUE);
244                 else if ((coop) || (teamplay && targ.team == self.team))
245                 {
246                         if (targ.target1.classname == "player")
247                         {
248                                 if (!targ.target1.ishuman)
249                                         bot_lost(targ, TRUE);
250                         }
251                         else if (targ.teleport_time > time)
252                         {
253                                 // try not to telefrag teammates
254                                 self.keys = self.keys & 960;
255                         }
256                         else if (vlen(targ.origin - self.origin) < 128)
257                         {
258                                 if (vlen(targ.origin - self.origin) < 48)
259                                         frik_walkmove(self.origin - targ.origin);
260                                 else
261                                 {
262                                         self.keys = self.keys & 960;
263                                         bot_start_topic(4);
264                                 }
265                                 self.search_time = time + 5; // never time out
266                         }
267                         else if (!fisible(targ))
268                                 bot_lost(targ, FALSE);
269                 }
270                 else if (waypoint_mode > WM_LOADED)
271                 {
272                         if (vlen(targ.origin - self.origin) < 128)
273                         {
274                                 bot_lost(targ, TRUE);
275                         }
276                 }
277         }
278
279         // buttons are lost of their frame changes
280         else if (targ.classname == "func_button")
281         {
282                 if (targ.frame)
283                 {
284                         bot_lost(targ, TRUE);
285                         if (self.enemy == targ)
286                                 self.enemy = world;
287                         //if (self.target1)
288                         //      bot_get_path(self.target1, TRUE);
289
290                 }
291         }
292         // trigger_multiple style triggers are lost if their thinktime changes
293         else if ((targ.movetype == MOVETYPE_NONE) && (targ.solid == SOLID_TRIGGER))
294         {
295                 if (targ.nextthink >= time)
296                 {
297                         bot_lost(targ, TRUE);
298                         //if (self.target1)
299                         //      bot_get_path(self.target1, TRUE);
300                 }
301         }
302         // lose any target way above the bot's head
303         // FIXME: if the bot can fly in your mod..
304         if ((targ.origin_z - self.origin_z) > 64)
305         {
306                 dist = targ.origin - self.origin;
307                 dist_z = 0;
308                 if (vlen(dist) < 32)
309                         if (self.flags & FL_ONGROUND)
310                                 if(!frik_recognize_plat(FALSE))
311                                         bot_lost(targ, FALSE);
312         }
313         else if (targ.classname == "train")
314         {
315                 if (frik_recognize_plat(FALSE))
316                         bot_lost(targ, TRUE);
317         }
318         // targets are lost if the bot's search time has expired
319         if (time > self.search_time)
320                 bot_lost(targ, FALSE);
321 };
322
323
324 /*
325 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
326
327 bot_handle_ai
328
329 This is a 0.10 addition. Handles any action
330 based b_aiflags.
331
332 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
333 */
334
335 void() bot_handle_ai =
336 {
337         local entity newt;
338         local vector v;
339
340         // handle ai flags -- note, not all aiflags are handled
341         // here, just those that perform some sort of action
342
343         // wait is used by the ai to stop the bot until his search time expires / or route changes
344
345         if (self.b_aiflags & AI_WAIT)
346                 self.keys = self.keys & 960;
347
348         if (self.b_aiflags & AI_DOORFLAG) // was on a door when spawned
349         {
350                 b_temp3 = self;
351                 self = self.last_way;
352                 if (!frik_recognize_plat(FALSE)) // if there is nothing there now
353                 {
354                         newt = FindThing("door"); // this is likely the door responsible (crossfingers)
355                         self = b_temp3;
356
357                         if (self.b_aiflags & AI_DOOR_NO_OPEN)
358                         {
359                                 if (newt.nextthink)
360                                         self.keys = self.keys & 960; // wait until it closes
361                                 else
362                                 {
363                                         bot_lost(self.last_way, FALSE);
364                                 }
365                         }
366                         else
367                         {
368                                 if (newt.targetname)
369                                 {
370                                         newt = find(world, target, newt.targetname);
371                                         if (newt.health > 0)
372                                         {
373                                                 self.enemy = newt;
374                                                 bot_weapon_switch(1);
375                                         }
376                                         else
377                                         {
378                                         //      target_drop(self.last_way);
379                                                 target_add(newt);
380                                         //      bot_get_path(newt, TRUE);
381                                         }
382                                 }
383                                 self.b_aiflags = self.b_aiflags - AI_DOORFLAG;
384                         }
385                 }
386                 else
387                         self = b_temp3;
388         }
389
390         if (self.b_aiflags & AI_JUMP)
391         {
392                 if (self.flags & FL_ONGROUND)
393                 {
394                         bot_jump();
395                         self.b_aiflags = self.b_aiflags - AI_JUMP;
396                 }
397         }
398         else if (self.b_aiflags & AI_SUPER_JUMP)
399         {
400                 if (self.weapon != WEP_LASER)
401                         self.impulse = 7;
402                 else if (self.flags & FL_ONGROUND)
403                 {
404                         self.b_aiflags = self.b_aiflags - AI_SUPER_JUMP;
405                         if (bot_can_rj(self))
406                         {
407                                 bot_jump();
408                                 self.v_angle_x = self.b_angle_x = 80;
409                                 self.button0 = TRUE;
410                         }
411                         else
412                                 bot_lost(self.target1, FALSE);
413
414                 }
415         }
416         if (self.b_aiflags & AI_SURFACE)
417         {
418                 if (self.waterlevel > 2)
419                 {
420                         self.keys = KEY_MOVEUP;
421                         self.button2 = TRUE; // swim!
422                 }
423                 else
424                         self.b_aiflags = self.b_aiflags - AI_SURFACE;
425         }
426         if (self.b_aiflags & AI_RIDE_TRAIN)
427         {
428                 // simple, but effective
429                 // this can probably be used for a lot of different
430                 // things, not just trains (door elevators come to mind)
431                 b_temp3 = self;
432                 self = self.last_way;
433
434                 if (!frik_recognize_plat(FALSE)) // if there is nothing there now
435                 {
436                         self = b_temp3;
437                         self.keys = self.keys & 960;
438                 }
439                 else
440                 {
441                         self = b_temp3;
442                         if (frik_recognize_plat(FALSE))
443                         {
444                                 v = realorigin(trace_ent) + trace_ent.origin - self.origin;
445                                 v_z = 0;
446                                 if (vlen(v) < 24)
447                                         self.keys = self.keys & 960;
448                                 else
449                                 {
450                                         self.b_aiflags = self.b_aiflags | AI_PRECISION;
451                                         self.keys = frik_KeysForDir(v);
452                                 }
453                         }
454                 }
455         }
456         if (self.b_aiflags & AI_PLAT_BOTTOM)
457         {
458                 newt = FindThing("plat");
459                 if (newt.state != 1)
460                 {
461                         v =  self.origin - realorigin(newt);
462                         v_z = 0;
463                         if (vlen(v) > 96)
464                                 self.keys = self.keys & 960;
465                         else
466                                 frik_walkmove(v);
467                 }
468                 else
469                         self.b_aiflags = self.b_aiflags - AI_PLAT_BOTTOM;
470         }
471         if (self.b_aiflags & AI_DIRECTIONAL)
472         {
473                 if ((normalize(self.last_way.origin - self.origin) * self.b_dir) > 0.4)
474                 {
475                         self.b_aiflags = self.b_aiflags - AI_DIRECTIONAL;
476                         bot_lost(self.target1, TRUE);
477                 }
478         }
479         if (self.b_aiflags & AI_SNIPER)
480         {
481                 self.b_aiflags = (self.b_aiflags | AI_WAIT | AI_PRECISION) - AI_SNIPER;
482                 // FIXME: Add a switch to wep command
483                 // FIXME: increase delay?
484         }
485         if (self.b_aiflags & AI_AMBUSH)
486         {
487                 self.b_aiflags = (self.b_aiflags | AI_WAIT) - AI_AMBUSH;
488                 // FIXME: Add a switch to wep command
489                 // FIXME: increase delay?
490         }
491
492 };
493
494 /*
495 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
496
497 bot_path
498
499 Bot will follow a route generated by the
500 begin_route set of functions in bot_way.qc.
501 This code, while it works pretty well, can get
502 confused
503
504 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
505 */
506
507 void() bot_path =
508 {
509         local entity jj, tele;
510
511         bot_check_lost(self.target1);
512         if (!self.target1)
513         {
514                 self.keys=0;
515                 return;
516         }
517         if (target_onstack(self.last_way))
518                 return; // old waypoint still being hunted
519
520         jj = FindRoute(self.last_way);
521         if (!jj)
522         {
523                 // this is an ugly hack
524                 if (self.target1.current_way != self.last_way)
525                 {
526                         if (self.target1.classname != "temp_waypoint")
527                                 if (self.target1.classname != "player")
528                                         bot_lost(self.target1, FALSE);
529                 }
530
531                 return;
532         }
533
534         // update the bot's special ai features
535
536         // Readahed types are AI conditions to perform while heading to a waypoint
537         // point types are AI flags that should be executed once reaching a waypoint
538
539         self.b_aiflags = (jj.b_aiflags & AI_READAHEAD_TYPES) | (self.last_way.b_aiflags & AI_POINT_TYPES);
540         target_add(jj);
541         if (self.last_way)
542         {
543                 if (CheckLinked(self.last_way, jj) == 2) // waypoints are telelinked
544                 {
545                         tele = FindThing("trigger_teleport"); // this is probbly the teleport responsible
546                         target_add(tele);
547                 }
548                 traceline(self.last_way.origin, jj.origin, FALSE, self); // check for blockage
549                 if (trace_fraction != 1)
550                 {
551                         if (trace_ent.classname == "door" && !(self.b_aiflags & AI_DOOR_NO_OPEN)) // a door blocks the way
552                         {
553                                 // linked doors fix
554                                 if (trace_ent.owner)
555                                         trace_ent = trace_ent.owner;
556                                 if ((trace_ent.health > 0) && (self.enemy == world))
557                                 {
558                                         self.enemy = trace_ent;
559                                         bot_weapon_switch(1);
560                                         self.b_aiflags = self.b_aiflags | AI_BLIND; // nick knack paddy hack
561                                 }
562                                 else if (trace_ent.targetname)
563                                 {
564                                         tele = find(world, target, trace_ent.targetname);
565                                         if (tele.health > 0)
566                                         {
567                                                 self.enemy = tele;
568                                                 bot_weapon_switch(1);
569                                         }
570                                         else
571                                         {
572                                         //      target_drop(jj);
573                                                 target_add(tele);
574                                         //      bot_get_path(tele, TRUE);
575                                                 self.b_aiflags = self.b_aiflags | AI_BLIND; // give a bot a bone
576                                                 return;
577                                         }
578                                 }
579                         }
580                         else if (trace_ent.classname == "func_wall")
581                         {
582                                 // give up
583                                 bot_lost(self.target1, FALSE);
584                                 return;
585                         }
586                 }
587         }
588         // this is used for AI_DRIECTIONAL
589         self.b_dir = normalize(jj.origin - self.last_way.origin);
590
591         self.last_way = jj;
592 };
593
594
595 /*
596 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
597
598 Bot Priority Look. What a stupid name. This is where
599 the bot finds things it wants to kill/grab.
600
601 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
602 */
603 // priority scale
604 // 0 - 10 virtually ignore
605 // 10 - 30 normal item range
606 // 30 - 50 bot will consider this a target worth changing course for
607 // 50 - 90 bot will hunt these as vital items
608
609 // *!* Make sure you add code to bot_check_lost to remove the target *!*
610
611 float(entity thing) priority_for_thing =
612 {
613         local float thisp;
614         thisp = 0;
615         // This is the most executed function in the bot. Careful what you do here.
616
617         if (thing.flags & FL_ITEM && thing.model != string_null && thing.search_time < time)
618         {
619                 // ugly hack
620                 if (thing._last != self)
621                         thisp = 20;
622                 else if (thing.classname == "item_strength") // IT_STRENGTH
623                         thisp = 65;
624                 else if (thing.classname == "item_invincible") // IT_INVINCIBLE
625                         thisp = 65;
626                 //else if (thing.classname == "item_speed") // IT_SPEED
627                 //      thisp = 65;
628                 //else if (thing.classname == "item_slowmo") // IT_SLOWMO
629                 //      thisp = 65;
630                 else if (thing.classname == "item_health")
631                 {
632                         if (thing.spawnflags & 2)
633                                 thisp = 55;
634                         if (self.health < 40)
635                                 thisp = thisp + 50;
636                 }
637                 else if (thing.classname == "item_health1" || thing.classname == "item_health2")
638                 {
639                         thisp = 20;
640                         if (self.health < 40)
641                                 thisp = thisp + 50;
642                 }
643                 else if (thing.classname == "item_health100")
644                 {
645                         thisp = 55;
646                         if (self.health < 40)
647                                 thisp = thisp + 50;
648                 }
649                 else if (thing.classname == "item_armor1")
650                 {
651                         thisp = 20;
652                         if (self.armorvalue < 100)
653                                 thisp = thisp + 25;
654                 }
655                 else if (thing.classname == "item_armor25")
656                 {
657                         thisp = 60;
658                         if (self.armorvalue < 100)
659                                 thisp = thisp + 25;
660                 }
661                 else if (thing.classname == "weapon_shotgun")
662                 {
663                         if (!(self.items & IT_SHOTGUN))
664                                 thisp = 25;
665                 }
666                 else if (thing.classname == "weapon_uzi")
667                 {
668                         if (!(self.items & IT_UZI))
669                                 thisp = 30;
670                 }
671                 else if (thing.classname == "weapon_electro")
672                 {
673                         if (!(self.items & IT_ELECTRO))
674                                 thisp = 35;
675                 }
676                 else if (thing.classname == "weapon_grenadelauncher")
677                 {
678                         if (!(self.items & IT_GRENADE_LAUNCHER))
679                                 thisp = 45;
680                 }
681                 else if (thing.classname == "weapon_crylink")
682                 {
683                         if (!(self.items & IT_CRYLINK))
684                                 thisp = 60;
685                 }
686                 else if (thing.classname == "weapon_nex")
687                 {
688                         if (!(self.items & IT_NEX)) // IT_LIGHTNING
689                                 thisp = 50;
690                 }
691                 else if (thing.classname == "weapon_hagar")
692                 {
693                         if (!(self.items & IT_HAGAR))
694                                 thisp = 45;
695                 }
696                 else if (thing.classname == "weapon_rocketlauncher")
697                 {
698                         if (!(self.items & IT_ROCKET_LAUNCHER))
699                                 thisp = 50;
700                 }
701         }
702         else if (thing.classname == "player")
703         {
704                 if (thing.health > 0)
705                 {
706                         if (thing == self)
707                                 return 0;
708                         else
709                         {
710                                 if (coop)
711                                 {
712                                         thisp = 100;
713                                         if (thing.target1.classname == "player")
714                                                 if (!thing.target1.ishuman)
715                                                         return 0;
716                                 }
717                                 else if (teamplay && thing.team == self.team)
718                                 {
719                                         thisp = 100;
720                                         if (thing.target1.classname == "player")
721                                                 return 0;
722                                 }
723                                 else thisp = 30;
724                         }
725                 }
726         }
727         else if (thing.classname == "waypoint")
728         {
729                 if (thing.b_aiflags & AI_SNIPER)
730                         thisp = 30;
731                 else if (thing.b_aiflags & AI_AMBUSH)
732                         thisp = 30;
733         }
734         if (pointcontents(thing.origin) < -3)
735                 return 0;
736         if (thisp)
737         {
738                 if (thing.current_way)
739                 {
740                         // check to see if it's unreachable
741                         if (thing.current_way.items == -1)
742                                 return 0;
743                         else
744                                 thisp = thisp + (13000 - thing.current_way.items) * 0.05;
745
746                 }
747         }
748         return thisp;
749 };
750
751 void(float scope) bot_look_for_crap =
752 {
753         local entity foe, best;
754         local float thatp, bestp, dist;
755
756         if (scope == 1)
757                 foe = findradius(self.origin, 13000);
758         else
759                 foe = findradius(self.origin, 500);
760
761         bestp = 1;
762         while(foe)
763         {
764                 thatp = priority_for_thing(foe);
765                 if (thatp)
766                         if (!scope)
767                                 if (!sisible(foe))
768                                         thatp = 0;
769                 if (thatp > bestp)
770                 {
771                         bestp = thatp;
772                         best = foe;
773                         dist = vlen(self.origin - foe.origin);
774                 }
775                 foe = foe.chain;
776         }
777         if (best == world)
778                 return;
779         if (!target_onstack(best))
780         {
781                 target_add(best);
782                 if (scope)
783                 {
784                         bot_get_path(best, FALSE);
785                         self.b_aiflags = self.b_aiflags | AI_WAIT;
786                 }
787         }
788 };
789
790
791 /*
792 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
793
794 bot_angle_set
795
796 Sets the bots look keys & b_angle to point at
797 the target - used for fighting and just
798 generally making the bot look good.
799
800 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
801 */
802
803 void() bot_angle_set =
804 {
805         local float h;
806         local vector view;
807
808         if (self.enemy)
809         {
810                 if (self.enemy.items & 524288)
811                         if (random() > 0.2)
812                                 return;
813                 if (self.missile_speed == 0)
814                         self.missile_speed = 10000;
815                 if (self.enemy.solid == SOLID_BSP)
816                 {
817                         view = (((self.enemy.absmin + self.enemy.absmax) * 0.5) - self.origin);
818                 }
819                 else
820                 {
821                         h = vlen(self.enemy.origin - self.origin) / self.missile_speed;
822                         if (self.enemy.flags & FL_ONGROUND)
823                                 view = self.enemy.velocity * h;
824                         else
825                                 view = (self.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
826                         view = self.enemy.origin + view;
827                         // FIXME: ?
828                         traceline(self.enemy.origin, view, FALSE, self);
829                         view = trace_endpos;
830
831                         if (self.weapon == WEP_GRENADE_LAUNCHER || self.weapon == WEP_ELECTRO || self.weapon == WEP_HAGAR || self.weapon == WEP_ROCKET_LAUNCHER)
832                                 view = view - '0 0 22';
833
834                         view = normalize(view - self.origin);
835                 }
836                 view = vectoangles(view);
837                 view_x = view_x * -1;
838                 self.b_angle = view;
839         }
840         else if (self.target1)
841         {
842                 view = realorigin(self.target1);
843                 if (self.target1.flags & FL_ITEM)
844                         view = view + '0 0 48';
845                 view = view - (self.origin + self.view_ofs);
846                 view = vectoangles(view);
847                 view_x = view_x * -1;
848                 self.b_angle = view;
849         }
850         else
851                 self.b_angle_x = 0;
852         // HACK HACK HACK HACK
853         // The bot falls off ledges a lot because of "turning around"
854         // so let the bot use instant turn around when not hunting a player
855         if (self.b_skill == 3)
856         {
857                 self.keys = self.keys & 63;
858                 self.v_angle = self.b_angle;
859                 while (self.v_angle_x < -180)
860                         self.v_angle_x = self.v_angle_x + 360;
861                 while (self.v_angle_x > 180)
862                         self.v_angle_x = self.v_angle_x - 360;
863
864         }
865         else if ((self.enemy == world || self.enemy.movetype == MOVETYPE_PUSH) && self.target1.classname != "player")
866         {
867                 self.keys = self.keys & 63;
868                 self.v_angle = self.b_angle;
869                 while (self.v_angle_x < -180)
870                         self.v_angle_x = self.v_angle_x + 360;
871                 while (self.v_angle_x > 180)
872                         self.v_angle_x = self.v_angle_x - 360;
873         }
874         else if (self.b_skill < 2) // skill 2 handled in bot_phys
875         {
876                 if (self.b_angle_x > 180)
877                         self.b_angle_x = self.b_angle_x - 360;
878                 self.keys = self.keys & 63;
879
880                 if (angcomp(self.b_angle_y, self.v_angle_y) > 10)
881                         self.keys = self.keys | KEY_LOOKLEFT;
882                 else if (angcomp(self.b_angle_y, self.v_angle_y) < -10)
883                         self.keys = self.keys | KEY_LOOKRIGHT;
884                 if (angcomp(self.b_angle_x, self.v_angle_x) < -10)
885                         self.keys = self.keys | KEY_LOOKUP;
886                 else if (angcomp(self.b_angle_x, self.v_angle_x) > 10)
887                         self.keys = self.keys | KEY_LOOKDOWN;
888         }
889 };
890
891 /*
892 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
893
894 BotAI
895
896 This is the main ai loop. Though called every
897 frame, the ai_time limits it's actual updating
898
899 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
900 */
901 float stagger_think;
902 float intermission_running;
903
904 void() BotAI =
905 {
906         // am I dead? Fire randomly until I respawn
907         // health < 1 is used because fractional healths show up as 0 on normal player
908         // status bars, and the mod probably already compensated for that
909
910         if (self.health < 1)
911         {
912                 self.button0 = floor(random() * 2);
913                 self.button2 = 0;
914                 self.keys = 0;
915                 self.b_aiflags = 0;
916                 ClearMyRoute();
917                 self.target1 = self.target2 = self.target3 = self.target4 = self.enemy = world;
918                 self.last_way = world;
919                 return;
920         }
921
922         // stagger the bot's AI out so they all don't think at the same time, causing game
923         // 'spikes'
924         if (self.b_skill < 2)
925         {
926                 if (self.ai_time > time)
927                         return;
928
929                 self.ai_time = time + 0.05;
930                 if (bot_count > 0)
931                 {
932                         if ((time - stagger_think) < (0.1 / bot_count))
933                                 self.ai_time = self.ai_time + 0.1 / (2 * bot_count);
934                 }
935                 else
936                         return;
937         }
938         if (intermission_running)
939                 bot_start_topic(7);
940         stagger_think = time;
941
942         // shut the bot's buttons off, various functions will turn them on by AI end
943
944         self.button2 = 0;
945         self.button0 = 0;
946
947
948         // target1 is like goalentity in normal Quake monster AI.
949         // it's the bot's most immediate target
950         if (route_table == self)
951         {
952                 if (busy_waypoints <= 0)
953                 {
954                         if (waypoint_mode < WM_EDITOR)
955                                 bot_look_for_crap(TRUE);
956                 }
957                 self.b_aiflags = 0;
958                 self.keys = 0;
959         }
960         else if (self.target1)
961         {
962                 frik_movetogoal();
963                 bot_path();
964         }
965         else
966         {
967                 if (waypoint_mode < WM_EDITOR)
968                 {
969                         if(self.route_failed)
970                         {
971                                 frik_bot_roam();
972                                 self.route_failed = 0;
973                         }
974                         else if(!begin_route())
975                         {
976                                 bot_look_for_crap(FALSE);
977                         }
978                         self.keys = 0;
979                 }
980                 else
981                 {
982                         self.b_aiflags = AI_WAIT;
983                         self.keys = 0;
984                 }
985         }
986
987         // bot_angle_set points the bot at it's goal (self.enemy or target1)
988
989         bot_angle_set();
990
991         // fight my enemy. Enemy is probably a field QC coders will most likely use a lot
992         // for their own needs, since it's unused on a normal player
993         // FIXME
994         if (self.enemy)
995                 bot_fight_style();
996         else if (random() < 0.2)
997                 if (random() < 0.2)
998                         bot_weapon_switch(-1);
999         bot_dodge_stuff();
1000
1001         // checks to see if bot needs to start going up for air
1002 /*      if (self.waterlevel > 2)
1003         {
1004                 if (time > (self.air_finished - 2))
1005                 {
1006                         traceline (self.origin, self.origin + '0 0 6800', TRUE, self);
1007                         if (trace_inopen)
1008                         {
1009                                 self.keys = KEY_MOVEUP;
1010                                 self.button2 = TRUE; // swim!
1011                                 return; // skip ai flags for now - this is life or death
1012                         }
1013                 }
1014         }
1015 */
1016         // b_aiflags handling
1017
1018
1019         if (self.b_aiflags)
1020                 bot_handle_ai();
1021         //else
1022         //      bot_chat(); // don't want chat to screw him up if he's rjing or something
1023 };