1 /***********************************************
4 * "The I'd rather be playing Quake AI" *
6 ***********************************************/
10 This program is in the Public Domain. My crack legal
11 team would like to add:
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.
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
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.
40 Any reproduction of this software must contain
41 this notice in its entirety.
46 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
50 checks to see if an entity is on the bot's stack
52 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
55 float(entity scot) target_onstack =
59 else if (self.target1 == scot)
61 else if (self.target2 == scot)
63 else if (self.target3 == scot)
65 else if (self.target4 == scot)
72 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
76 adds a new entity to the stack, since it's a
77 LIFO stack, this will be the bot's new target1
79 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
82 void(entity ent) target_add =
86 if (target_onstack(ent))
88 self.target4 = self.target3;
89 self.target3 = self.target2;
90 self.target2 = self.target1;
92 self.search_time = time + 5;
97 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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
107 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
110 void(entity ent) target_drop =
114 tg = target_onstack(ent);
117 self.target1 = self.target2;
118 self.target2 = self.target3;
119 self.target3 = self.target4;
120 self.target4 = world;
124 self.target1 = self.target3;
125 self.target2 = self.target4;
126 self.target3 = self.target4 = world;
130 self.target1 = self.target4;
131 self.target2 = self.target3 = self.target4 = world;
134 self.target1 = self.target2 = self.target3 = self.target4 = world;
135 self.search_time = time + 5;
139 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
143 Bot has lost its target.
145 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
148 void(entity targ, float success) bot_lost =
154 if (targ.classname == "waypoint")
155 targ.b_sound = targ.b_sound - (targ.b_sound & ClientBitFlag(self.b_clientno));
160 self.target1 = self.target2 = self.target3 = self.target4 = world;
161 self.last_way = FindWayPoint(self.current_way);
167 if (targ.classname == "item_artifact_invisibility")
168 if (self.items & 524288)
171 if (targ.flags & FL_ITEM)
173 if (targ.model == string_null)
181 if (targ.classname != "player")
182 targ.search_time = time + 5;
186 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
190 decide if my most immediate target should be
193 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
195 void(entity targ) bot_check_lost =
198 dist = realorigin(targ) - self.origin;
203 // waypoints and items are lost if you get close enough to them
205 else if (targ.flags & FL_ITEM)
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);
212 else if (targ.classname == "waypoint")
214 if (!(self.b_aiflags & (AI_SNIPER | AI_AMBUSH)))
216 if (self.b_aiflags & AI_RIDE_TRAIN)
218 if (vlen(targ.origin - self.origin) < 48)
219 bot_lost(targ, TRUE);
221 else if (self.b_aiflags & AI_PRECISION)
223 if (vlen(targ.origin - self.origin) < 24)
224 bot_lost(targ, TRUE);
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
230 if (vlen(targ.origin - self.origin) < 128)
231 bot_lost(targ, TRUE);
235 else if (targ.classname == "temp_waypoint")
237 if (vlen(targ.origin - self.origin) < 32)
238 bot_lost(targ, TRUE);
240 else if (targ.classname == "player")
242 if (targ.health <= 0)
243 bot_lost(targ, TRUE);
244 else if ((coop) || (teamplay && targ.team == self.team))
246 if (targ.target1.classname == "player")
248 if (!targ.target1.ishuman)
249 bot_lost(targ, TRUE);
251 else if (targ.teleport_time > time)
253 // try not to telefrag teammates
254 self.keys = self.keys & 960;
256 else if (vlen(targ.origin - self.origin) < 128)
258 if (vlen(targ.origin - self.origin) < 48)
259 frik_walkmove(self.origin - targ.origin);
262 self.keys = self.keys & 960;
265 self.search_time = time + 5; // never time out
267 else if (!fisible(targ))
268 bot_lost(targ, FALSE);
270 else if (waypoint_mode > WM_LOADED)
272 if (vlen(targ.origin - self.origin) < 128)
274 bot_lost(targ, TRUE);
279 // buttons are lost of their frame changes
280 else if (targ.classname == "func_button")
284 bot_lost(targ, TRUE);
285 if (self.enemy == targ)
288 // bot_get_path(self.target1, TRUE);
292 // trigger_multiple style triggers are lost if their thinktime changes
293 else if ((targ.movetype == MOVETYPE_NONE) && (targ.solid == SOLID_TRIGGER))
295 if (targ.nextthink >= time)
297 bot_lost(targ, TRUE);
299 // bot_get_path(self.target1, TRUE);
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)
306 dist = targ.origin - self.origin;
309 if (self.flags & FL_ONGROUND)
310 if(!frik_recognize_plat(FALSE))
311 bot_lost(targ, FALSE);
313 else if (targ.classname == "train")
315 if (frik_recognize_plat(FALSE))
316 bot_lost(targ, TRUE);
318 // targets are lost if the bot's search time has expired
319 if (time > self.search_time)
320 bot_lost(targ, FALSE);
325 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
329 This is a 0.10 addition. Handles any action
332 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
335 void() bot_handle_ai =
340 // handle ai flags -- note, not all aiflags are handled
341 // here, just those that perform some sort of action
343 // wait is used by the ai to stop the bot until his search time expires / or route changes
345 if (self.b_aiflags & AI_WAIT)
346 self.keys = self.keys & 960;
348 if (self.b_aiflags & AI_DOORFLAG) // was on a door when spawned
351 self = self.last_way;
352 if (!frik_recognize_plat(FALSE)) // if there is nothing there now
354 newt = FindThing("door"); // this is likely the door responsible (crossfingers)
357 if (self.b_aiflags & AI_DOOR_NO_OPEN)
360 self.keys = self.keys & 960; // wait until it closes
363 bot_lost(self.last_way, FALSE);
370 newt = find(world, target, newt.targetname);
374 bot_weapon_switch(1);
378 // target_drop(self.last_way);
380 // bot_get_path(newt, TRUE);
383 self.b_aiflags = self.b_aiflags - AI_DOORFLAG;
390 if (self.b_aiflags & AI_JUMP)
392 if (self.flags & FL_ONGROUND)
395 self.b_aiflags = self.b_aiflags - AI_JUMP;
398 else if (self.b_aiflags & AI_SUPER_JUMP)
400 if (self.weapon != 32)
402 else if (self.flags & FL_ONGROUND)
404 self.b_aiflags = self.b_aiflags - AI_SUPER_JUMP;
405 if (bot_can_rj(self))
408 self.v_angle_x = self.b_angle_x = 80;
412 bot_lost(self.target1, FALSE);
416 if (self.b_aiflags & AI_SURFACE)
418 if (self.waterlevel > 2)
420 self.keys = KEY_MOVEUP;
421 self.button2 = TRUE; // swim!
424 self.b_aiflags = self.b_aiflags - AI_SURFACE;
426 if (self.b_aiflags & AI_RIDE_TRAIN)
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)
432 self = self.last_way;
434 if (!frik_recognize_plat(FALSE)) // if there is nothing there now
437 self.keys = self.keys & 960;
442 if (frik_recognize_plat(FALSE))
444 v = realorigin(trace_ent) + trace_ent.origin - self.origin;
447 self.keys = self.keys & 960;
450 self.b_aiflags = self.b_aiflags | AI_PRECISION;
451 self.keys = frik_KeysForDir(v);
456 if (self.b_aiflags & AI_PLAT_BOTTOM)
458 newt = FindThing("plat");
461 v = self.origin - realorigin(newt);
464 self.keys = self.keys & 960;
469 self.b_aiflags = self.b_aiflags - AI_PLAT_BOTTOM;
471 if (self.b_aiflags & AI_DIRECTIONAL)
473 if ((normalize(self.last_way.origin - self.origin) * self.b_dir) > 0.4)
475 self.b_aiflags = self.b_aiflags - AI_DIRECTIONAL;
476 bot_lost(self.target1, TRUE);
479 if (self.b_aiflags & AI_SNIPER)
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?
485 if (self.b_aiflags & AI_AMBUSH)
487 self.b_aiflags = (self.b_aiflags | AI_WAIT) - AI_AMBUSH;
488 // FIXME: Add a switch to wep command
489 // FIXME: increase delay?
495 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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
504 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
510 local entity jj, tele;
513 bot_check_lost(self.target1);
519 if (target_onstack(self.last_way))
520 return; // old waypoint still being hunted
522 jj = FindRoute(self.last_way);
525 // this is an ugly hack
526 if (self.target1.current_way != self.last_way)
528 if (self.target1.classname != "temp_waypoint")
529 if (self.target1.classname != "player")
530 bot_lost(self.target1, FALSE);
536 // update the bot's special ai features
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
541 self.b_aiflags = (jj.b_aiflags & AI_READAHEAD_TYPES) | (self.last_way.b_aiflags & AI_POINT_TYPES);
545 if (CheckLinked(self.last_way, jj) == 2) // waypoints are telelinked
547 tele = FindThing("trigger_teleport"); // this is probbly the teleport responsible
550 traceline(self.last_way.origin, jj.origin, FALSE, self); // check for blockage
551 if (trace_fraction != 1)
553 if (trace_ent.classname == "door" && !(self.b_aiflags & AI_DOOR_NO_OPEN)) // a door blocks the way
557 trace_ent = trace_ent.owner;
558 if ((trace_ent.health > 0) && (self.enemy == world))
560 self.enemy = trace_ent;
561 bot_weapon_switch(1);
562 self.b_aiflags = self.b_aiflags | AI_BLIND; // nick knack paddy hack
564 else if (trace_ent.targetname)
566 tele = find(world, target, trace_ent.targetname);
570 bot_weapon_switch(1);
576 // bot_get_path(tele, TRUE);
577 self.b_aiflags = self.b_aiflags | AI_BLIND; // give a bot a bone
582 else if (trace_ent.classname == "func_wall")
585 bot_lost(self.target1, FALSE);
590 // this is used for AI_DRIECTIONAL
591 self.b_dir = normalize(jj.origin - self.last_way.origin);
598 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
600 Bot Priority Look. What a stupid name. This is where
601 the bot finds things it wants to kill/grab.
603 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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
611 // *!* Make sure you add code to bot_check_lost to remove the target *!*
613 float(entity thing) priority_for_thing =
617 // This is the most executed function in the bot. Careful what you do here.
619 if (thing.flags & FL_ITEM && thing.model != string_null && thing.search_time < time)
622 if (thing._last != self)
624 else if (thing.classname == "item_strength") // IT_STRENGTH
626 else if (thing.classname == "item_invincible") // IT_INVINCIBLE
628 else if (thing.classname == "item_speed") // IT_SPEED
630 else if (thing.classname == "item_slowmo") // IT_SLOWMO
632 else if (thing.classname == "item_health")
634 if (thing.spawnflags & 2)
636 if (self.health < 40)
639 else if (thing.model == "progs/armor.mdl")
641 if (self.armorvalue < 200)
645 else if (self.armorvalue < 100)
649 else if (thing.classname == "weapon_shotgun")
651 if (!(self.items & IT_SHOTGUN))
654 else if (thing.classname == "weapon_uzi")
656 if (!(self.items & IT_UZI))
659 else if (thing.classname == "weapon_electro")
661 if (!(self.items & IT_ELECTRO))
664 else if (thing.classname == "weapon_grenadelauncher")
666 if (!(self.items & IT_GRENADE_LAUNCHER))
669 else if (thing.classname == "weapon_crylink")
671 if (!(self.items & IT_CRYLINK))
674 else if (thing.classname == "weapon_nex")
676 if (!(self.items & IT_NEX)) // IT_LIGHTNING
679 else if (thing.classname == "weapon_hagar")
681 if (!(self.items & IT_HAGAR))
684 else if (thing.classname == "weapon_rocketlauncher")
686 if (!(self.items & IT_ROCKET_LAUNCHER))
690 else if (thing.classname == "player")
692 if (thing.health > 0)
701 if (thing.target1.classname == "player")
702 if (!thing.target1.ishuman)
705 else if (teamplay && thing.team == self.team)
708 if (thing.target1.classname == "player")
715 else if (thing.classname == "waypoint")
717 if (thing.b_aiflags & AI_SNIPER)
719 else if (thing.b_aiflags & AI_AMBUSH)
722 if (pointcontents(thing.origin) < -3)
726 if (thing.current_way)
728 // check to see if it's unreachable
729 if (thing.current_way.items == -1)
732 thisp = thisp + (13000 - thing.current_way.items) * 0.05;
739 void(float scope) bot_look_for_crap =
741 local entity foe, best;
742 local float thatp, bestp, dist;
745 foe = findradius(self.origin, 13000);
747 foe = findradius(self.origin, 500);
752 thatp = priority_for_thing(foe);
761 dist = vlen(self.origin - foe.origin);
767 if (!target_onstack(best))
772 bot_get_path(best, FALSE);
773 self.b_aiflags = self.b_aiflags | AI_WAIT;
780 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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.
788 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
791 void() bot_angle_set =
798 if (self.enemy.items & 524288)
801 if (self.missile_speed == 0)
802 self.missile_speed = 10000;
803 if (self.enemy.solid == SOLID_BSP)
805 view = (((self.enemy.absmin + self.enemy.absmax) * 0.5) - self.origin);
809 h = vlen(self.enemy.origin - self.origin) / self.missile_speed;
810 if (self.enemy.flags & FL_ONGROUND)
811 view = self.enemy.velocity * h;
813 view = (self.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
814 view = self.enemy.origin + view;
816 traceline(self.enemy.origin, view, FALSE, self);
819 if (self.weapon == 32)
820 view = view - '0 0 22';
822 view = normalize(view - self.origin);
824 view = vectoangles(view);
825 view_x = view_x * -1;
828 else if (self.target1)
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;
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)
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;
853 else if ((self.enemy == world || self.enemy.movetype == MOVETYPE_PUSH) && self.target1.classname != "player")
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;
862 else if (self.b_skill < 2) // skill 2 handled in bot_phys
864 if (self.b_angle_x > 180)
865 self.b_angle_x = self.b_angle_x - 360;
866 self.keys = self.keys & 63;
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;
880 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
884 This is the main ai loop. Though called every
885 frame, the ai_time limits it's actual updating
887 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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
899 self.button0 = floor(random() * 2);
904 self.target1 = self.target2 = self.target3 = self.target4 = self.enemy = world;
905 self.last_way = world;
909 // stagger the bot's AI out so they all don't think at the same time, causing game
911 if (self.b_skill < 2)
913 if (self.ai_time > time)
916 self.ai_time = time + 0.05;
919 if ((time - stagger_think) < (0.1 / bot_count))
920 self.ai_time = self.ai_time + 0.1 / (2 * bot_count);
925 if (self.view_ofs == '0 0 0')
927 stagger_think = time;
929 // shut the bot's buttons off, various functions will turn them on by AI end
935 // target1 is like goalentity in normal Quake monster AI.
936 // it's the bot's most immediate target
937 if (route_table == self)
939 if (busy_waypoints <= 0)
941 if (waypoint_mode < WM_EDITOR)
942 bot_look_for_crap(TRUE);
947 else if (self.target1)
954 if (waypoint_mode < WM_EDITOR)
956 if(self.route_failed)
959 self.route_failed = 0;
961 else if(!begin_route())
963 bot_look_for_crap(FALSE);
969 self.b_aiflags = AI_WAIT;
974 // bot_angle_set points the bot at it's goal (self.enemy or target1)
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
983 else if (random() < 0.2)
985 bot_weapon_switch(-1);
988 // checks to see if bot needs to start going up for air
989 /* if (self.waterlevel > 2)
991 if (time > (self.air_finished - 2))
993 traceline (self.origin, self.origin + '0 0 6800', TRUE, self);
996 self.keys = KEY_MOVEUP;
997 self.button2 = TRUE; // swim!
998 return; // skip ai flags for now - this is life or death
1003 // b_aiflags handling
1009 bot_chat(); // don't want chat to screw him up if he's rjing or something