2022-02-08 18:42:28 +00:00
/*
server / ai / ai_core . qc
ai stuff
2022-11-18 02:25:29 +00:00
Copyright ( C ) 2021 - 2022 NZ : P Team
2022-02-08 18:42:28 +00:00
This program is free software ; you can redistribute it and / or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation ; either version 2
of the License , or ( at your option ) any later version .
This program is distributed in the hope that it will be useful ,
but WITHOUT ANY WARRANTY ; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE .
See the GNU General Public License for more details .
You should have received a copy of the GNU General Public License
along with this program ; if not , write to :
Free Software Foundation , Inc .
59 Temple Place - Suite 330
Boston , MA 02111 - 1307 , USA
*/
void ( float what ) play_sound_z ;
2022-12-21 00:50:56 +00:00
void ( ) LinkZombiesHitbox ;
2022-02-08 18:42:28 +00:00
void ( ) path_corner_touch =
{
self . origin_z = self . origin_z + 32 ;
setorigin ( self , self . origin ) ;
self . classname = " path_corner " ;
self . movetype = MOVETYPE_NONE ;
self . solid = SOLID_NOT ;
self . touch = SUB_Null ;
setsize ( self , ' 0 0 0 ' , ' 0 0 0 ' ) ;
if ( ! self . target )
{
if ( self . spawnflags & 1 )
return ;
bprint ( PRINT_HIGH , " path_corner with name: " ) ;
bprint ( PRINT_HIGH , self . targetname ) ;
bprint ( PRINT_HIGH , " has no target! \n " ) ;
}
}
//We want the path corner to drop to the ground and then we set it up 32 units so it's exact
void ( ) path_corner =
{
self . classname = " path_corner_set " ;
self . movetype = MOVETYPE_BOUNCE ;
self . solid = SOLID_BBOX ;
self . touch = path_corner_touch ;
setsize ( self , ' 0 0 0 ' , ' 0 0 0 ' ) ;
} ;
void removeZombie ( ) ;
void ( ) Respawn =
{
Current_Zombies - - ;
removeZombie ( ) ;
} ;
entity ( entity blarg ) find_new_enemy =
{
2022-12-22 16:44:13 +00:00
entity targets ;
entity best_target ;
float best_distance ;
float distance ;
best_distance = 10000 ;
best_target = world ;
if ( blarg . classname = = " ai_zombie " | | blarg . classname = = " ai_dog " ) {
// Monkey Bomb (TODO -- if multiple, target first one thrown)
targets = find ( world , classname , " monkey_bomb " ) ;
if ( targets ! = world & & blarg . classname ! = " ai_dog " ) {
best_target = targets ;
return best_target ;
2022-02-08 18:42:28 +00:00
}
2022-12-22 16:44:13 +00:00
// Now, try and find a viable player
targets = find ( world , classname , " player " ) ;
while ( targets ! = world ) {
// Don't target downed players.
2022-12-29 00:32:21 +00:00
if ( targets . downed = = true | | targets . isspec = = true ) {
2022-12-22 16:44:13 +00:00
targets = find ( targets , classname , " player " ) ;
2022-10-25 16:49:02 +00:00
continue ;
}
2022-12-22 16:44:13 +00:00
// Found one, let's see if it's closer than our last ideal target.
distance = vlen ( blarg . origin - targets . origin ) ;
if ( distance < best_distance ) {
best_target = targets ;
best_distance = distance ;
2022-02-08 18:42:28 +00:00
}
2022-12-22 16:44:13 +00:00
// Continue iterating
targets = find ( targets , classname , " player " ) ;
2022-02-08 18:42:28 +00:00
}
2022-12-22 16:44:13 +00:00
// Return a good player if we found one.
if ( best_target ! = world )
return best_target ;
// We couldn't find a good player. How about a horde point?
targets = find ( world , classname , " zombie_horde_point " ) ;
while ( targets ! = world ) {
// Found one, let's see if it's closer than our last ideal target.
distance = vlen ( blarg . origin - targets . origin ) ;
if ( distance < best_distance ) {
best_target = targets ;
best_distance = distance ;
}
// Continue iterating
targets = find ( targets , classname , " zombie_horde_point " ) ;
}
// Return a horde point if we found one.
if ( best_target ! = world )
return best_target ;
2022-02-08 18:42:28 +00:00
}
2022-12-22 16:44:13 +00:00
// We didn't have much luck, just return the world.
return best_target ;
2022-02-08 18:42:28 +00:00
} ;
float ( ) avoid_zombies =
{
local entity ent ;
ent = findradius ( self . origin , 23 ) ; //22.67
makevectors ( self . angles ) ;
float left_amount , right_amount ;
left_amount = right_amount = 0 ;
while ( ent )
{
if ( ent . classname = = " ai_zombie " & & ent ! = self )
{
local vector vec_b ;
local float dot ;
//vec_b = normalize(self.origin - ent.origin);
//dot = v_right * vec_b;
//dot = self.angles_y - (dot > 0.5) ? 90 : 270;
vec_b = ( self . origin - ent . origin ) ;
dot = ( v_right_x * vec_b_x ) + ( v_right_y * vec_b_y ) ; //dot product
if ( dot > 0 ) // on right
right_amount + + ;
else // on left
left_amount + + ;
}
ent = ent . chain ;
}
if ( left_amount + right_amount = = 0 )
return 0 ;
return ( left_amount > right_amount ) ? 15 : - 15 ;
} ;
float ( ) push_away_zombies =
{
local entity ent ;
ent = findradius ( self . origin , 11 ) ;
float return_value ;
return_value = 0 ;
while ( ent )
{
if ( ent . classname = = " ai_zombie " & & ent ! = self )
{
vector push ;
push = ent . origin - self . origin ;
push_z = 0 ;
push = normalize ( push ) * 10 ;
ent . velocity + = push ;
return_value = 1 ;
}
ent = ent . chain ;
}
return return_value ;
}
void ( float dist , vector vec ) do_walk_to_vec =
{
if ( dist = = 0 )
return ;
self . ideal_yaw = vectoyaw ( vec - self . origin ) ;
if ( self . outside = = false )
{
2023-03-04 18:46:39 +00:00
// this is a performance net negative for like 0 gain in horde space.
//push_away_zombies();
2022-02-08 18:42:28 +00:00
}
ChangeYaw ( ) ;
vector new_velocity ;
float len ;
len = vlen ( self . origin - vec ) ;
if ( dist > len ) //if we're moving past our goal position
{
dist = len ;
}
//This movement method is moving directly towards the goal, regardless of where our yaw is facing (fixes several issues)
new_velocity = normalize ( vec - self . origin ) * dist * 10 ;
new_velocity_z = self . velocity_z ;
self . velocity = new_velocity ;
} ;
void ( float dist ) do_walk =
{
do_walk_to_vec ( dist , self . goalentity . origin ) ;
} ;
void ( float dist ) walk_to_window =
{
do_walk_to_vec ( dist , self . goalorigin ) ;
} ;
// unused
void ( vector org , float scale ) interpolateToVector =
{
self . origin_x + = ( org_x - self . origin_x ) * scale ;
self . origin_y + = ( org_y - self . origin_y ) * scale ;
setorigin ( self , self . origin ) ;
self . zoom = 1 ; //value to let engine know to not check for collisions
}
float ( vector where ) nearby =
{
2023-01-13 17:29:08 +00:00
if ( self . classname = = " ai_zombie " | | self . classname = = " ai_dog " )
2022-02-08 18:42:28 +00:00
{
float xdist ;
float ydist ;
float zdist ;
xdist = fabs ( self . origin_x - where_x ) ;
ydist = fabs ( self . origin_y - where_y ) ;
zdist = fabs ( self . origin_z - where_z ) ;
if ( xdist < 4 & & ydist < 4 ) //horizontal distance is fairly close
{
if ( zdist < 50 ) //vertical distance just has to be arbitrarily close
{
return 1 ;
}
}
return 0 ;
}
return 0 ;
} ;
void ( float dist ) Window_Walk =
{
if ( self . reload_delay < time )
Respawn ( ) ;
if ( self . hop_step = = 0 ) //following path corners
{
if ( self . goalentity = = world )
{
if ( ( ! self . target ) & & ( self . outside = = TRUE ) )
{
bprint ( PRINT_HIGH , " Error: Outside zombie spawn point has no target. \n " ) ;
Respawn ( ) ;
}
self . goalentity = find ( world , targetname , self . target ) ;
if ( ! self . goalentity )
{
bprint ( PRINT_HIGH , " Error: Outside zombie spawn point target does not exist. \n " ) ;
Respawn ( ) ;
}
}
if ( self . goalentity . classname = = " path_corner " & & nearby ( self . goalentity . origin ) )
{
if ( self . goalentity . spawnflags & 1 ) //this path corner sets zombie on inside.
{
self . outside = FALSE ;
self . goalentity = world ;
self . enemy = find_new_enemy ( self ) ;
self . th_walk ( ) ;
return ;
}
self . reload_delay = time + 30 ;
self . goalentity = find ( world , targetname , self . goalentity . target ) ;
//Assumption is that when the zombie is outside, we can always walk from one path_corner to the next in a straight line, any devation should be corrected by the mapper
}
do_walk ( dist ) ;
if ( self . goalentity . classname = = " window " )
{
if ( ! self . goalentity . box1owner )
{
//self.used = WBOX1;
self . goalentity . box1owner = self ;
self . goalorigin = self . goalentity . box1 ;
self . hop_step = 3 ;
self . reload_delay = time + 30 ;
}
else if ( ! self . goalentity . box2owner )
{
//self.used = WBOX2;
self . goalentity . box2owner = self ;
self . goalorigin = self . goalentity . box2 ;
self . hop_step = 3 ;
self . reload_delay = time + 30 ;
}
else if ( ! self . goalentity . box3owner )
{
//self.used = WBOX3;
self . goalentity . box3owner = self ;
self . goalorigin = self . goalentity . box3 ;
self . hop_step = 3 ;
self . reload_delay = time + 30 ;
}
else if ( vlen ( self . origin - self . goalentity . origin ) < 150 )
{
//we don't claim the idlebox
//self.used = WIDLEBOX;
self . goalorigin = self . goalentity . idlebox ;
self . hop_step = 1 ;
self . reload_delay = time + 30 ;
}
//else we continue walking to window until we either find one that's good, or we are close enough to chase idle_spot
}
}
else if ( self . hop_step = = 1 ) //walking to the window's idle location
{
if ( nearby ( self . goalorigin ) )
{
self . hop_step = 2 ;
self . reload_delay = time + 30 ;
self . th_idle ( ) ;
}
else
{
walk_to_window ( dist ) ;
}
}
else if ( self . hop_step = = 2 ) //we're at idle box, waiting for a window attack box to be free...
{
if ( ! self . goalentity . box1owner )
{
//self.used = WBOX1;
self . goalentity . box1owner = self ;
self . goalorigin = self . goalentity . box1 ;
self . hop_step = 3 ;
self . reload_delay = time + 30 ;
self . th_walk ( ) ;
}
else if ( ! self . goalentity . box2owner )
{
//self.used = WBOX2;
self . goalentity . box2owner = self ;
self . goalorigin = self . goalentity . box2 ;
self . hop_step = 3 ;
self . reload_delay = time + 30 ;
self . th_walk ( ) ;
}
else if ( ! self . goalentity . box3owner )
{
//self.used = WBOX3;
self . goalentity . box3owner = self ;
self . goalorigin = self . goalentity . box3 ;
self . hop_step = 3 ;
self . reload_delay = time + 30 ;
self . th_walk ( ) ;
}
}
else if ( self . hop_step = = 3 ) //walking to window attack box
{
if ( nearby ( self . goalorigin ) )
{
self . hop_step = 4 ;
self . reload_delay = time + 30 ;
self . th_idle ( ) ;
}
else
{
walk_to_window ( dist ) ;
}
}
else if ( self . hop_step = = 4 ) //attacking box
{
if ( self . chase_time < time )
{
if ( self . angles_z ! = self . goalentity . angles_z )
{
self . ideal_yaw = self . goalentity . angles_z ;
ChangeYaw ( ) ;
return ;
}
if ( self . goalentity . health > 0 )
{
self . reload_delay = time + 30 ;
self . th_melee ( ) ;
if ( rounds < = 5 )
self . chase_time = time + 1.5 ;
else
self . chase_time = time + 0.75 ;
return ;
}
}
if ( self . goalentity . health < = 0 )
{
self . outside = 2 ;
self . chase_time = 0 ;
self . hop_step = 0 ;
}
else return ;
}
} ;
//
// kind of a shoddy fix, but essentially what we do to fix
// issues with zomb ents colliding with each other during hopping
// is make sure we wait a bit longer before freeing the window for
// another usage.
//
void ( ) free_window =
{
self . usedent = world ;
}
void ( float dist ) Window_Hop =
{
if ( self . hop_step = = 0 ) {
if ( self . goalentity . box1owner = = self ) { //we're at center box.
self . hop_step = 4 ;
} else {
self . hop_step = 1 ; //wait for box1 to be free so we can claim it and walk to it.
self . th_idle ( ) ;
}
}
if ( self . hop_step = = 1 ) { //waiting idly for box1 to be free, when free, we will claim it.
if ( ! self . goalentity . box1owner | | self . goalentity . box1owner = = self ) {
self . goalentity . box1owner = self ;
if ( self . goalentity . box2owner = = self )
self . goalentity . box2owner = world ;
if ( self . goalentity . box3owner = = self )
self . goalentity . box3owner = world ;
//self.used = WBOX1;
self . goalorigin = self . goalentity . box1 ;
self . hop_step = 2 ;
self . th_walk ( ) ;
}
}
if ( self . hop_step = = 2 ) { //we've claimed it, walk to box1
if ( nearby ( self . goalorigin ) ) {
self . hop_step = 4 ;
self . angles = self . goalentity . angles ;
} else {
walk_to_window ( dist ) ;
}
}
if ( self . hop_step = = 4 & & self . chase_time < time ) { //we're at this step because we already own box1, so don't even check if window is busy...
if ( ! self . goalentity . usedent ) {
self . hop_step = 5 ;
self . angles = self . goalentity . angles ;
self . goalentity . box1owner = world ; //free box1
self . goalentity . usedent = self ; //we own the window
//don't need to set goalorigin here
//self.used = WWINDOW;
self . chase_time = 0 ;
self . th_windowhop ( ) ;
return ;
} else {
self . tries + + ;
self . chase_time = time + 0.2 ;
if ( self . tries > 10 ) {
// wait enough time before freeing window, to give time for zomb to move.
self . goalentity . think = free_window ;
self . goalentity . nextthink = time + 0.5 ;
//self.goalentity.usedent = world;//free up the window if we've been waiting to hop
}
}
}
if ( self . hop_step = = 6 ) {
self . outside = FALSE ;
//self.goalentity.usedent = world;//free up the window, we're done hopping it
//self.used = 0;
self . goalentity . think = free_window ;
self . goalentity . nextthink = time + 0.5 ;
self . goalentity = world ;
self . enemy = find_new_enemy ( self ) ;
//self.th_die();
self . th_walk ( ) ;
2022-12-28 02:50:52 +00:00
//LinkZombiesHitbox();
2022-12-21 00:50:56 +00:00
//bprint (PRINT_HIGH, "Linked hitboxes");
2022-02-08 18:42:28 +00:00
}
}
float ( ) TryWalkToEnemy =
{
//was tracebox
float TraceResult ;
TraceResult = tracemove ( self . origin , VEC_HULL_MIN , VEC_HULL_MAX , self . enemy . origin , TRUE , self ) ;
if ( TraceResult = = 1 ) {
self . goalentity = self . enemy ;
self . chase_time = time + 7 ;
return 1 ;
} else {
return 0 ;
}
} ;
void ( ) PathfindToEnemy =
{
float path_result ;
float path_failure ;
//just to stop any warns.
path_failure = 0 ;
2023-02-05 21:03:57 +00:00
# ifndef FTE
2022-02-08 18:42:28 +00:00
path_result = Do_Pathfind_psp ( self , self . enemy ) ;
2023-02-05 21:03:57 +00:00
# else
2022-02-08 18:42:28 +00:00
path_result = Do_Pathfind ( self , self . enemy ) ;
2023-02-05 21:03:57 +00:00
# endif // FTE
2022-02-08 18:42:28 +00:00
if ( path_result > = 1 ) {
2023-02-05 21:03:57 +00:00
# ifndef FTE
2022-02-08 18:42:28 +00:00
self . goaldummy . origin = Get_First_Waypoint ( self , self . origin , VEC_HULL_MIN , VEC_HULL_MAX ) ;
setorigin ( self . goaldummy , self . goaldummy . origin ) ;
path_failure = path_result ;
2023-02-05 21:03:57 +00:00
# else
2022-02-08 18:42:28 +00:00
self . goalway = path_result ;
setorigin ( self . goaldummy , waypoints [ self . goalway ] . org ) ;
path_failure = self . goalway ;
2023-02-05 21:03:57 +00:00
# endif // FTE
2022-02-08 18:42:28 +00:00
self . goalentity = self . goaldummy ;
self . chase_time = time + 7 ;
} else if ( path_failure = = - 1 ) {
self . goalentity = self . enemy ;
self . chase_time = time + 6 ;
} else {
2023-02-05 21:03:57 +00:00
# ifdef FTE
2022-02-08 18:42:28 +00:00
bprint ( PRINT_HIGH , " FirstPathfind Failure \n " ) ;
2023-02-05 21:03:57 +00:00
# endif // FTE
2022-02-08 18:42:28 +00:00
}
}
void ( ) NextPathfindToEnemy {
// same as PathfindToEnemy on non-FTE platforms
2023-02-05 21:03:57 +00:00
# ifndef FTE
2022-02-08 18:42:28 +00:00
float path_success ;
path_success = Do_Pathfind_psp ( self , self . enemy ) ;
if ( path_success = = 1 ) {
self . goaldummy . origin = Get_Next_Waypoint ( self , self . origin , VEC_HULL_MIN , VEC_HULL_MAX ) ;
setorigin ( self . goaldummy , self . goaldummy . origin ) ;
self . goalentity = self . goaldummy ;
self . chase_time = time + 7 ;
} else if ( path_success = = - 1 ) {
self . goalentity = self . enemy ;
self . chase_time = time + 6 ;
} else {
bprint ( PRINT_HIGH , " NextPathfind Failure \n " ) ; // this lags like hell
}
2023-02-05 21:03:57 +00:00
# else
2022-02-08 18:42:28 +00:00
self . way_cur + + ;
if ( self . way_cur < 40 & & self . way_path [ self . way_cur ] ! = - 1 ) {
self . goalway = self . way_path [ self . way_cur ] ;
setorigin ( self . goaldummy , waypoints [ self . goalway - 1 ] . org ) ;
} else {
self . way_cur = 0 ;
}
2023-02-05 21:03:57 +00:00
# endif // FTE
2022-02-08 18:42:28 +00:00
}
2023-02-05 21:03:57 +00:00
# ifdef FTE
2023-02-09 03:15:14 +00:00
inline float ( vector start , vector min , vector max , vector end , float nomonsters , entity forent ) tracemove
2022-02-08 18:42:28 +00:00
{
//was tracebox
traceline ( start , end , nomonsters , forent ) ;
if ( trace_ent = = forent | | trace_endpos = = end ) {
return 1 ;
} else {
return 0 ;
}
}
2023-02-05 21:03:57 +00:00
# endif // FTE
2022-02-08 18:42:28 +00:00
void ( float dist ) Inside_Walk = {
2022-12-22 16:44:13 +00:00
if ( self . enemy_timeout < time | | self . enemy = = world | | self . enemy . downed = = true ) {
2022-02-08 18:42:28 +00:00
self . enemy_timeout = time + 5 ;
local entity oldEnemy ;
oldEnemy = self . enemy ;
self . enemy = find_new_enemy ( self ) ;
}
//================Check for proximity to player ===========
if ( vlen ( self . enemy . origin - self . origin ) < 60 ) {
2023-01-13 17:29:08 +00:00
if ( self . enemy . classname = = " monkey_bomb " & & self . classname ! = " ai_dog " ) {
2022-02-08 18:42:28 +00:00
self . th_idle ( ) ;
}
if ( self . attack_delay < time ) {
self . attack_delay = time + 1 + ( 1 * random ( ) ) ;
self . th_melee ( ) ;
2022-12-22 16:44:13 +00:00
if ( self . enemy . downed = = true )
self . enemy = find_new_enemy ( self ) ;
2022-02-08 18:42:28 +00:00
self . goalentity = self . enemy ;
self . chase_time = time + 5 ;
}
return ;
}
if ( vlen ( self . enemy . origin - self . origin ) < 600 ) { //50 feet
if ( self . goalentity = = self . enemy & & self . chase_enemy_time > time ) {
return ;
}
if ( TryWalkToEnemy ( ) )
{
self . chase_enemy_time = time + 0.5 ;
return ;
}
}
if ( self . goalentity = = self . enemy ) {
self . goalentity = self . goaldummy ;
self . chase_time = 0 ;
}
//============= No Target ====================
2022-12-27 18:06:37 +00:00
if ( self . goalentity = = world ) { //not sure when this would ever occur... but whatever.
self . goalentity = self . goaldummy ;
}
2022-02-08 18:42:28 +00:00
//============ GoalDummy is Target ============
if ( self . goalentity = = self . goaldummy ) {
if ( nearby ( self . goaldummy . origin ) ) {
2023-02-05 21:03:57 +00:00
# ifndef FTE
2022-02-08 18:42:28 +00:00
NextPathfindToEnemy ( ) ;
2023-02-05 21:03:57 +00:00
# else
2022-02-08 18:42:28 +00:00
PathfindToEnemy ( ) ;
2023-02-05 21:03:57 +00:00
# endif // FTE
2022-02-08 18:42:28 +00:00
}
if ( self . chase_time < time ) {
if ( self . goaldummy . origin ! = world . origin & & tracemove ( self . origin , VEC_HULL_MIN , VEC_HULL_MAX , self . goalentity . origin , TRUE , self ) = = 1 ) {
self . chase_time = time + 7 ;
} else {
PathfindToEnemy ( ) ;
}
}
}
}
. float droptime ;
void ( float dist ) Zombie_Walk = {
//Resetting velocity from last frame (except for vertical)
self . velocity_x = 0 ;
self . velocity_y = 0 ;
//self.flags = self.flags | FL_PARTIALGROUND;
//check_onfire();
if ( ! ( self . flags & FL_ONGROUND ) ) {
if ( ! self . droptime ) {
self . droptime = time + 1 ;
} else if ( self . droptime < time ) {
self . droptime = 0 ;
//bprint(PRINT_HIGH, "not on ground\n");
2023-01-13 17:29:08 +00:00
if ( self . classname ! = " ai_dog " )
self . th_fall ( ) ;
2022-02-08 18:42:28 +00:00
return ;
}
}
if ( self . outside = = TRUE ) {
//handle special walk case for walking to org
Window_Walk ( dist ) ;
return ;
}
if ( self . outside = = 2 ) {
//play_sound_z(2);
Window_Hop ( dist ) ;
//handle special walk case for walking to org
return ;
}
if ( self . outside = = FALSE ) {
if ( self . goalentity = = self . enemy ) {
if ( vlen ( self . origin - self . enemy . origin ) < 60 ) {
return ;
}
}
}
do_walk ( dist ) ;
}
void ( ) Zombie_AI = {
//dist = 0;
float dist = 0 ;
self . flags = self . flags | FL_PARTIALGROUND ;
//check_onfire();
if ( self . outside = = TRUE ) {
play_sound_z ( 2 ) ;
//self.calc_time = time + (0.3 * random());
//Window_Walk(dist);
return ;
} else if ( self . outside = = 2 ) {
play_sound_z ( 2 ) ;
//Window_Hop(0);
return ;
} else if ( self . outside = = FALSE ) {
play_sound_z ( 2 ) ;
//self.calc_time = time + (0.25 + (0.15 * random()));
Inside_Walk ( dist ) ;
}
}
2023-01-13 17:29:08 +00:00
void ( ) Hellhound_AI =
{
Inside_Walk ( 0 ) ;
}
2022-02-08 18:42:28 +00:00
//This function ensures that only one zombie's ai is done at a time, brings down lag significantly
void ( ) Do_Zombie_AI = {
local entity z ;
z = find ( lastzombie , aistatus , " 1 " ) ;
if ( z = = world ) {
z = find ( world , aistatus , " 1 " ) ;
if ( z = = world ) {
return ; //no zombies alive.
}
}
local entity oself ;
oself = self ;
self = z ;
2023-01-13 17:29:08 +00:00
if ( z . classname = = " ai_zombie " )
Zombie_AI ( ) ;
else if ( z . classname = = " ai_dog " )
Hellhound_AI ( ) ;
2022-02-08 18:42:28 +00:00
self = oself ;
lastzombie = z ;
2022-10-25 16:49:02 +00:00
}