]> icculus.org git repositories - divverent/nexuiz.git/blob - data/qcsrc/bot/bot_ai.qc
commit 1.2.1 game media
[divverent/nexuiz.git] / data / qcsrc / bot / bot_ai.qc
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 != 32)
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
510         local entity jj, tele;
511         //local vector org;
512
513         bot_check_lost(self.target1);
514         if (!self.target1)
515         {
516                 self.keys=0;
517                 return;
518         }
519         if (target_onstack(self.last_way))
520                 return; // old waypoint still being hunted
521
522         jj = FindRoute(self.last_way);
523         if (!jj)
524         {
525                 // this is an ugly hack
526                 if (self.target1.current_way != self.last_way)
527                 {
528                         if (self.target1.classname != "temp_waypoint")
529                                 if (self.target1.classname != "player")
530                                         bot_lost(self.target1, FALSE);
531                 }
532
533                 return;
534         }
535
536         // update the bot's special ai features
537
538         // Readahed types are AI conditions to perform while heading to a waypoint
539         // point types are AI flags that should be executed once reaching a waypoint
540
541         self.b_aiflags = (jj.b_aiflags & AI_READAHEAD_TYPES) | (self.last_way.b_aiflags & AI_POINT_TYPES);
542         target_add(jj);
543         if (self.last_way)
544         {
545                 if (CheckLinked(self.last_way, jj) == 2) // waypoints are telelinked
546                 {
547                         tele = FindThing("trigger_teleport"); // this is probbly the teleport responsible
548                         target_add(tele);
549                 }
550                 traceline(self.last_way.origin, jj.origin, FALSE, self); // check for blockage
551                 if (trace_fraction != 1)
552                 {
553                         if (trace_ent.classname == "door" && !(self.b_aiflags & AI_DOOR_NO_OPEN)) // a door blocks the way
554                         {
555                                 // linked doors fix
556                                 if (trace_ent.owner)
557                                         trace_ent = trace_ent.owner;
558                                 if ((trace_ent.health > 0) && (self.enemy == world))
559                                 {
560                                         self.enemy = trace_ent;
561                                         bot_weapon_switch(1);
562                                         self.b_aiflags = self.b_aiflags | AI_BLIND; // nick knack paddy hack
563                                 }
564                                 else if (trace_ent.targetname)
565                                 {
566                                         tele = find(world, target, trace_ent.targetname);
567                                         if (tele.health > 0) 
568                                         {
569                                                 self.enemy = tele;
570                                                 bot_weapon_switch(1);
571                                         }
572                                         else
573                                         {
574                                         //      target_drop(jj);
575                                                 target_add(tele);
576                                         //      bot_get_path(tele, TRUE);
577                                                 self.b_aiflags = self.b_aiflags | AI_BLIND; // give a bot a bone        
578                                                 return;
579                                         }                                       
580                                 }               
581                         }
582                         else if (trace_ent.classname == "func_wall")
583                         {
584                                 // give up
585                                 bot_lost(self.target1, FALSE);
586                                 return;
587                         }
588                 }
589         }
590         // this is used for AI_DRIECTIONAL
591         self.b_dir = normalize(jj.origin - self.last_way.origin);
592
593         self.last_way = jj;
594 };
595
596
597 /*
598 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
599
600 Bot Priority Look. What a stupid name. This is where
601 the bot finds things it wants to kill/grab.
602
603 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
604 */
605 // priority scale
606 // 0 - 10 virtually ignore
607 // 10 - 30 normal item range
608 // 30 - 50 bot will consider this a target worth changing course for
609 // 50 - 90 bot will hunt these as vital items
610
611 // *!* Make sure you add code to bot_check_lost to remove the target *!*
612
613 float(entity thing) priority_for_thing =
614 {
615         local float thisp;
616         thisp = 0;
617         // This is the most executed function in the bot. Careful what you do here.
618
619         if (thing.flags & FL_ITEM && thing.model != string_null && thing.search_time < time)
620         {
621                 // ugly hack
622                 if (thing._last != self)
623                         thisp = 20;
624                 else if (thing.classname == "item_strength") // IT_STRENGTH
625                         thisp = 65;
626                 else if (thing.classname == "item_invincible") // IT_INVINCIBLE
627                         thisp = 65;
628                 else if (thing.classname == "item_speed") // IT_SPEED
629                         thisp = 65;
630                 else if (thing.classname == "item_slowmo") // IT_SLOWMO
631                         thisp = 65;
632                 else if (thing.classname == "item_health")
633                 {
634                         if (thing.spawnflags & 2)
635                                 thisp = 55;
636                         if (self.health < 40)
637                                 thisp = thisp + 50;
638                 }
639                 else if (thing.model == "progs/armor.mdl")
640                 {
641                         if (self.armorvalue < 200)
642                         {
643                                 if (thing.skin == 2)
644                                         thisp = 60;
645                                 else if (self.armorvalue < 100)                 
646                                         thisp = thisp + 25;
647                         }
648                 }
649                 else if (thing.classname == "weapon_shotgun")
650                 {
651                         if (!(self.items & IT_SHOTGUN))
652                                 thisp = 25;
653                 }
654                 else if (thing.classname == "weapon_uzi")
655                 {
656                         if (!(self.items & IT_UZI))
657                                 thisp = 30;
658                 }
659                 else if (thing.classname == "weapon_electro")
660                 {
661                         if (!(self.items & IT_ELECTRO))
662                                 thisp = 35;
663                 }
664                 else if (thing.classname == "weapon_grenadelauncher")
665                 {
666                         if (!(self.items & IT_GRENADE_LAUNCHER))
667                                 thisp = 45;
668                 }
669                 else if (thing.classname == "weapon_crylink")
670                 {
671                         if (!(self.items & IT_CRYLINK))
672                                 thisp = 60;
673                 }
674                 else if (thing.classname == "weapon_nex")
675                 {
676                         if (!(self.items & IT_NEX)) // IT_LIGHTNING
677                                 thisp = 50;
678                 }
679                 else if (thing.classname == "weapon_hagar")
680                 {
681                         if (!(self.items & IT_HAGAR))
682                                 thisp = 45;
683                 }
684                 else if (thing.classname == "weapon_rocketlauncher")
685                 {
686                         if (!(self.items & IT_ROCKET_LAUNCHER))
687                                 thisp = 50;
688                 }
689         }
690         else if (thing.classname == "player")
691         { 
692                 if (thing.health > 0)
693                 {
694                         if (thing == self)
695                                 return 0;
696                         else
697                         {
698                                 if (coop)
699                                 {
700                                         thisp = 100;
701                                         if (thing.target1.classname == "player")
702                                                 if (!thing.target1.ishuman)
703                                                         return 0;
704                                 }
705                                 else if (teamplay && thing.team == self.team)
706                                 {
707                                         thisp = 100;
708                                         if (thing.target1.classname == "player")
709                                                 return 0;
710                                 }
711                                 else thisp = 30;
712                         }
713                 }
714         }
715         else if (thing.classname == "waypoint")
716         {
717                 if (thing.b_aiflags & AI_SNIPER)
718                         thisp = 30;
719                 else if (thing.b_aiflags & AI_AMBUSH)
720                         thisp = 30;
721         }
722         if (pointcontents(thing.origin) < -3)
723                 return 0;
724         if (thisp)
725         {
726                 if (thing.current_way)
727                 {
728                         // check to see if it's unreachable
729                         if (thing.current_way.items == -1)
730                                 return 0;
731                         else
732                                 thisp = thisp + (13000 - thing.current_way.items) * 0.05; 
733
734                 }
735         }               
736         return thisp;
737 };
738
739 void(float scope) bot_look_for_crap =
740 {
741         local entity foe, best;
742         local float thatp, bestp, dist;
743
744         if (scope == 1)
745                 foe = findradius(self.origin, 13000);
746         else
747                 foe = findradius(self.origin, 500);
748
749         bestp = 1;
750         while(foe)
751         {
752                 thatp = priority_for_thing(foe);
753                 if (thatp)
754                         if (!scope)
755                                 if (!sisible(foe))
756                                         thatp = 0;
757                 if (thatp > bestp)
758                 {
759                         bestp = thatp;
760                         best = foe;
761                         dist = vlen(self.origin - foe.origin);
762                 }
763                 foe = foe.chain;
764         }
765         if (best == world)
766                 return;
767         if (!target_onstack(best))
768         {       
769                 target_add(best);
770                 if (scope)
771                 {
772                         bot_get_path(best, FALSE);
773                         self.b_aiflags = self.b_aiflags | AI_WAIT;
774                 }
775         }
776 };
777
778
779 /*
780 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
781
782 bot_angle_set
783
784 Sets the bots look keys & b_angle to point at
785 the target - used for fighting and just
786 generally making the bot look good.
787
788 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
789 */
790
791 void() bot_angle_set =
792 {
793         local float h;
794         local vector view;
795         
796         if (self.enemy)
797         {
798                 if (self.enemy.items & 524288)
799                         if (random() > 0.2)
800                                 return;
801                 if (self.missile_speed == 0)
802                         self.missile_speed = 10000;
803                 if (self.enemy.solid == SOLID_BSP)
804                 {
805                         view = (((self.enemy.absmin + self.enemy.absmax) * 0.5) - self.origin);
806                 }
807                 else
808                 {
809                         h = vlen(self.enemy.origin - self.origin) / self.missile_speed;
810                         if (self.enemy.flags & FL_ONGROUND)
811                                 view = self.enemy.velocity * h;
812                         else
813                                 view = (self.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
814                         view = self.enemy.origin + view;  
815                         // FIXME: ?
816                         traceline(self.enemy.origin, view, FALSE, self);
817                         view = trace_endpos;
818                         
819                         if (self.weapon == 32) 
820                                 view = view - '0 0 22';
821
822                         view = normalize(view - self.origin);
823                 }
824                 view = vectoangles(view);
825                 view_x = view_x * -1;
826                 self.b_angle = view;
827         }
828         else if (self.target1)
829         {
830                 view = realorigin(self.target1);
831                 if (self.target1.flags & FL_ITEM)
832                         view = view + '0 0 48';
833                 view = view - (self.origin + self.view_ofs);
834                 view = vectoangles(view);
835                 view_x = view_x * -1;
836                 self.b_angle = view;
837         }
838         else
839                 self.b_angle_x = 0;
840         // HACK HACK HACK HACK
841         // The bot falls off ledges a lot because of "turning around"
842         // so let the bot use instant turn around when not hunting a player
843         if (self.b_skill == 3)
844         {
845                 self.keys = self.keys & 63;
846                 self.v_angle = self.b_angle;
847                 while (self.v_angle_x < -180)
848                         self.v_angle_x = self.v_angle_x + 360;
849                 while (self.v_angle_x > 180)
850                         self.v_angle_x = self.v_angle_x - 360;
851
852         }
853         else if ((self.enemy == world || self.enemy.movetype == MOVETYPE_PUSH) && self.target1.classname != "player")
854         {
855                 self.keys = self.keys & 63;
856                 self.v_angle = self.b_angle;
857                 while (self.v_angle_x < -180)
858                         self.v_angle_x = self.v_angle_x + 360;
859                 while (self.v_angle_x > 180)
860                         self.v_angle_x = self.v_angle_x - 360;
861         }
862         else if (self.b_skill < 2) // skill 2 handled in bot_phys
863         {
864                 if (self.b_angle_x > 180)
865                         self.b_angle_x = self.b_angle_x - 360;
866                 self.keys = self.keys & 63;
867
868                 if (angcomp(self.b_angle_y, self.v_angle_y) > 10)
869                         self.keys = self.keys | KEY_LOOKLEFT;
870                 else if (angcomp(self.b_angle_y, self.v_angle_y) < -10)
871                         self.keys = self.keys | KEY_LOOKRIGHT;
872                 if (angcomp(self.b_angle_x, self.v_angle_x) < -10)
873                         self.keys = self.keys | KEY_LOOKUP;
874                 else if (angcomp(self.b_angle_x, self.v_angle_x) > 10)
875                         self.keys = self.keys | KEY_LOOKDOWN;
876         }
877 };
878
879 /*
880 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
881
882 BotAI
883
884 This is the main ai loop. Though called every
885 frame, the ai_time limits it's actual updating
886
887 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
888 */
889 float stagger_think;
890
891 void() BotAI =
892 {
893         // am I dead? Fire randomly until I respawn
894         // health < 1 is used because fractional healths show up as 0 on normal player
895         // status bars, and the mod probably already compensated for that
896
897         if (self.health < 1)
898         { 
899                 self.button0 = floor(random() * 2);
900                 self.button2 = 0;
901                 self.keys = 0;
902                 self.b_aiflags = 0;
903                 ClearMyRoute();
904                 self.target1 = self.target2 = self.target3 = self.target4 = self.enemy = world;
905                 self.last_way = world;
906                 return;
907         }
908                 
909         // stagger the bot's AI out so they all don't think at the same time, causing game
910         // 'spikes'
911         if (self.b_skill < 2)
912         {
913                 if (self.ai_time > time)
914                         return;
915
916                 self.ai_time = time + 0.05;
917                 if (bot_count > 0)
918                 {
919                         if ((time - stagger_think) < (0.1 / bot_count))
920                                 self.ai_time = self.ai_time + 0.1 / (2 * bot_count);
921                 }
922                 else
923                         return;
924         }
925         if (self.view_ofs == '0 0 0')
926                 bot_start_topic(7);
927         stagger_think = time;
928
929         // shut the bot's buttons off, various functions will turn them on by AI end
930
931         self.button2 = 0;
932         self.button0 = 0;
933         
934
935         // target1 is like goalentity in normal Quake monster AI.
936         // it's the bot's most immediate target
937         if (route_table == self)
938         {
939                 if (busy_waypoints <= 0)
940                 {
941                         if (waypoint_mode < WM_EDITOR)
942                                 bot_look_for_crap(TRUE);
943                 }
944                 self.b_aiflags = 0;
945                 self.keys = 0;
946         }
947         else if (self.target1)
948         {
949                 frik_movetogoal();
950                 bot_path();
951         }
952         else
953         {
954                 if (waypoint_mode < WM_EDITOR)
955                 {
956                         if(self.route_failed)
957                         {
958                                 frik_bot_roam();
959                                 self.route_failed = 0;
960                         }
961                         else if(!begin_route())
962                         {       
963                                 bot_look_for_crap(FALSE);
964                         }
965                         self.keys = 0;
966                 }
967                 else
968                 {
969                         self.b_aiflags = AI_WAIT;
970                         self.keys = 0;
971                 }
972         }
973
974         // bot_angle_set points the bot at it's goal (self.enemy or target1)
975
976         bot_angle_set();
977
978         // fight my enemy. Enemy is probably a field QC coders will most likely use a lot
979         // for their own needs, since it's unused on a normal player
980         // FIXME
981         if (self.enemy)
982                 bot_fight_style();
983         else if (random() < 0.2)
984                 if (random() < 0.2)
985                         bot_weapon_switch(-1);
986         bot_dodge_stuff();
987
988         // checks to see if bot needs to start going up for air
989 /*      if (self.waterlevel > 2)
990         {
991                 if (time > (self.air_finished - 2))
992                 {
993                         traceline (self.origin, self.origin + '0 0 6800', TRUE, self);
994                         if (trace_inopen)       
995                         {
996                                 self.keys = KEY_MOVEUP;
997                                 self.button2 = TRUE; // swim!
998                                 return; // skip ai flags for now - this is life or death
999                         }
1000                 }
1001         }
1002 */
1003         // b_aiflags handling
1004
1005         
1006         if (self.b_aiflags)
1007                 bot_handle_ai(); 
1008         else
1009                 bot_chat(); // don't want chat to screw him up if he's rjing or something
1010 };