Files
quakemapping/mod_slipgate/source/server/ai.qc
2019-12-30 17:23:05 +01:00

913 lines
18 KiB
Plaintext

/*QUAKED path_corner (0.5 0.3 0) (-8 -8 -8) (8 8 8) X X X X X X X X NOT_IN_EASY NOT_IN_NORMAL NOT_IN_HARD NOT_IN_DM
If a monster targets a path_corner, they will move towards it.
Once a monster hits a path_corner, they will move to the next targeted path_corner.
You can set "pausetime" on a path_corner to enforce a delay before the monster moves
to the next target.
*/
void() path_corner =
{
if (!self.targetname)
{
objerror ("path_corner: no targetname");
remove(self);
return;
}
self.movetype = MOVETYPE_NONE;
self.solid = SOLID_TRIGGER;
setsize (self, '-8 -8 -8', '8 8 8');
};
//============================================================================
/*
=============
range
returns the range catagorization of an entity reletive to self
0 melee range, will become hostile even if back is turned
1 visibility and infront, or visibility and show hostile
2 infront and show hostile
3 only triggered by damage
=============
*/
float(entity targ) range =
{
float r = vlen (self.origin + self.view_ofs - targ.origin + targ.view_ofs);
if (r < 500)
{
if (r < 120)
return RANGE_MELEE;
else
return RANGE_NEAR;
}
else
{
if (r < 1000)
return RANGE_MID;
else
return RANGE_FAR;
}
};
/*
=============
visible
returns 1 if the entity is visible to self, even if not infront ()
=============
*/
float (entity targ) visible =
{
traceline (self.origin + self.view_ofs, targ.origin + targ.view_ofs, TRUE, self);
if (trace_inwater)
{
if (trace_inopen)
return FALSE;
}
if (trace_fraction == 1)
return TRUE;
return FALSE;
};
/*
=============
infront
returns 1 if the entity is in front (in sight) of self
=============
*/
float(entity targ) infront =
{
makevectors (self.angles);
vector vec = normalize (targ.origin - self.origin);
float dot = vec * v_forward;
if (dot > 0.3)
return TRUE;
else
return FALSE;
};
void() HuntTarget =
{
self.goalentity = self.enemy;
self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
self.think = self.th_run;
self.nextthink = time + 0.1;
SUB_AttackFinished(1); // wait a while before first attack
};
void() SightSound =
{
if (self.classname == "monster_ogre")
sound (self, CHAN_BODY, "ogre/ogwake.wav", 1, ATTN_NORM);
else if (self.classname == "monster_knight")
sound (self, CHAN_BODY, "knight/ksight.wav", 1, ATTN_NORM);
else if (self.classname == "monster_shambler")
sound (self, CHAN_BODY, "shambler/ssight.wav", 1, ATTN_NORM);
else if (self.classname == "monster_demon1")
sound (self, CHAN_BODY, "demon/sight2.wav", 1, ATTN_NORM);
else if (self.classname == "monster_wizard")
sound (self, CHAN_BODY, "wizard/wsight.wav", 1, ATTN_NORM);
else if (self.classname == "monster_zombie")
sound (self, CHAN_BODY, "zombie/z_idle.wav", 1, ATTN_NORM);
else if (self.classname == "monster_dog")
sound (self, CHAN_BODY, "dog/dsight.wav", 1, ATTN_NORM);
else if (self.classname == "monster_hell_knight")
sound (self, CHAN_BODY, "hknight/sight1.wav", 1, ATTN_NORM);
else if (self.classname == "monster_tarbaby")
sound (self, CHAN_BODY, "blob/sight1.wav", 1, ATTN_NORM);
else if (self.classname == "monster_enforcer")
{
float r = randomrange(4);
if (r == 0)
sound (self, CHAN_BODY, "enforcer/sight1.wav", 1, ATTN_NORM);
else if (r == 1)
sound (self, CHAN_BODY, "enforcer/sight2.wav", 1, ATTN_NORM);
else if (r == 2)
sound (self, CHAN_BODY, "enforcer/sight3.wav", 1, ATTN_NORM);
else
sound (self, CHAN_BODY, "enforcer/sight4.wav", 1, ATTN_NORM);
}
else if (self.classname == "monster_army")
sound (self, CHAN_BODY, "soldier/sight1.wav", 1, ATTN_NORM);
else if (self.classname == "monster_shalrath")
sound (self, CHAN_BODY, "shalrath/sight.wav", 1, ATTN_NORM);
};
void() FoundTarget =
{
if (self.enemy.classname == "player")
{
sight_entity = self;
sight_entity_time = time;
}
self.show_hostile = time + 1; // wake up other monsters
SightSound ();
HuntTarget ();
};
/*==========
FindTarget
==========*/
float() FindTarget =
{
if (intermission_running)
return FALSE;
if (sight_entity_time >= time - 0.1 && !(self.spawnflags & MONSTER_AMBUSH) )
{
entity client = sight_entity;
if (client.enemy == self.enemy)
return FALSE;
}
else
{
client = checkclient ();
if (!client)
return FALSE;
}
if (client == self.enemy)
return FALSE;
if (client.flags & FL_NOTARGET)
return FALSE;
if (client.items & IT_INVISIBILITY)
return FALSE;
float r = range(client);
if (r == RANGE_FAR)
return FALSE;
if (!visible (client))
return FALSE;
if (r == RANGE_NEAR)
{
if (client.show_hostile < time && !infront (client))
return FALSE;
}
else if (r == RANGE_MID)
{
if (!infront (client))
return FALSE;
}
//
// got one
//
self.enemy = client;
if (self.enemy.classname != "player")
{
self.enemy = self.enemy.enemy;
if (self.enemy.classname != "player")
{
self.enemy = world;
return FALSE;
}
}
FoundTarget ();
return TRUE;
};
//=============================================================================
/*==========
ai_movetogoal
==========*/
float (float dist) SV_CloseEnough =
{
if (self.goalentity.absmin.x > self.absmax.x + dist)
return FALSE;
if (self.goalentity.absmax.x < self.absmin.x - dist)
return FALSE;
if (self.goalentity.absmin.y > self.absmax.y + dist)
return FALSE;
if (self.goalentity.absmax.y < self.absmin.y - dist)
return FALSE;
if (self.goalentity.absmin.z > self.absmax.z + dist)
return FALSE;
if (self.goalentity.absmax.z < self.absmin.z - dist)
return FALSE;
return TRUE;
};
//.float blocked_time;
//.float blocked_count;
void(entity goal, float dist) ai_movetogoal =
{
// new target
if (self.goalentity != goal)
{
self.goalentity = goal;
self.ideal_yaw = vectoyaw(goal.origin - self.origin);
}
vector oldorigin = self.origin;
movetogoal(dist);
if (self.origin != oldorigin)
return;
if (SV_CloseEnough(dist))
{
//dprint("SV_CloseEnough\n");
self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin);
changeyaw();
if (walkmove(self.angles_y, dist))
return;
float ofs;
if (self.lefty)
ofs = 45;
else
ofs = -45;
if (walkmove(self.angles_y + ofs, dist))
return;
self.lefty = 1 - self.lefty;
walkmove(self.angles_y - ofs, dist);
}
/*
if (self.blocked_count > 10)
{
self.blocked_count = 0;
self.blocked_time = time + 5;
}
if (self.blocked_time > time)
{
movetogoal(dist);
return;
}
self.ideal_yaw = vectoyaw(goal.origin - self.origin);
changeyaw();
if (walkmove(self.angles_y, dist))
return;
self.blocked_count = self.blocked_count + 1;
float ofs;
if (self.lefty)
ofs = 45;
else
ofs = -45;
if (walkmove(self.angles_y + ofs, dist))
return;
self.lefty = 1 - self.lefty;
walkmove(self.angles_y - ofs, dist);*/
};
void(float dist) ai_forward =
{
walkmove(self.angles_y, dist);
};
void(float dist) ai_back =
{
walkmove(self.angles_y + 180, dist);
};
void(float dist) ai_pain =
{
ai_back(dist);
};
void(float dist) ai_painforward =
{
ai_forward(dist);
};
void(float dist) ai_walk =
{
if (FindTarget())
return;
ai_movetogoal(self.movetarget, dist);
if (vlen(self.origin - self.movetarget.origin) < 32)
{
entity targ = find(world, targetname, self.movetarget.target);
if (targ.classname == "path_corner")
{
self.movetarget = self.goalentity = targ;
self.ideal_yaw = vectoyaw(targ.origin - self.origin);
if (targ.pausetime > 0)
{
self.think = self.th_stand;
self.pausetime = time + targ.pausetime;
}
}
else
{
self.think = self.th_stand;
self.pausetime = -1;
}
}
};
void() ai_stand =
{
if (FindTarget())
return;
if (time > self.pausetime && self.pausetime > 0)
{
if (self.movetarget)
{
self.th_walk();
return;
}
}
// random angle turns
if (time > self.search_time)
{
self.search_time = time + 1 + random() * 2;
if (self.angles_y != self.ideal_yaw)
self.angles_y = self.ideal_yaw;
else
{
if (random() < 0.5)
self.angles_y = self.ideal_yaw + random() * 30;
else
self.angles_y = self.ideal_yaw - random() * 30;
}
}
};
void() ai_turn =
{
if (FindTarget())
return;
changeyaw ();
};
float(float ang) FacingIdeal =
{
float delta = angcomp(self.ideal_yaw, self.angles_y);
if (delta > ang)
return FALSE;
if (delta < -ang)
return FALSE;
/*float delta = anglemod(self.angles_y - self.ideal_yaw);
if (delta > 45 && delta < 315)
return FALSE;
*/
return TRUE;
};
//=============================================================================
float() WizardCheckAttack;
float() DogCheckAttack;
float() CheckAnyAttack =
{
if (self.classname == "monster_ogre")
return OgreCheckAttack ();
if (!enemy_vis)
return FALSE;
if (self.classname == "monster_army")
return SoldierCheckAttack ();
if (self.classname == "monster_shambler")
return ShamCheckAttack ();
if (self.classname == "monster_demon1")
return DemonCheckAttack ();
if (self.classname == "monster_dog")
return DogCheckAttack ();
if (self.classname == "monster_wizard")
return WizardCheckAttack ();
return CheckAttack ();
};
/*==========
ai_run_melee
Turn until facing enemy then launch melee attack
==========
*/
void() ai_run_melee =
{
self.ideal_yaw = enemy_yaw;
changeyaw();
if (FacingIdeal(90))
{
self.th_melee ();
self.attack_state = AS_STRAIGHT;
}
};
/*==============
ai_run_missile
Turn until facing enemy then launch missile attack
==========*/
void() ai_run_missile =
{
if (self.attack_state == AS_SPAM)
{
self.ideal_yaw = vectoyaw(self.enemy_last - self.origin);
changeyaw();
}
else
{
self.ideal_yaw = enemy_yaw;
changeyaw();
// if the enemy has dropped out of sight, cancel the attack
if (!enemy_vis)
{
dprint("ai_run_missile: not visible\n");
self.attack_state = AS_STRAIGHT;
return;
}
}
if (FacingIdeal(90))
{
if (self.attack_state == AS_SPAM)
self.th_spam();
else
self.th_missile();
self.attack_state = AS_STRAIGHT;
}
};
/*==========
ai_run_slide
Face the enemy and strafe sideways
==========*/
void(float dist) ai_run_slide =
{
self.ideal_yaw = enemy_yaw;
changeyaw();
float ofs;
if (self.lefty)
ofs = 90;
else
ofs = -90;
if (walkmove (self.ideal_yaw + ofs, dist))
return;
self.lefty = 1 - self.lefty;
walkmove (self.ideal_yaw - ofs, dist);
};
#define ROUTE_MAX 256
float route_busy;
.float route;
.entity route_table[ROUTE_MAX];
.float distance;
.float route_time;
/*============
ClearRoute
clear the monsters current route
============*/
void() ClearRoute =
{
self.route = -1;
for (float i = 0; i < ROUTE_MAX; i = i + 1)
self.route_table[i] = world;
};
/*============
ClearWayTable
clear the waypoint table (which is shared between all monsters)
marks all waypoints as not visited and infinite distance
============*/
void() ClearWayTable =
{
entity e = way_head;
while (e)
{
e.state = -1;
e.distance = -1;
e.last_way = world;
e = e._next;
}
};
/*==========
CheckDoors
check if a door is in the way of two waypoints while checking a route
==========*/
float(entity e) CheckDoors =
{
traceline(self.enemy.origin, e.origin, TRUE, self.enemy);
if (trace_fraction == 1)
return TRUE;
if (trace_ent == e)
return TRUE;
if (trace_ent.classname == "door")
{
// we check .owner because it's the master door
if (trace_ent.owner.items) // key door
return FALSE;
if (trace_ent.owner.health) // shootable door
return FALSE;
if (trace_ent.owner.targetname != "") // triggered door
return FALSE;
return TRUE; // regular door with a trigger field
}
dprint("CheckDoors: \n");
dprint(trace_ent.classname);
dprint("\n");
return FALSE; // something else in the way
};
/*==========
CheckRoute
called from RouteThink to update the distances between the waypoints
==========*/
void(entity e) CheckRoute =
{
if (!e)
return;
if (e.state > 0)
return;
if (!CheckDoors(e))
return;
float dist = self.enemy.distance;
dist = dist + vlen(self.enemy.origin - e.origin);
dist = dist + random() * 32; // add some fuzzyness
if ((e.distance == -1) || (dist < e.distance))
{
e.distance = dist;
e.last_way = self.enemy;
}
};
/*==========
RouteThink
return the shortest path to the goal waypoint
==========*/
void() RouteThink =
{
entity e;
// the monster died while calculating a route - free up the table
if (self.owner.health <= 0)
{
route_busy = 0;
remove(self);
fixer = world;
return;
}
// visit all waypoints linking to current waypoint and update their distance
CheckRoute(self.enemy.target1);
CheckRoute(self.enemy.target2);
CheckRoute(self.enemy.target3);
CheckRoute(self.enemy.target4);
// mark this waypoint as visited
self.enemy.state = 1;
// found route
if (self.goalentity.state == 1)
{
//dprint("RouteThink: found route\n");
e = self.goalentity;
float i = 0;
while (e)
{
self.owner.route_table[i] = e;
e = e.last_way;
if (e) // don't count the final waypoint, as it's the one the monster is already at
i = i + 1;
}
self.owner.route = i - 1; // because route_table is a zero indexed array
self.owner.route_time = time + 5;
route_busy = FALSE;
remove(self);
fixer = world;
return;
}
// find the nearest, unvisited waypoint
entity best = world;
float bdist = -1;
e = way_head;
while (e)
{
if (e.state == -1) // not visited
{
if (e.distance > 0) // we use -1 for "infinite distance"
{
if (e.distance < bdist || bdist == -1)
{
best = e;
bdist = e.distance;
}
}
}
e = e._next;
}
// no valid route exists between the two waypoints
if (best == world)
{
//dprint("RouteThink: no valid route exists\n");
route_busy = FALSE;
remove(self);
fixer = world;
return;
}
// update the next waypoint
self.enemy = best;
self.think = RouteThink;
self.nextthink = time;
};
/*============
StartRoute
called by monsters when they want to find a path between waypoint A and B
uses thinks to avoid tripping the runaway counter (RouteThink)
============*/
void(entity e1, entity e2) StartRoute =
{
// no waypoints exist in map
if (!waypoints)
return;
// another monster is already calculating a route
if (route_busy)
return;
// sanity check
if (e1 == world || e2 == world || e1 == e2)
return;
//dprint("StartRoute\n");
// mark the table as busy and clear it for use
route_busy = TRUE;
ClearWayTable();
// the first waypoint is always distance zero
e1.distance = 0;
if (!fixer)
fixer = spawn();
fixer.owner = self;
fixer.enemy = e1; // start waypoint
fixer.goalentity = e2; // end waypoint
fixer.nextthink = time;
fixer.think = RouteThink;
};
/*==========
FindWaypoint
==========*/
entity(entity monster, entity start) FindWaypoint =
{
if (!waypoints)
return world;
if (start != world)
{
float best_dist = vlen(start.origin - monster.origin);
entity best = start;
}
else
{
best_dist = -1;
best = world;
}
if (best_dist < 20)
return best;
entity w = way_head;
while(w)
{
float way_dist = vlen(w.origin - monster.origin);
if (way_dist < best_dist || best_dist == -1)
{
if (wisible(monster, w))
{
best_dist = way_dist;
best = w;
}
}
w = w._next;
}
if (best_dist < 256)
{
//dprint("FindWaypoint: found\n");
return best;
}
else
{
//dprint("FindWaypoint: world\n");
return world;
}
};
/*
=============
ai_run
The monster has an enemy it is trying to kill
=============
*/
void(float dist) ai_run =
{
// stop all attacks during the intermission
if (intermission_running)
{
ClearRoute();
self.enemy = self.goalentity = world;
self.th_stand();
return;
}
// our enemy has died, so look for a new one
if (self.enemy.health <= 0)
{
ClearRoute();
self.enemy = self.goalentity = world;
// look for previous enemy
if (self.oldenemy.health > 0)
{
self.enemy = self.oldenemy;
HuntTarget();
}
else
{
self.th_stand();
return;
}
}
// FIXME: get rid of globals
enemy_vis = visible(self.enemy);
enemy_infront = infront(self.enemy);
enemy_range = range(self.enemy);
enemy_yaw = vectoyaw(self.enemy.origin - self.origin);
if (enemy_vis)
{
self.enemy_last = self.enemy.origin + self.enemy.view_ofs;
self.search_time = time + 5;
}
// look for other players in co-op
if (coop && self.search_time < time)
{
if (FindTarget())
return;
}
// we've started an attack, so make sure we're facing the right way before launching
if (self.attack_state == AS_MISSILE || self.attack_state == AS_SPAM)
{
ai_run_missile();
return;
}
if (self.attack_state == AS_MELEE)
{
ai_run_melee();
return;
}
// check for beginning an attack
if (CheckAnyAttack())
return;
// used by scrags
if (self.attack_state == AS_SLIDING)
{
ai_run_slide(dist);
return;
}
// no waypoints in map, so just use regular movement
if (waypoint_mode < WM_LOADED || !waypoints)
{
ai_movetogoal(self.enemy, dist);
return;
}
// we have an enemy, but no route, so find a route to the enemy
if (self.route < 0)
{
//dprint("Finding route\n");
self.current_way = FindWaypoint(self, self.current_way);
self.enemy.current_way = FindWaypoint(self.enemy, self.enemy.current_way);
StartRoute(self.current_way, self.enemy.current_way);
ai_movetogoal(self.enemy, dist);
return;
}
// monster has failed to move to the next waypoint, so drop the route
if (time > self.route_time)
{
//dprint("Dropping route\n");
ClearRoute();
ai_movetogoal(self.enemy, dist);
return;
}
// periodically refresh enemy waypoint
if (!route_busy)
{
self.enemy.current_way = FindWaypoint(self.enemy, self.enemy.current_way);
if (self.route_table[0] != self.enemy.current_way)
StartRoute(self.current_way, self.enemy.current_way);
}
// move to the next waypoint
entity w = self.route_table[self.route];
ai_movetogoal(w, dist);
// hit next waypoint
if (vlen(self.origin - w.origin) < 64)
{
self.route_time = time + 5;
self.current_way = self.route_table[self.route];
self.route = self.route - 1;
// hit last waypoint
if (self.route < 0)
{
dprint("Hit last waypoint\n");
ClearRoute();
}
}
};