mirror of
https://git.code.sf.net/p/quake/game-source
synced 2024-11-25 13:21:30 +00:00
825 lines
20 KiB
C++
825 lines
20 KiB
C++
/***********************************************
|
|
* *
|
|
* FrikBot General AI *
|
|
* "The I'd rather be playing Quake AI" *
|
|
* *
|
|
***********************************************/
|
|
|
|
/*
|
|
|
|
This program is in the Public Domain. My crack legal
|
|
team would like to add:
|
|
|
|
RYAN "FRIKAC" SMITH IS PROVIDING THIS SOFTWARE "AS IS"
|
|
AND MAKES NO WARRANTY, EXPRESS OR IMPLIED, AS TO THE
|
|
ACCURACY, CAPABILITY, EFFICIENCY, MERCHANTABILITY, OR
|
|
FUNCTIONING OF THIS SOFTWARE AND/OR DOCUMENTATION. IN
|
|
NO EVENT WILL RYAN "FRIKAC" SMITH BE LIABLE FOR ANY
|
|
GENERAL, CONSEQUENTIAL, INDIRECT, INCIDENTAL,
|
|
EXEMPLARY, OR SPECIAL DAMAGES, EVEN IF RYAN "FRIKAC"
|
|
SMITH HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
DAMAGES, IRRESPECTIVE OF THE CAUSE OF SUCH DAMAGES.
|
|
|
|
You accept this software on the condition that you
|
|
indemnify and hold harmless Ryan "FrikaC" Smith from
|
|
any and all liability or damages to third parties,
|
|
including attorney fees, court costs, and other
|
|
related costs and expenses, arising out of your use
|
|
of this software irrespective of the cause of said
|
|
liability.
|
|
|
|
The export from the United States or the subsequent
|
|
reexport of this software is subject to compliance
|
|
with United States export control and munitions
|
|
control restrictions. You agree that in the event you
|
|
seek to export this software, you assume full
|
|
responsibility for obtaining all necessary export
|
|
licenses and approvals and for assuring compliance
|
|
with applicable reexport restrictions.
|
|
|
|
Any reproduction of this software must contain
|
|
this notice in its entirety.
|
|
|
|
*/
|
|
|
|
#include "libfrikbot.h"
|
|
|
|
float stagger_think;
|
|
|
|
@implementation Bot (AI)
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
targetOnstack
|
|
|
|
checks to see if an entity is on the bot's stack
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(integer)targetOnstack:(Target *)scot
|
|
{
|
|
if (scot == nil)
|
|
return FALSE;
|
|
else if (targets[0] == scot)
|
|
return 1;
|
|
else if (targets[1] == scot)
|
|
return 2;
|
|
else if (targets[2] == scot)
|
|
return 3;
|
|
else if (targets[3] == scot)
|
|
return 4;
|
|
else
|
|
return FALSE;
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
targetAdd
|
|
|
|
adds a new entity to the stack, since it's a
|
|
LIFO stack, this will be the bot's new targets[0]
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(void)targetAdd:(Target *)e
|
|
{
|
|
if (e == nil)
|
|
return;
|
|
if ([self targetOnstack:e])
|
|
return;
|
|
|
|
targets[3] = targets[2];
|
|
targets[2] = targets[1];
|
|
targets[1] = targets[0];
|
|
targets[0] = e;
|
|
[self setSearchTime:time + 5];
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
targetDrop
|
|
|
|
Removes an entity from the bot's target stack.
|
|
The stack will empty everything up to the object
|
|
So if you have target2 item_health, targets[0]
|
|
waypoint, and you drop the health, the waypoint
|
|
is gone too.
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(void)targetDrop:(Target *)e
|
|
{
|
|
switch ([self targetOnstack:e]) {
|
|
case 1:
|
|
targets[0] = targets[1];
|
|
targets[1] = targets[2];
|
|
targets[2] = targets[3];
|
|
targets[3] = nil;
|
|
break;
|
|
case 2:
|
|
targets[0] = targets[2];
|
|
targets[1] = targets[3];
|
|
targets[2] = targets[3] = nil;
|
|
break;
|
|
case 3:
|
|
targets[0] = targets[3];
|
|
targets[1] = targets[2] = targets[3] = nil;
|
|
break;
|
|
case 4:
|
|
targets[0] = targets[1] = targets[2] = targets[3] = nil;
|
|
default:
|
|
break;
|
|
}
|
|
[self setSearchTime:time + 5];
|
|
}
|
|
|
|
-(void)targetClearAll
|
|
{
|
|
targets[0] = targets[1] = targets[2] = targets[3] = nil;
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
bot_lost
|
|
|
|
Bot has lost its target.
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(void)lost:(Target *)targ :(integer)success
|
|
{
|
|
if (!targ)
|
|
return;
|
|
|
|
[self targetDrop:targ];
|
|
if ([targ classname] == "waypoint")
|
|
[(Waypoint *)targ clearRouteForBot:self];
|
|
|
|
// find a new route
|
|
if (!success) {
|
|
targets[0] = targets[1] = targets[2] = targets[3] = nil;
|
|
last_way = [self findWaypoint:current_way];
|
|
[Waypoint clearMyRoute:self];
|
|
b_aiflags = 0;
|
|
} else {
|
|
if ([targ classname] == "item_artifact_invisibility")
|
|
if (ent.items & IT_INVISIBILITY)
|
|
[self startTopic:3];
|
|
|
|
if (targ.ent.flags & FL_ITEM) {
|
|
if (!targ.ent.model)
|
|
targ._last = nil;
|
|
else
|
|
targ._last = self;
|
|
}
|
|
}
|
|
|
|
if ([targ classname] != "player")
|
|
[self setSearchTime:time + 5];
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
checkLost
|
|
|
|
decide if my most immediate target should be
|
|
removed.
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(void)checkLost:(Target *)targ
|
|
{
|
|
local vector dist;
|
|
|
|
if (targ == nil)
|
|
return;
|
|
|
|
dist = [targ realorigin] - ent.origin;
|
|
dist.z = 0;
|
|
|
|
// waypoints and items are lost if you get close enough to them
|
|
if ([targ isKindOfClass:[Waypoint class]]) {
|
|
local Waypoint *way = (Waypoint *) targ;
|
|
if ([way classname] == "waypoint") {
|
|
if (!(b_aiflags & (AI_SNIPER | AI_AMBUSH))) {
|
|
if (b_aiflags & AI_RIDE_TRAIN) {
|
|
if (vlen (way.origin - ent.origin) < 48)
|
|
[self lost:way :TRUE];
|
|
} else if (b_aiflags & AI_PRECISION) {
|
|
if (vlen (way.origin - ent.origin) < 24)
|
|
[self lost:way :TRUE];
|
|
} else if (vlen (way.origin - ent.origin) < 32)
|
|
[self lost:way :TRUE];
|
|
}
|
|
} else {
|
|
//temp_waypoint
|
|
if (vlen (way.origin - ent.origin) < 32)
|
|
[self lost:way :TRUE];
|
|
}
|
|
} else if ([targ isKindOfClass:[Bot class]]) {
|
|
local Bot *bot = (Bot *) targ;
|
|
if (bot.ent.health <= 0)
|
|
[self lost:bot :TRUE];
|
|
else if ((coop) || (teamplay && bot.ent.team == ent.team)) {
|
|
if ([bot.targets[0] classname] == "player") {
|
|
if (![bot.targets[0] ishuman])
|
|
[self lost:bot :TRUE];
|
|
} else if (bot.ent.teleport_time > time) {
|
|
// try not to telefrag teammates
|
|
keys &= ~KEY_MOVE;
|
|
} else if (vlen (bot.ent.origin - ent.origin) < 128) {
|
|
if (vlen (bot.ent.origin - ent.origin) < 48)
|
|
[self walkmove: ent.origin - bot.ent.origin];
|
|
else {
|
|
keys &= ~KEY_MOVE;
|
|
[self startTopic:4];
|
|
}
|
|
[self setSearchTime:time + 5]; // never time out
|
|
} else if (![self canSee:bot])
|
|
[self lost:bot :FALSE];
|
|
} else if (waypoint_mode > WM_LOADED) {
|
|
if (vlen (bot.ent.origin - ent.origin) < 128) {
|
|
[self lost:bot :TRUE];
|
|
}
|
|
}
|
|
} else {
|
|
if (targ.ent.flags & FL_ITEM) {
|
|
if (vlen (targ.ent.origin - ent.origin) < 32)
|
|
[self lost:targ :TRUE];
|
|
else if (!targ.ent.model)
|
|
[self lost:targ :TRUE];
|
|
} else if (targ.ent.classname == "func_button") {
|
|
// buttons are lost of their frame changes
|
|
if (targ.ent.frame) {
|
|
[self lost:targ :TRUE];
|
|
if (ent.enemy == targ.ent)
|
|
ent.enemy = nil;
|
|
// if (target[0])
|
|
// [self getPath:target[0] :TRUE];
|
|
}
|
|
} else if ((targ.ent.movetype == MOVETYPE_NONE)
|
|
&& (targ.ent.solid == SOLID_TRIGGER)) {
|
|
// trigger_multiple style triggers are lost if their thinktime
|
|
// changes
|
|
if (targ.ent.nextthink >= time) {
|
|
[self lost:targ :TRUE];
|
|
// if (target[0])
|
|
// [self getPath:target[0] :TRUE];
|
|
}
|
|
}
|
|
}
|
|
// lose any target way above the bot's head
|
|
// FIXME: if the bot can fly in your mod..
|
|
if ((targ.ent.origin.z - ent.origin.z) > 64) {
|
|
dist = targ.ent.origin - ent.origin;
|
|
dist.z = 0;
|
|
if (vlen (dist) < 32)
|
|
if (ent.flags & FL_ONGROUND)
|
|
if (![self recognizePlat:FALSE])
|
|
[self lost:targ :FALSE];
|
|
} else if (targ.ent.classname == "train") {
|
|
if ([self recognizePlat:FALSE])
|
|
[self lost:targ :TRUE];
|
|
}
|
|
// targets are lost if the bot's search time has expired
|
|
if (time > [self searchTime])
|
|
[self lost:targ :FALSE];
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
handleAI
|
|
|
|
This is a 0.10 addition. Handles any action
|
|
based b_aiflags.
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(void)handleAI
|
|
{
|
|
local entity newt;
|
|
local vector v;
|
|
|
|
// handle ai flags -- note, not all aiflags are handled
|
|
// here, just those that perform some sort of action
|
|
|
|
// wait is used by the ai to stop the bot until his search time expires / or route changes
|
|
|
|
if (b_aiflags & AI_WAIT)
|
|
keys &= ~KEY_MOVE;
|
|
|
|
if (b_aiflags & AI_DOORFLAG) {
|
|
// was on a door when spawned
|
|
// if there is nothing there now
|
|
if (![last_way recognizePlat:FALSE]) {
|
|
newt = [self findThing: "door"]; // this is likely the door responsible (crossfingers)
|
|
|
|
if (b_aiflags & AI_DOOR_NO_OPEN) {
|
|
if (newt.nextthink)
|
|
keys &= ~KEY_MOVE; // wait until it closes
|
|
else {
|
|
[self lost:last_way :FALSE];
|
|
}
|
|
} else {
|
|
if (newt.targetname) {
|
|
newt = find (nil, target, newt.targetname);
|
|
if (newt.health > 0) {
|
|
ent.enemy = newt;
|
|
[self weaponSwitch:1];
|
|
} else {
|
|
// targetDrop (last_way);
|
|
[self targetAdd:[Target forEntity:newt]];
|
|
// [self getPath:newt :TRUE];
|
|
}
|
|
}
|
|
b_aiflags &= ~AI_DOORFLAG;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (b_aiflags & AI_JUMP) {
|
|
if (ent.flags & FL_ONGROUND) {
|
|
[self jump];
|
|
b_aiflags &= ~AI_JUMP;
|
|
}
|
|
} else if (b_aiflags & AI_SUPER_JUMP) {
|
|
if (ent.weapon != 32)
|
|
impulse = 7;
|
|
else if (ent.flags & FL_ONGROUND) {
|
|
b_aiflags &= ~AI_SUPER_JUMP;
|
|
if ([self canRJ]) {
|
|
[self jump];
|
|
ent.v_angle.x = b_angle.x = 80;
|
|
buttons |= 1;
|
|
} else
|
|
[self lost: targets[0] :FALSE];
|
|
}
|
|
}
|
|
if (b_aiflags & AI_SURFACE) {
|
|
if (ent.waterlevel > 2) {
|
|
keys = KEY_MOVEUP;
|
|
buttons |= 4; // swim!
|
|
} else
|
|
b_aiflags &= ~AI_SURFACE;
|
|
}
|
|
if (b_aiflags & AI_RIDE_TRAIN) {
|
|
// simple, but effective
|
|
// this can probably be used for a lot of different
|
|
// things, not just trains (door elevators come to mind)
|
|
if (![last_way recognizePlat:FALSE]) {
|
|
// if there is nothing there now
|
|
keys &= ~KEY_MOVE;
|
|
} else {
|
|
if ([self recognizePlat:FALSE]) {
|
|
v = realorigin (trace_ent) + trace_ent.origin - ent.origin;
|
|
v.z = 0;
|
|
if (vlen (v) < 24)
|
|
keys &= ~KEY_MOVE;
|
|
else {
|
|
b_aiflags |= AI_PRECISION;
|
|
keys |= [self keysForDir:v];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (b_aiflags & AI_PLAT_BOTTOM) {
|
|
newt = [self findThing:"plat"];
|
|
if (newt.state != 1) {
|
|
v = ent.origin - realorigin (newt);
|
|
v.z = 0;
|
|
if (vlen (v) > 96)
|
|
keys &= ~KEY_MOVE;
|
|
else
|
|
[self walkmove:v];
|
|
} else
|
|
b_aiflags &= ~AI_PLAT_BOTTOM;
|
|
}
|
|
if (b_aiflags & AI_DIRECTIONAL) {
|
|
if ((normalize (last_way.origin - ent.origin) * b_dir) > 0.4) {
|
|
b_aiflags &= ~AI_DIRECTIONAL;
|
|
[self lost:targets[0] :TRUE];
|
|
}
|
|
}
|
|
if (b_aiflags & AI_SNIPER) {
|
|
b_aiflags |= AI_WAIT | AI_PRECISION | AI_SNIPER;
|
|
// FIXME: Add a switch to wep command
|
|
// FIXME: increase delay?
|
|
}
|
|
if (b_aiflags & AI_AMBUSH) {
|
|
b_aiflags |= AI_WAIT | AI_AMBUSH;
|
|
// FIXME: Add a switch to wep command
|
|
// FIXME: increase delay?
|
|
}
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
bot_path
|
|
|
|
Bot will follow a route generated by the
|
|
beginRoute set of functions in bot_way.qc.
|
|
This code, while it works pretty well, can get
|
|
confused
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
|
|
-(void)path
|
|
{
|
|
|
|
local Waypoint *jj;
|
|
local entity e, tele;
|
|
|
|
[self checkLost:targets[0]];
|
|
if (!targets[0]) {
|
|
keys=0;
|
|
return;
|
|
}
|
|
if ([self targetOnstack:last_way])
|
|
return; // old waypoint still being hunted
|
|
|
|
jj = [self findRoute:last_way];
|
|
if (!jj) {
|
|
// this is an ugly hack
|
|
if (targets[0].current_way != last_way) {
|
|
local string cn = [targets[0] classname];
|
|
if (cn != "temp_waypoint")
|
|
if (cn != "player")
|
|
[self lost:targets[0] :FALSE];
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// update the bot's special ai features
|
|
|
|
// Readahed types are AI conditions to perform while heading to a waypoint
|
|
// point types are AI flags that should be executed once reaching a waypoint
|
|
|
|
b_aiflags = (jj.flags & AI_READAHEAD_TYPES)
|
|
| (last_way.flags & AI_POINT_TYPES);
|
|
[self targetAdd:jj];
|
|
if (last_way) {
|
|
if ([last_way isLinkedTo:jj] == 2) {
|
|
// waypoints are telelinked
|
|
tele = [self findThing:"trigger_teleport"]; // this is probbly the teleport responsible
|
|
[self targetAdd:[Target forEntity:tele]];
|
|
}
|
|
traceline (last_way.origin, jj.origin, FALSE, ent); // check for blockage
|
|
if (trace_fraction != 1) {
|
|
if (trace_ent.classname == "door"
|
|
&& !(b_aiflags & AI_DOOR_NO_OPEN)) {
|
|
// a door blocks the way
|
|
// linked doors fix
|
|
if (trace_ent.owner)
|
|
trace_ent = trace_ent.owner;
|
|
if ((trace_ent.health > 0) && (ent.enemy == nil)) {
|
|
ent.enemy = trace_ent;
|
|
[self weaponSwitch:1];
|
|
b_aiflags |= AI_BLIND; // nick knack paddy hack
|
|
} else if (trace_ent.targetname) {
|
|
e = find (nil, target, trace_ent.targetname);
|
|
if (e.health > 0) {
|
|
ent.enemy = e;
|
|
[self weaponSwitch:1];
|
|
} else {
|
|
// targetDrop (jj);
|
|
[self targetAdd:[Target forEntity:e]];
|
|
// [self getPath:tele :TRUE];
|
|
b_aiflags |= AI_BLIND; // give a bot a bone
|
|
return;
|
|
}
|
|
}
|
|
} else if (trace_ent.classname == "func_wall") {
|
|
// give up
|
|
[self lost:targets[0] :FALSE];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
// this is used for AI_DRIECTIONAL
|
|
b_dir = normalize (jj.origin - last_way.origin);
|
|
|
|
last_way = jj;
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
Bot Priority Look. What a stupid name. This is where
|
|
the bot finds things it wants to kill/grab.
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
// priority scale
|
|
// 0 - 10 virtually ignore
|
|
// 10 - 30 normal item range
|
|
// 30 - 50 bot will consider this a target worth changing course for
|
|
// 50 - 90 bot will hunt these as vital items
|
|
|
|
// *!* Make sure you add code to checkLost to remove the target *!*
|
|
-(integer)priority:(Bot *)bot
|
|
{
|
|
local integer p;
|
|
|
|
if (ent.health <= 0)
|
|
return 0;
|
|
if (ent.items & IT_INVISIBILITY) //FIXME
|
|
p = 2;
|
|
else if (coop) {
|
|
if (targets[0]) {
|
|
if ([targets[0] classname] == "player")
|
|
if (![targets[0] ishuman])
|
|
return 0;
|
|
}
|
|
p = 100;
|
|
} else if (teamplay && ent.team == bot.ent.team) {
|
|
if (targets[0]) {
|
|
if ([targets[0] classname] == "player")
|
|
return 0;
|
|
}
|
|
p = 100;
|
|
} else
|
|
p = 30;
|
|
return p;
|
|
}
|
|
|
|
-(integer)priorityForThing:(Target *)thing
|
|
{
|
|
local integer p;
|
|
// This is the most executed function in the bot. Careful what you do here.
|
|
if (pointcontents ([thing origin]) < -3)
|
|
return 0;
|
|
|
|
p = [thing priority:self];
|
|
|
|
if (thing.current_way) {
|
|
// check to see if it's unreachable
|
|
if (thing.current_way.distance == -1)
|
|
return 0;
|
|
else
|
|
p += (integer) ((13000 - thing.current_way.distance) * 0.05);
|
|
}
|
|
return p;
|
|
}
|
|
|
|
-(void)lookForCrap:(integer)scope
|
|
{
|
|
local Target *foe, *best = nil;
|
|
local Waypoint *way;
|
|
local integer thatp, bestp;
|
|
local float radius;
|
|
|
|
if (scope == 1)
|
|
radius = 13000;
|
|
else
|
|
radius = 500;
|
|
|
|
foe = [Target forEntity:findradius (ent.origin, radius)];
|
|
bestp = 1;
|
|
while (foe) {
|
|
if (foe != self) {
|
|
thatp = [self priorityForThing:foe];
|
|
if (thatp)
|
|
if (!scope)
|
|
if (!sisible (ent, foe.ent, foe.ent.origin))
|
|
thatp = 0;
|
|
if (thatp > bestp) {
|
|
bestp = thatp;
|
|
best = foe;
|
|
}
|
|
}
|
|
foe = [Target forEntity:foe.ent.chain];
|
|
}
|
|
|
|
way = [Waypoint find:ent.origin radius:radius];
|
|
while (way) {
|
|
thatp = [self priorityForThing:way];
|
|
if (thatp)
|
|
if (!scope)
|
|
if (!sisible (ent, nil, way.origin))
|
|
thatp = 0;
|
|
if (thatp > bestp) {
|
|
bestp = thatp;
|
|
best = way;
|
|
}
|
|
way = way.chain;
|
|
}
|
|
|
|
if (best == nil)
|
|
return;
|
|
if (![self targetOnstack:best]) {
|
|
[self targetAdd:best];
|
|
if (scope) {
|
|
[self getPath:best :FALSE];
|
|
b_aiflags |= AI_WAIT;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
angleSet
|
|
|
|
Sets the bots look keys & b_angle to point at
|
|
the target - used for fighting and just
|
|
generally making the bot look good.
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
-(void)angleSet
|
|
{
|
|
local float h;
|
|
local vector view;
|
|
|
|
if (ent.enemy) {
|
|
if (ent.enemy.items & IT_INVISIBILITY)
|
|
if (random () > 0.2)
|
|
return;
|
|
if (missile_speed == 0)
|
|
missile_speed = 10000;
|
|
if (ent.enemy.solid == SOLID_BSP) {
|
|
view = (((ent.enemy.absmin + ent.enemy.absmax) * 0.5) - ent.origin);
|
|
} else {
|
|
h = vlen (ent.enemy.origin - ent.origin) / missile_speed;
|
|
if (ent.enemy.flags & FL_ONGROUND)
|
|
view = ent.enemy.velocity * h;
|
|
else
|
|
view = (ent.enemy.velocity - (sv_gravity * '0 0 1') * h) * h;
|
|
view = ent.enemy.origin + view;
|
|
// FIXME: ?
|
|
traceline (ent.enemy.origin, view, FALSE, ent);
|
|
view = trace_endpos;
|
|
|
|
if (ent.weapon == 32)
|
|
view = view - '0 0 22';
|
|
|
|
view = normalize (view - ent.origin);
|
|
}
|
|
view = vectoangles (view);
|
|
view.x = view.x * -1;
|
|
b_angle = view;
|
|
} else if (targets[0]) {
|
|
view = [targets[0] realorigin];
|
|
if (targets[0].ent.flags & FL_ITEM)
|
|
view = view + '0 0 48';
|
|
view -= (ent.origin + ent.view_ofs);
|
|
view = vectoangles (view);
|
|
view.x = -view.x;
|
|
b_angle = view;
|
|
} else
|
|
b_angle.x = 0;
|
|
// HACK HACK HACK HACK
|
|
// The bot falls off ledges a lot because of "turning around"
|
|
// so let the bot use instant turn around when not hunting a player
|
|
if (b_skill == 3) {
|
|
keys &= ~KEY_LOOK;
|
|
ent.v_angle = b_angle;
|
|
while (ent.v_angle.x < -180)
|
|
ent.v_angle.x += 360;
|
|
while (ent.v_angle.x > 180)
|
|
ent.v_angle.x -= 360;
|
|
} else if ((ent.enemy == nil || ent.enemy.movetype == MOVETYPE_PUSH)
|
|
&& (targets[0] ? [targets[0] classname] != "player" : 1)) {
|
|
keys &= ~KEY_LOOK;
|
|
ent.v_angle = b_angle;
|
|
while (ent.v_angle.x < -180)
|
|
ent.v_angle.x += 360;
|
|
while (ent.v_angle.x > 180)
|
|
ent.v_angle.x -= 360;
|
|
} else if (b_skill < 2) {
|
|
// skill 2 handled in bot_phys
|
|
if (b_angle.x > 180)
|
|
b_angle.x -= 360;
|
|
keys &= ~KEY_LOOK;
|
|
|
|
if (angcomp (b_angle.y, ent.v_angle.y) > 10)
|
|
keys |= KEY_LOOKLEFT;
|
|
else if (angcomp(b_angle.y, ent.v_angle.y) < -10)
|
|
keys |= KEY_LOOKRIGHT;
|
|
if (angcomp(b_angle.x, ent.v_angle.x) < -10)
|
|
keys |= KEY_LOOKUP;
|
|
else if (angcomp (b_angle.x, ent.v_angle.x) > 10)
|
|
keys |= KEY_LOOKDOWN;
|
|
}
|
|
}
|
|
|
|
/*
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
|
|
BotAI
|
|
|
|
This is the main ai loop. Though called every
|
|
frame, the ai_time limits it's actual updating
|
|
|
|
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
|
|
*/
|
|
|
|
-(void)AI
|
|
{
|
|
// am I dead? Fire randomly until I respawn
|
|
// health < 1 is used because fractional healths show up as 0 on normal
|
|
// playerstatus bars, and the mod probably already compensated for that
|
|
|
|
if (ent.health < 1) {
|
|
buttons = (integer) (random() * 2);
|
|
keys = 0;
|
|
b_aiflags = 0;
|
|
[Waypoint clearMyRoute:self];
|
|
targets[0] = targets[1] = targets[2] = targets[3] = nil;
|
|
ent.enemy = nil;
|
|
last_way = nil;
|
|
return;
|
|
}
|
|
|
|
// stagger the bot's AI out so they all don't think at the same time,
|
|
// causing game 'spikes'
|
|
if (b_skill < 2) {
|
|
if (ai_time > time)
|
|
return;
|
|
|
|
ai_time = time + 0.05;
|
|
if (bot_count > 0) {
|
|
if ((time - stagger_think) < (0.1 / bot_count))
|
|
ai_time += 0.1 / (2 * bot_count);
|
|
} else
|
|
return;
|
|
}
|
|
|
|
if (ent.view_ofs == '0 0 0')
|
|
[self startTopic:7];
|
|
stagger_think = time;
|
|
|
|
// shut the bot's buttons off, various functions will turn them on by AI
|
|
// end
|
|
buttons = 0;
|
|
|
|
// targets[0] is like goalentity in normal Quake monster AI.
|
|
// it's the bot's most immediate target
|
|
if (route_table == self) {
|
|
if (busy_waypoints <= 0) {
|
|
if (waypoint_mode < WM_EDITOR)
|
|
[self lookForCrap:TRUE];
|
|
}
|
|
b_aiflags = 0;
|
|
keys = 0;
|
|
} else if (targets[0]) {
|
|
[self movetogoal];
|
|
[self path];
|
|
} else {
|
|
if (waypoint_mode < WM_EDITOR) {
|
|
if (route_failed) {
|
|
[self roam];
|
|
route_failed = 0;
|
|
} else if (![self beginRoute]) {
|
|
[self lookForCrap:FALSE];
|
|
}
|
|
keys = 0;
|
|
} else {
|
|
b_aiflags = AI_WAIT;
|
|
keys = 0;
|
|
}
|
|
}
|
|
|
|
// angleSet points the bot at it's goal (ent.enemy or targets[0])
|
|
[self angleSet];
|
|
|
|
// fight my enemy. Enemy is probably a field QC coders will most likely
|
|
// use a lot for their own needs, since it's unused on a normal player
|
|
// FIXME
|
|
if (ent.enemy)
|
|
[self fightStyle];
|
|
else if (random () < 0.2)
|
|
if (random () < 0.2)
|
|
[self weaponSwitch:-1];
|
|
[self dodgeStuff];
|
|
|
|
// checks to see if bot needs to start going up for air
|
|
if (ent.waterlevel > 2) {
|
|
if (time > (ent.air_finished - 2)) {
|
|
traceline (ent.origin, ent.origin + '0 0 6800', TRUE, ent);
|
|
if (trace_inopen) {
|
|
keys = KEY_MOVEUP;
|
|
buttons |= 4; // swim!
|
|
return; // skip ai flags for now - this is life or death
|
|
}
|
|
}
|
|
}
|
|
|
|
// b_aiflags handling
|
|
// don't want chat to screw him up if he's concentrating
|
|
if (b_aiflags)
|
|
[self handleAI];
|
|
else
|
|
[self chat];
|
|
}
|
|
|
|
@end
|