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.model == "progs/armor.mdl")
663 if (self.armorvalue < 200)
667 else if (self.armorvalue < 100)
671 else if (thing.classname == "weapon_shotgun")
673 if (!(self.items & IT_SHOTGUN))
676 else if (thing.classname == "weapon_uzi")
678 if (!(self.items & IT_UZI))
681 else if (thing.classname == "weapon_electro")
683 if (!(self.items & IT_ELECTRO))
686 else if (thing.classname == "weapon_grenadelauncher")
688 if (!(self.items & IT_GRENADE_LAUNCHER))
691 else if (thing.classname == "weapon_crylink")
693 if (!(self.items & IT_CRYLINK))
696 else if (thing.classname == "weapon_nex")
698 if (!(self.items & IT_NEX)) // IT_LIGHTNING
701 else if (thing.classname == "weapon_hagar")
703 if (!(self.items & IT_HAGAR))
706 else if (thing.classname == "weapon_rocketlauncher")
708 if (!(self.items & IT_ROCKET_LAUNCHER))
712 else if (thing.classname == "player")
714 if (thing.health > 0)
723 if (thing.target1.classname == "player")
724 if (!thing.target1.ishuman)
727 else if (teamplay && thing.team == self.team)
730 if (thing.target1.classname == "player")
737 else if (thing.classname == "waypoint")
739 if (thing.b_aiflags & AI_SNIPER)
741 else if (thing.b_aiflags & AI_AMBUSH)
744 if (pointcontents(thing.origin) < -3)
748 if (thing.current_way)
750 // check to see if it's unreachable
751 if (thing.current_way.items == -1)
754 thisp = thisp + (13000 - thing.current_way.items) * 0.05;
761 void(float scope) bot_look_for_crap =
763 local entity foe, best;
764 local float thatp, bestp, dist;
767 foe = findradius(self.origin, 13000);
769 foe = findradius(self.origin, 500);
774 thatp = priority_for_thing(foe);
783 dist = vlen(self.origin - foe.origin);
789 if (!target_onstack(best))
794 bot_get_path(best, FALSE);
795 self.b_aiflags = self.b_aiflags | AI_WAIT;
802 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
806 Sets the bots look keys & b_angle to point at
807 the target - used for fighting and just
808 generally making the bot look good.
810 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
813 void() bot_angle_set =
820 if (self.enemy.items & 524288)
823 if (self.missile_speed == 0)
824 self.missile_speed = 10000;
825 if (self.enemy.solid == SOLID_BSP)
827 view = (((self.enemy.absmin + self.enemy.absmax) * 0.5) - self.origin);
831 h = vlen(self.enemy.origin - self.origin) / self.missile_speed;
832 if (self.enemy.flags & FL_ONGROUND)
833 view = self.enemy.velocity * h;
835 view = (self.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
836 view = self.enemy.origin + view;
838 traceline(self.enemy.origin, view, FALSE, self);
841 if (self.weapon == WEP_GRENADE_LAUNCHER || self.weapon == WEP_ELECTRO || self.weapon == WEP_HAGAR || self.weapon == WEP_ROCKET_LAUNCHER)
842 view = view - '0 0 22';
844 view = normalize(view - self.origin);
846 view = vectoangles(view);
847 view_x = view_x * -1;
850 else if (self.target1)
852 view = realorigin(self.target1);
853 if (self.target1.flags & FL_ITEM)
854 view = view + '0 0 48';
855 view = view - (self.origin + self.view_ofs);
856 view = vectoangles(view);
857 view_x = view_x * -1;
862 // HACK HACK HACK HACK
863 // The bot falls off ledges a lot because of "turning around"
864 // so let the bot use instant turn around when not hunting a player
865 if (self.b_skill == 3)
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;
875 else if ((self.enemy == world || self.enemy.movetype == MOVETYPE_PUSH) && self.target1.classname != "player")
877 self.keys = self.keys & 63;
878 self.v_angle = self.b_angle;
879 while (self.v_angle_x < -180)
880 self.v_angle_x = self.v_angle_x + 360;
881 while (self.v_angle_x > 180)
882 self.v_angle_x = self.v_angle_x - 360;
884 else if (self.b_skill < 2) // skill 2 handled in bot_phys
886 if (self.b_angle_x > 180)
887 self.b_angle_x = self.b_angle_x - 360;
888 self.keys = self.keys & 63;
890 if (angcomp(self.b_angle_y, self.v_angle_y) > 10)
891 self.keys = self.keys | KEY_LOOKLEFT;
892 else if (angcomp(self.b_angle_y, self.v_angle_y) < -10)
893 self.keys = self.keys | KEY_LOOKRIGHT;
894 if (angcomp(self.b_angle_x, self.v_angle_x) < -10)
895 self.keys = self.keys | KEY_LOOKUP;
896 else if (angcomp(self.b_angle_x, self.v_angle_x) > 10)
897 self.keys = self.keys | KEY_LOOKDOWN;
902 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
906 This is the main ai loop. Though called every
907 frame, the ai_time limits it's actual updating
909 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
912 float intermission_running;
916 // am I dead? Fire randomly until I respawn
917 // health < 1 is used because fractional healths show up as 0 on normal player
918 // status bars, and the mod probably already compensated for that
922 self.button0 = floor(random() * 2);
927 self.target1 = self.target2 = self.target3 = self.target4 = self.enemy = world;
928 self.last_way = world;
932 // stagger the bot's AI out so they all don't think at the same time, causing game
934 if (self.b_skill < 2)
936 if (self.ai_time > time)
939 self.ai_time = time + 0.05;
942 if ((time - stagger_think) < (0.1 / bot_count))
943 self.ai_time = self.ai_time + 0.1 / (2 * bot_count);
948 if (intermission_running)
950 stagger_think = time;
952 // shut the bot's buttons off, various functions will turn them on by AI end
958 // target1 is like goalentity in normal Quake monster AI.
959 // it's the bot's most immediate target
960 if (route_table == self)
962 if (busy_waypoints <= 0)
964 if (waypoint_mode < WM_EDITOR)
965 bot_look_for_crap(TRUE);
970 else if (self.target1)
977 if (waypoint_mode < WM_EDITOR)
979 if(self.route_failed)
982 self.route_failed = 0;
984 else if(!begin_route())
986 bot_look_for_crap(FALSE);
992 self.b_aiflags = AI_WAIT;
997 // bot_angle_set points the bot at it's goal (self.enemy or target1)
1001 // fight my enemy. Enemy is probably a field QC coders will most likely use a lot
1002 // for their own needs, since it's unused on a normal player
1006 else if (random() < 0.2)
1008 bot_weapon_switch(-1);
1011 // checks to see if bot needs to start going up for air
1012 /* if (self.waterlevel > 2)
1014 if (time > (self.air_finished - 2))
1016 traceline (self.origin, self.origin + '0 0 6800', TRUE, self);
1019 self.keys = KEY_MOVEUP;
1020 self.button2 = TRUE; // swim!
1021 return; // skip ai flags for now - this is life or death
1026 // b_aiflags handling
1032 // bot_chat(); // don't want chat to screw him up if he's rjing or something