/* server/ai/ai_core.qc ai stuff Copyright (C) 2021-2024 NZ:P Team 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; void() LinkZombiesHitbox; entity() Dog_FindEnemy; 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() Respawn = { // cypress (20 dec 2023) -- whoever wrote this originally wasn't considering // many factors.. if a zombie respawns there's a chance it'll be in a // completely different playspace, where the initial window isn't visible // at all, and removeZombie() does not clear that field (rightfully so). // so i've replaced this with the same function i use to respawn zombies // inside of the playable area, which calls self.th_die() and fibs to the // zombie counter a little better, fixes that issue. // Kill it. self.th_die(); // This is a pseudo-hack that just tells the rounds core that we should // spawn another. Current_Zombies--; Remaining_Zombies++; }; entity(entity blarg) find_new_enemy = { 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; } // Now, try and find a viable player targets = find(world, classname, "player"); while(targets != world) { // Don't target downed players. if (targets.downed == true || targets.isspec == true) { targets = find(targets, classname, "player"); continue; } // 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, "player"); } // 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"); } // Last chance -- try a player spawn point. targets = find(world, classname, "info_player_1_spawn"); 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, "info_player_1_spawn"); } // Return a horde point if we found one. if (best_target != world) return best_target; } // We didn't have much luck, just return the world. return best_target; }; 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.aistatus == "1" && 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) { // this is a performance net negative for like 0 gain in horde space. push_away_zombies(); } 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 = { if(self.classname == "ai_zombie" || self.classname == "ai_dog") { 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") { // cypress (17 dec 2023) -- fixed a nasty race condition here. // essentially, if a new zombie spawns in fast enough after // a zombie is currently/just finished hopping a spot, they will // end up always taking box1owner (the hoppable zombie)'s place, // regardless of whether or not there was someone else waiting. // apparently this is an issue i've created rather recently, // which does not make sense to me considering this is a pretty // glaring oversight that's been here since 2014. not my whoops? if(!self.goalentity.box1owner && !self.goalentity.box2owner && !self.goalentity.box3owner) { //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 == world) { //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 == world) { //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 == world) { //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 { // sometimes, we've assigned a zombie a waiting-position // while another zombie is actively hopping. this can cause // a bit of gameplay slowdown since the zombie has to walk // to that waiting position, then to the hop spot (approx 1sec), // so let's do continuous checks here to see if box1 is free, // and claim it if so, in order to smooth the path and claim // that time. only do this if there isn't anyone else in the queue, // though. if (!self.goalentity.box1owner) { // Claim it as ours mid-walk, if there isn't someone else waiting. if ((self.goalentity.box2owner == self && !self.goalentity.box3owner) || (self.goalentity.box3owner == self && !self.goalentity.box2owner)) { self.goalentity.box1owner = self; self.goalorigin = self.goalentity.box1; // Free up the spot we were walking to before. if (self.goalentity.box2owner == self) self.goalentity.box2owner = world; if (self.goalentity.box3owner == self) self.goalentity.box3owner = world; } } 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.goalentity.owner) { 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; } else if (self.goalentity.owner) { self.chase_time = time + 0.05; return; } } if(self.goalentity.health <= 0 && !self.goalentity.owner) { self.outside = 2; self.chase_time = 0; self.hop_step = 0; } else return; } }; 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) { if (!self.goalentity.owner) 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.goalentity = world; self.enemy = find_new_enemy(self); //self.th_die(); self.th_walk(); //LinkZombiesHitbox(); //bprint (PRINT_HIGH, "Linked hitboxes"); } } // // FTE's custom "tracemove" -- no way in hell I'm reimplementing SV_Move // in QuakeC. So this is just a really bad traceline hack. We can't even // use tracebox since that's limited by BSP hulls, // #ifdef FTE inline float(vector start, vector min, vector max, vector end, float nomonsters, entity forent) tracemove = #else float(vector start, vector min, vector max, vector end, float nomonsters, entity forent) tracemove_fake = #endif // FTE { makevectors(forent.angles); // Top left of the box traceline(start + '0 0 32' + v_right * -18, end, nomonsters, forent); // Results Check if (trace_ent != forent && trace_endpos != end) return 0; // Top right of the box traceline(start + '0 0 32' + v_right * 18, end, nomonsters, forent); // Results Check if (trace_ent != forent && trace_endpos != end) return 0; // Bottom left of the box traceline(start - '0 0 24' + v_right * -18, end, nomonsters, forent); // Results Check if (trace_ent != forent && trace_endpos != end) return 0; // Bottom right of the box traceline(start - '0 0 24' + v_right * 18, end, nomonsters, forent); // Results Check if (trace_ent != forent && trace_endpos != end) return 0; return 1; } float() TryWalkToEnemy = { // Early out hack for FTE -- if there's tons of z-diff, GTFO! float z_axis_diff = fabs(self.origin_z - self.enemy.origin_z); if (z_axis_diff >= 30) return 0; // This has been a headache... // TryWalkToEnemy is a system that attempts to ignore waypoints from a // certain distance to simulate proper player-targeting. It does this // using the custom builtin tracemove, which calls SV_Move to determine // if it's possible for the enemy to walk directly to the target. This // is problematic, however -- in that FTE does not feature this builtin // since it's non-standard and was written by blubs. This means there // needs to be improvisation, and as a result there is disparity here. // See the custom tracemove() function for details on that. // -- cypress (28 Nov 2023) #ifdef FTE float TraceResult = tracemove(self.origin, VEC_HULL_MIN, VEC_HULL_MAX, self.enemy.origin, TRUE, self); #else float TraceResult = tracemove_fake(self.origin, VEC_HULL_MIN, VEC_HULL_MAX, self.enemy.origin, TRUE, self); #endif // FTE 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; #ifndef FTE path_result = Do_Pathfind_psp(self, self.enemy); #else path_result = Do_Pathfind(self, self.enemy); #endif // FTE if (path_result >= 1) { #ifndef FTE 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; #else self.goalway = path_result; setorigin(self.goaldummy,waypoints[self.goalway].org); path_failure = self.goalway; #endif // FTE self.goalentity = self.goaldummy; self.chase_time = time + 7; } else if (path_failure == -1) { self.goalentity = self.enemy; self.chase_time = time + 6; } else { #ifdef FTE //bprint(PRINT_HIGH, "FirstPathfind Failure\n"); #endif // FTE } } void() NextPathfindToEnemy { // same as PathfindToEnemy on non-FTE platforms #ifndef FTE 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 } #else 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; } #endif // FTE } void(float dist) Inside_Walk = { // Hellhounds should only change targets if current one is in Last Stand if (self.classname == "ai_dog" && self.enemy.downed == true) { self.enemy = Dog_FindEnemy(); } // Normal Zombie time-out re-targeting else if (self.classname != "ai_dog") { if(self.enemy_timeout < time || self.enemy == world || self.enemy.downed == true) { 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) { if(self.enemy.classname == "monkey_bomb" && self.classname != "ai_dog") { self.th_idle(); } if(self.attack_delay < time) { self.attack_delay = time + 1 + (1 * random()); if (self.enemy.health) self.th_melee(); if (self.enemy.downed == true) { if (self.classname == "ai_dog") self.enemy = Dog_FindEnemy(); else self.enemy = find_new_enemy(self); } 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 ==================== if(self.goalentity == world) {//not sure when this would ever occur... but whatever. self.goalentity = self.goaldummy; } //============ GoalDummy is Target ============ if(self.goalentity == self.goaldummy) { if(nearby(self.goaldummy.origin)) { #ifndef FTE NextPathfindToEnemy(); #else PathfindToEnemy(); #endif // FTE } 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; if (!(self.flags & FL_ONGROUND) && self.watertype != CONTENT_WATER) { if (!self.droptime) { self.droptime = time + 1; } else if (self.droptime < time) { self.droptime = 0; //bprint(PRINT_HIGH, "not on ground\n"); if (self.classname != "ai_dog") self.th_fall(); 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); } } void() Hellhound_AI = { Inside_Walk(0); } // // Do_Zombie_AI() // Behaves differently based on platform -- in FTE, // all AI can afford to be executed at once. However, // on every other platform there is a timed delay in // place and only one zombie AI can be updated at a time. // void() Do_Zombie_AI = { entity z; entity old_self; #ifndef FTE z = find(lastzombie,aistatus,"1"); if(z == world) { z = find(world,aistatus,"1"); if(z == world) { return;//no zombies alive. } } old_self = self; self = z; if(z.classname == "ai_zombie") Zombie_AI(); else if (z.classname == "ai_dog") Hellhound_AI(); self = old_self; lastzombie = z; #else z = find(world, aistatus, "1"); while(z != world) { old_self = self; self = z; // Execute AI if (z.classname == "ai_zombie") Zombie_AI(); else if (z.classname == "ai_dog") Hellhound_AI(); self = old_self; z = find(z, aistatus, "1"); } #endif // FTE } #ifdef FTE void() AI_SetAllEnemiesBBOX = { entity zombies = find(world, classname, "ai_zombie"); while(zombies != world) { if (zombies.aistatus == "1") { zombies.last_solid = zombies.solid; zombies.solid = SOLID_BBOX; if (zombies.head) { zombies.head.last_solid = zombies.head.solid; zombies.head.solid = SOLID_BBOX; } if (zombies.larm) { zombies.larm.last_solid = zombies.larm.solid; zombies.larm.solid = SOLID_BBOX; } if (zombies.rarm) { zombies.rarm.last_solid = zombies.rarm.solid; zombies.rarm.solid = SOLID_BBOX; } } zombies = find(zombies, classname, "ai_zombie"); } } void() AI_RevertEnemySolidState = { entity zombies = find(world, classname, "ai_zombie"); while(zombies != world) { if (zombies.aistatus == "1") { zombies.solid = zombies.last_solid; if (zombies.head) zombies.head.solid = zombies.head.last_solid; if (zombies.larm) zombies.larm.solid = zombies.larm.last_solid; if (zombies.rarm) zombies.rarm.solid = zombies.rarm.last_solid; } zombies = find(zombies, classname, "ai_zombie"); } } #endif // FTE