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 != WEP_LASER)
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 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
509 local entity jj, tele;
511 bot_check_lost(self.target1);
517 if (target_onstack(self.last_way))
518 return; // old waypoint still being hunted
520 jj = FindRoute(self.last_way);
523 // this is an ugly hack
524 if (self.target1.current_way != self.last_way)
526 if (self.target1.classname != "temp_waypoint")
527 if (self.target1.classname != "player")
528 bot_lost(self.target1, FALSE);
534 // update the bot's special ai features
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
539 self.b_aiflags = (jj.b_aiflags & AI_READAHEAD_TYPES) | (self.last_way.b_aiflags & AI_POINT_TYPES);
543 if (CheckLinked(self.last_way, jj) == 2) // waypoints are telelinked
545 tele = FindThing("trigger_teleport"); // this is probbly the teleport responsible
548 traceline(self.last_way.origin, jj.origin, FALSE, self); // check for blockage
549 if (trace_fraction != 1)
551 if (trace_ent.classname == "door" && !(self.b_aiflags & AI_DOOR_NO_OPEN)) // a door blocks the way
555 trace_ent = trace_ent.owner;
556 if ((trace_ent.health > 0) && (self.enemy == world))
558 self.enemy = trace_ent;
559 bot_weapon_switch(1);
560 self.b_aiflags = self.b_aiflags | AI_BLIND; // nick knack paddy hack
562 else if (trace_ent.targetname)
564 tele = find(world, target, trace_ent.targetname);
568 bot_weapon_switch(1);
574 // bot_get_path(tele, TRUE);
575 self.b_aiflags = self.b_aiflags | AI_BLIND; // give a bot a bone
580 else if (trace_ent.classname == "func_wall")
583 bot_lost(self.target1, FALSE);
588 // this is used for AI_DRIECTIONAL
589 self.b_dir = normalize(jj.origin - self.last_way.origin);
596 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
598 Bot Priority Look. What a stupid name. This is where
599 the bot finds things it wants to kill/grab.
601 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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
609 // *!* Make sure you add code to bot_check_lost to remove the target *!*
611 float(entity thing) priority_for_thing =
615 // This is the most executed function in the bot. Careful what you do here.
617 if (thing.flags & FL_ITEM && thing.model != string_null && thing.search_time < time)
620 if (thing._last != self)
622 else if (thing.classname == "item_strength") // IT_STRENGTH
624 else if (thing.classname == "item_invincible") // IT_INVINCIBLE
626 //else if (thing.classname == "item_speed") // IT_SPEED
628 //else if (thing.classname == "item_slowmo") // IT_SLOWMO
630 else if (thing.classname == "item_health")
632 if (thing.spawnflags & 2)
634 if (self.health < 40)
637 else if (thing.classname == "item_health1" || thing.classname == "item_health2")
640 if (self.health < 40)
643 else if (thing.classname == "item_health100")
646 if (self.health < 40)
649 else if (thing.classname == "item_armor1")
652 if (self.armorvalue < 100)
655 else if (thing.classname == "item_armor25")
658 if (self.armorvalue < 100)
661 else if (thing.classname == "weapon_shotgun")
663 if (!(self.items & IT_SHOTGUN))
666 else if (thing.classname == "weapon_uzi")
668 if (!(self.items & IT_UZI))
671 else if (thing.classname == "weapon_electro")
673 if (!(self.items & IT_ELECTRO))
676 else if (thing.classname == "weapon_grenadelauncher")
678 if (!(self.items & IT_GRENADE_LAUNCHER))
681 else if (thing.classname == "weapon_crylink")
683 if (!(self.items & IT_CRYLINK))
686 else if (thing.classname == "weapon_nex")
688 if (!(self.items & IT_NEX)) // IT_LIGHTNING
691 else if (thing.classname == "weapon_hagar")
693 if (!(self.items & IT_HAGAR))
696 else if (thing.classname == "weapon_rocketlauncher")
698 if (!(self.items & IT_ROCKET_LAUNCHER))
702 else if (thing.classname == "player")
704 if (thing.health > 0)
713 if (thing.target1.classname == "player")
714 if (!thing.target1.ishuman)
717 else if (teamplay && thing.team == self.team)
720 if (thing.target1.classname == "player")
727 else if (thing.classname == "waypoint")
729 if (thing.b_aiflags & AI_SNIPER)
731 else if (thing.b_aiflags & AI_AMBUSH)
734 if (pointcontents(thing.origin) < -3)
738 if (thing.current_way)
740 // check to see if it's unreachable
741 if (thing.current_way.items == -1)
744 thisp = thisp + (13000 - thing.current_way.items) * 0.05;
751 void(float scope) bot_look_for_crap =
753 local entity foe, best;
754 local float thatp, bestp, dist;
757 foe = findradius(self.origin, 13000);
759 foe = findradius(self.origin, 500);
764 thatp = priority_for_thing(foe);
773 dist = vlen(self.origin - foe.origin);
779 if (!target_onstack(best))
784 bot_get_path(best, FALSE);
785 self.b_aiflags = self.b_aiflags | AI_WAIT;
792 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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.
800 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
803 void() bot_angle_set =
810 if (self.enemy.items & 524288)
813 if (self.missile_speed == 0)
814 self.missile_speed = 10000;
815 if (self.enemy.solid == SOLID_BSP)
817 view = (((self.enemy.absmin + self.enemy.absmax) * 0.5) - self.origin);
821 h = vlen(self.enemy.origin - self.origin) / self.missile_speed;
822 if (self.enemy.flags & FL_ONGROUND)
823 view = self.enemy.velocity * h;
825 view = (self.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
826 view = self.enemy.origin + view;
828 traceline(self.enemy.origin, view, FALSE, self);
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';
834 view = normalize(view - self.origin);
836 view = vectoangles(view);
837 view_x = view_x * -1;
840 else if (self.target1)
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;
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)
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;
865 else if ((self.enemy == world || self.enemy.movetype == MOVETYPE_PUSH) && self.target1.classname != "player")
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;
874 else if (self.b_skill < 2) // skill 2 handled in bot_phys
876 if (self.b_angle_x > 180)
877 self.b_angle_x = self.b_angle_x - 360;
878 self.keys = self.keys & 63;
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;
892 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
896 This is the main ai loop. Though called every
897 frame, the ai_time limits it's actual updating
899 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
902 float intermission_running;
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
912 self.button0 = floor(random() * 2);
917 self.target1 = self.target2 = self.target3 = self.target4 = self.enemy = world;
918 self.last_way = world;
922 // stagger the bot's AI out so they all don't think at the same time, causing game
924 if (self.b_skill < 2)
926 if (self.ai_time > time)
929 self.ai_time = time + 0.05;
932 if ((time - stagger_think) < (0.1 / bot_count))
933 self.ai_time = self.ai_time + 0.1 / (2 * bot_count);
938 if (intermission_running)
940 stagger_think = time;
942 // shut the bot's buttons off, various functions will turn them on by AI end
948 // target1 is like goalentity in normal Quake monster AI.
949 // it's the bot's most immediate target
950 if (route_table == self)
952 if (busy_waypoints <= 0)
954 if (waypoint_mode < WM_EDITOR)
955 bot_look_for_crap(TRUE);
960 else if (self.target1)
967 if (waypoint_mode < WM_EDITOR)
969 if(self.route_failed)
972 self.route_failed = 0;
974 else if(!begin_route())
976 bot_look_for_crap(FALSE);
982 self.b_aiflags = AI_WAIT;
987 // bot_angle_set points the bot at it's goal (self.enemy or target1)
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
996 else if (random() < 0.2)
998 bot_weapon_switch(-1);
1001 // checks to see if bot needs to start going up for air
1002 /* if (self.waterlevel > 2)
1004 if (time > (self.air_finished - 2))
1006 traceline (self.origin, self.origin + '0 0 6800', TRUE, self);
1009 self.keys = KEY_MOVEUP;
1010 self.button2 = TRUE; // swim!
1011 return; // skip ai flags for now - this is life or death
1016 // b_aiflags handling
1022 // bot_chat(); // don't want chat to screw him up if he's rjing or something