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 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
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.classname == "item_health1" || thing.classname == "item_health2")
642 if (self.health < 40)
645 else if (thing.classname == "item_health100")
648 if (self.health < 40)
651 else if (thing.classname == "item_armor1")
654 if (self.armorvalue < 100)
657 else if (thing.classname == "item_armor25")
660 if (self.armorvalue < 100)
663 else if (thing.model == "progs/armor.mdl")
665 if (self.armorvalue < 200)
669 else if (self.armorvalue < 100)
673 else if (thing.classname == "weapon_shotgun")
675 if (!(self.items & IT_SHOTGUN))
678 else if (thing.classname == "weapon_uzi")
680 if (!(self.items & IT_UZI))
683 else if (thing.classname == "weapon_electro")
685 if (!(self.items & IT_ELECTRO))
688 else if (thing.classname == "weapon_grenadelauncher")
690 if (!(self.items & IT_GRENADE_LAUNCHER))
693 else if (thing.classname == "weapon_crylink")
695 if (!(self.items & IT_CRYLINK))
698 else if (thing.classname == "weapon_nex")
700 if (!(self.items & IT_NEX)) // IT_LIGHTNING
703 else if (thing.classname == "weapon_hagar")
705 if (!(self.items & IT_HAGAR))
708 else if (thing.classname == "weapon_rocketlauncher")
710 if (!(self.items & IT_ROCKET_LAUNCHER))
714 else if (thing.classname == "player")
716 if (thing.health > 0)
725 if (thing.target1.classname == "player")
726 if (!thing.target1.ishuman)
729 else if (teamplay && thing.team == self.team)
732 if (thing.target1.classname == "player")
739 else if (thing.classname == "waypoint")
741 if (thing.b_aiflags & AI_SNIPER)
743 else if (thing.b_aiflags & AI_AMBUSH)
746 if (pointcontents(thing.origin) < -3)
750 if (thing.current_way)
752 // check to see if it's unreachable
753 if (thing.current_way.items == -1)
756 thisp = thisp + (13000 - thing.current_way.items) * 0.05;
763 void(float scope) bot_look_for_crap =
765 local entity foe, best;
766 local float thatp, bestp, dist;
769 foe = findradius(self.origin, 13000);
771 foe = findradius(self.origin, 500);
776 thatp = priority_for_thing(foe);
785 dist = vlen(self.origin - foe.origin);
791 if (!target_onstack(best))
796 bot_get_path(best, FALSE);
797 self.b_aiflags = self.b_aiflags | AI_WAIT;
804 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
808 Sets the bots look keys & b_angle to point at
809 the target - used for fighting and just
810 generally making the bot look good.
812 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
815 void() bot_angle_set =
822 if (self.enemy.items & 524288)
825 if (self.missile_speed == 0)
826 self.missile_speed = 10000;
827 if (self.enemy.solid == SOLID_BSP)
829 view = (((self.enemy.absmin + self.enemy.absmax) * 0.5) - self.origin);
833 h = vlen(self.enemy.origin - self.origin) / self.missile_speed;
834 if (self.enemy.flags & FL_ONGROUND)
835 view = self.enemy.velocity * h;
837 view = (self.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
838 view = self.enemy.origin + view;
840 traceline(self.enemy.origin, view, FALSE, self);
843 if (self.weapon == WEP_GRENADE_LAUNCHER || self.weapon == WEP_ELECTRO || self.weapon == WEP_HAGAR || self.weapon == WEP_ROCKET_LAUNCHER)
844 view = view - '0 0 22';
846 view = normalize(view - self.origin);
848 view = vectoangles(view);
849 view_x = view_x * -1;
852 else if (self.target1)
854 view = realorigin(self.target1);
855 if (self.target1.flags & FL_ITEM)
856 view = view + '0 0 48';
857 view = view - (self.origin + self.view_ofs);
858 view = vectoangles(view);
859 view_x = view_x * -1;
864 // HACK HACK HACK HACK
865 // The bot falls off ledges a lot because of "turning around"
866 // so let the bot use instant turn around when not hunting a player
867 if (self.b_skill == 3)
869 self.keys = self.keys & 63;
870 self.v_angle = self.b_angle;
871 while (self.v_angle_x < -180)
872 self.v_angle_x = self.v_angle_x + 360;
873 while (self.v_angle_x > 180)
874 self.v_angle_x = self.v_angle_x - 360;
877 else if ((self.enemy == world || self.enemy.movetype == MOVETYPE_PUSH) && self.target1.classname != "player")
879 self.keys = self.keys & 63;
880 self.v_angle = self.b_angle;
881 while (self.v_angle_x < -180)
882 self.v_angle_x = self.v_angle_x + 360;
883 while (self.v_angle_x > 180)
884 self.v_angle_x = self.v_angle_x - 360;
886 else if (self.b_skill < 2) // skill 2 handled in bot_phys
888 if (self.b_angle_x > 180)
889 self.b_angle_x = self.b_angle_x - 360;
890 self.keys = self.keys & 63;
892 if (angcomp(self.b_angle_y, self.v_angle_y) > 10)
893 self.keys = self.keys | KEY_LOOKLEFT;
894 else if (angcomp(self.b_angle_y, self.v_angle_y) < -10)
895 self.keys = self.keys | KEY_LOOKRIGHT;
896 if (angcomp(self.b_angle_x, self.v_angle_x) < -10)
897 self.keys = self.keys | KEY_LOOKUP;
898 else if (angcomp(self.b_angle_x, self.v_angle_x) > 10)
899 self.keys = self.keys | KEY_LOOKDOWN;
904 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
908 This is the main ai loop. Though called every
909 frame, the ai_time limits it's actual updating
911 -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
914 float intermission_running;
918 // am I dead? Fire randomly until I respawn
919 // health < 1 is used because fractional healths show up as 0 on normal player
920 // status bars, and the mod probably already compensated for that
924 self.button0 = floor(random() * 2);
929 self.target1 = self.target2 = self.target3 = self.target4 = self.enemy = world;
930 self.last_way = world;
934 // stagger the bot's AI out so they all don't think at the same time, causing game
936 if (self.b_skill < 2)
938 if (self.ai_time > time)
941 self.ai_time = time + 0.05;
944 if ((time - stagger_think) < (0.1 / bot_count))
945 self.ai_time = self.ai_time + 0.1 / (2 * bot_count);
950 if (intermission_running)
952 stagger_think = time;
954 // shut the bot's buttons off, various functions will turn them on by AI end
960 // target1 is like goalentity in normal Quake monster AI.
961 // it's the bot's most immediate target
962 if (route_table == self)
964 if (busy_waypoints <= 0)
966 if (waypoint_mode < WM_EDITOR)
967 bot_look_for_crap(TRUE);
972 else if (self.target1)
979 if (waypoint_mode < WM_EDITOR)
981 if(self.route_failed)
984 self.route_failed = 0;
986 else if(!begin_route())
988 bot_look_for_crap(FALSE);
994 self.b_aiflags = AI_WAIT;
999 // bot_angle_set points the bot at it's goal (self.enemy or target1)
1003 // fight my enemy. Enemy is probably a field QC coders will most likely use a lot
1004 // for their own needs, since it's unused on a normal player
1008 else if (random() < 0.2)
1010 bot_weapon_switch(-1);
1013 // checks to see if bot needs to start going up for air
1014 /* if (self.waterlevel > 2)
1016 if (time > (self.air_finished - 2))
1018 traceline (self.origin, self.origin + '0 0 6800', TRUE, self);
1021 self.keys = KEY_MOVEUP;
1022 self.button2 = TRUE; // swim!
1023 return; // skip ai flags for now - this is life or death
1028 // b_aiflags handling
1034 bot_chat(); // don't want chat to screw him up if he's rjing or something