// g_ai.c #include "g_local.h" #include "g_func.h" qboolean AI_FindTarget (edict_t *self); // RAFAEL 28-dec-98 qboolean AI_HearPlayer (edict_t *self); extern cvar_t *maxclients; qboolean enemy_vis; qboolean enemy_infront; int enemy_range; float enemy_yaw; void AI_GetAvoidDirection( edict_t *self, edict_t *other ); //============================================================================ //============================================================================ /* ============= ai_onfire_run Called while a character is running around on fire ============== */ void ai_onfire_run( edict_t *self, float dist ) { int side_result; if (self->onfiretime <= 0) { // stopping running around self->cast_info.currentmove = self->cast_info.move_stand; return; } if (!self->groundentity) return; // M_ChangeYaw( self ); self->s.angles[YAW] = self->ideal_yaw; if (M_walkmove (self, self->s.angles[YAW], dist*0.5) && AI_SideTrace( self, 64, 0, 1 )) return; // look for a new direction side_result = AI_SideTrace( self, 128, 30, SIDE_RANDOM ); if (side_result) { self->ideal_yaw = anglemod(self->s.angles[YAW] + (30.0 * side_result)); return; } side_result = AI_SideTrace( self, 32, 45, SIDE_RANDOM ); if (side_result) { self->ideal_yaw = anglemod(self->s.angles[YAW] + (30.0 * side_result)); return; } // try a sharper angle side_result = AI_SideTrace( self, 64, 100, SIDE_RANDOM ); if (side_result) { self->ideal_yaw = anglemod(self->s.angles[YAW] + (100.0 * side_result)); return; } side_result = AI_SideTrace( self, 8, 150, SIDE_RANDOM ); if (side_result) { self->ideal_yaw = anglemod(self->s.angles[YAW] + (150.0 * side_result)); return; } // last ditch, try running to our attacker // ai_run( self, dist ); // self->cast_info.currentmove = self->cast_info.move_stand; }; /* ============= ai_move Move the specified distance at current facing. This replaces the QC functions: ai_forward, ai_back, ai_pain, and ai_painforward ============== */ void ai_move (edict_t *self, float dist) { M_walkmove (self, self->s.angles[YAW], dist); // Ridah, added this so we turn to face our attacker after killed if (self->health <= 0) { M_ChangeYaw( self ); if (self->velocity[2] < -50 && fabs(self->velocity[0]) < 50 && fabs(self->velocity[1]) < 50 && !self->groundentity && self->gravity < 1.0) { self->gravity = 0.1; // sliding down wall? } } } /* ============ AI_CheckEvade ============ */ void AI_CheckEvade( edict_t *self ) { vec3_t vec; cast_memory_t *mem=NULL; edict_t **enemy; if (self->enemy) enemy = &self->enemy; else enemy = &self->cast_info.avoid_ent; if (!(*enemy)) { self->cast_info.currentmove = self->cast_info.move_stand; return; } if (self->combat_goalent) { if ( (*enemy)->client && (!(*enemy)->client->pers.holsteredweapon) && ((*enemy)->client->pers.weapon)) { // get the fuck outta here self->cast_info.currentmove = self->cast_info.move_run; return; } else if ( !(*enemy)->client && ((*enemy)->noise_time > (level.time - 2) && (*enemy)->noise_type == PNOISE_WEAPON)) { // get the fuck outta here self->cast_info.currentmove = self->cast_info.move_run; return; } } else if ((enemy == &self->cast_info.avoid_ent) || self->pain_debounce_time < (level.time - 3)) { if ((*enemy) && (*enemy)->health > 0) { mem = level.global_cast_memory[self->character_index][(*enemy)->character_index]; if (mem && (mem->timestamp > (level.time - 4))) { if ((*enemy)->client) { if ( (!(*enemy)->client->pers.holsteredweapon) && ((*enemy)->client->pers.weapon)) { float dist; VectorSubtract( (*enemy)->s.origin, self->s.origin, vec ); dist = VectorNormalize( vec ); self->ideal_yaw = vectoyaw( vec ); if (dist < AI_NOT_HOLSTERED_RANGE_1) { if (self->moral > MORAL_HAPPY || directly_infront((*enemy), self)) { AI_StartAttack( self, (*enemy) ); if (self->cast_info.sight) self->cast_info.sight ( self, (*enemy) ); } else if (AI_SideTrace( self, -64, 0, 1) && dist < 64) { // go back again self->cast_info.currentmove = self->cast_info.move_evade; } } else if (directly_infront((*enemy), self) && (*enemy)->client && (*enemy)->client->pers.weapon && (*enemy)->client->pers.weapon->ammo) { if (AI_CheckTakeCover( self )) { // get outta here self->cast_info.currentmove = self->cast_info.move_run; } else if (AI_SideTrace( self, -64, 0, 1)) { // go back again self->cast_info.currentmove = self->cast_info.move_evade; } else if (dist < 400) { AI_StartAttack( self, (*enemy) ); } } return; } } else // AI character { // FIXME: do more thorough AI here? if (enemy == &self->enemy) { self->cast_info.currentmove = self->cast_info.move_run; } return; } } if (mem) { // they're not hostile anymore mem->flags &= ~MEMORY_HOSTILE_ENEMY; } } } // noone to evade, resume standing (*enemy) = NULL; self->cast_info.currentmove = self->cast_info.move_stand; } /* ============= AI_ClearSight Checks that we can draw a line to other, without hitting something else in the process ============= */ qboolean AI_ClearSight ( edict_t *self, edict_t *other, qboolean boxtrace ) { static vec3_t mins = { -8, -8, -4 }; static vec3_t maxs = { 8, 8, 4 }; // Ridah, used for Flamethrower AI static vec3_t mins2 = { -12, -12, -12 }; static vec3_t maxs2 = { 12, 12, 12 }; vec3_t start, end, vec; float *tmins, *tmaxs; int mask; trace_t tr; qboolean rval; float dist, progdist, distleft; if (!self || !other) return false; if (boxtrace) { if (self->cast_info.max_attack_distance == 384) { tmins = mins2; tmaxs = maxs2; } else { tmins = mins; tmaxs = maxs; } mask = MASK_PLAYERSOLID; } else { tmins = NULL; tmaxs = NULL; mask = MASK_SOLID; } again: VectorCopy( self->s.origin, start ); start[2] += self->viewheight-8*boxtrace; // adjust for gun position VectorCopy( other->s.origin, end ); end[2] += other->viewheight-8*boxtrace; VectorSubtract( end, start, vec ); dist = VectorNormalize( vec ); progdist = 0; distleft = dist; if (fabs(vec[2]) > 0.8) // too steep return false; tr = gi.trace( start, tmins, tmaxs, end, self, mask ); while ( (tr.fraction < 1) && ( ((tr.ent) && (tr.ent->s.renderfx2 & RF2_SURF_ALPHA)) || (tr.contents & MASK_ALPHA) || (tr.surface->flags & SURF_ALPHA) // Rafael : this will allow immortals to see through non breaking glass || (tr.contents & CONTENTS_TRANSLUCENT && tr.contents & CONTENTS_WINDOW && self->cast_info.aiflags & AI_IMMORTAL) )) { progdist += tr.fraction * dist; distleft -= tr.fraction * dist; if (distleft < 14) { // gone passed the point, return true return true; } VectorMA( tr.endpos, 12, vec, start ); tr = gi.trace( start, tmins, tmaxs, end, self, mask ); } rval = ((tr.ent == other) || (tr.fraction == 1)); if (!rval && self->deadflag && self->viewheight < 32) { self->viewheight = 32; goto again; } return rval; } /* ============= AI_MoveToPlatCenter moves the character towards the center position of the platform ============= */ void AI_MoveToPlatCenter( edict_t *self, edict_t *plat) { vec3_t center, vec; node_t *plat_node; if (plat->deadflag) { plat_node = level.node_data->nodes[plat->nav_data.cache_node]; VectorCopy(plat_node->origin, center); center[2] = self->s.origin[2]; VectorSubtract(center, self->s.origin, vec); VectorNormalize2(vec, vec); self->ideal_yaw = vectoyaw(vec); M_ChangeYaw(self); M_walkmove(self, self->ideal_yaw, 15); } } /* ============= AI_CheckTalk look for a friend in close vicinity, to make a talking jesture at ============= */ qboolean AI_CheckTalk( edict_t *self ) { cast_memory_t *cast=NULL; float best_dist=500, this_dist; edict_t *best_cast=NULL; int count=1, total; int i; if (!self->cast_info.talk) // can't speak return false; if (self->goal_ent || self->enemy) return false; // check our current talk_ent if (self->cast_info.talk_ent) { if (!(cast = level.global_cast_memory[self->character_index][self->cast_info.talk_ent->character_index])) { self->cast_info.talk_ent = NULL; return false; } if ( ((cast->timestamp > (level.time - 1)) && (++count)) && (/*!best_cast ||*/ infront(&g_edicts[cast->cast_ent], self)) // ignore them if they aren't facing us && ((this_dist = VectorDistance(g_edicts[cast->cast_ent].s.origin, self->s.origin)) < best_dist)) { best_cast = &g_edicts[cast->cast_ent]; best_dist = this_dist; } } // check clients for (i=1; i<=maxclients->value; i++) { if (g_edicts[i].client && g_edicts[i].health > 0) { cast = level.global_cast_memory[self->character_index][g_edicts[i].character_index]; if (cast) { if ((cast->timestamp_dist < 300) && (cast->timestamp > (level.time - 3)) && (((&g_edicts[i] == self->leader) && (g_edicts[i].last_talk_time < (level.time - 4))) || (directly_infront(&g_edicts[i], self)))) { // pick the client that spoke most recently if (!best_cast || !best_cast->client || (best_cast->last_talk_time < g_edicts[i].last_talk_time)) { best_cast = &g_edicts[i]; } } } } } if (best_cast) goto done; if (self->cast_info.last_talk_turn > (level.time - 2)) { // ignore other AI characters for this time goto done; } if (!best_cast) // only check for other ents if our current talk_ent isn't valid anymore for (i=0; i<3; i++) { switch (i) { case 0: cast = self->cast_info.friend_memory; break; case 1: cast = self->cast_info.neutral_memory; break; case 2: cast = self->cast_info.enemy_memory; break; } total = (int) (random() * 10); while (cast) { if ( ((cast->timestamp > (level.time - 1)) && (++count)) && (/*!best_cast ||*/ infront(&g_edicts[cast->cast_ent], self)) // ignore them if they aren't facing us && (g_edicts[cast->cast_ent].cast_info.aiflags & AI_TALK) && !(g_edicts[cast->cast_ent].cast_info.aiflags & AI_NO_TALK) && ((this_dist = VectorDistance(g_edicts[cast->cast_ent].s.origin, self->s.origin)) < best_dist)) { best_cast = &g_edicts[cast->cast_ent]; best_dist = this_dist; } // if (count > 6) // only check 5 visible friends at once // break; cast = cast->next; } if (i==0 && best_cast) break; } self->cast_info.last_talk_turn = level.time; done: if (best_cast) { if (directly_infront(self, best_cast) || !self->cast_info.move_avoid_walk) { self->cast_info.talk(self); } // else // turn to face them else { vec3_t vec; if (self->cast_info.aiflags & AI_SIT_TALK) { self->cast_info.aiflags &= ~AI_SIT_TALK; } else { VectorSubtract(best_cast->s.origin, self->s.origin, vec); VectorNormalize(vec); self->ideal_yaw = vectoyaw(vec); self->cast_info.currentmove = self->cast_info.move_avoid_walk; } self->cast_info.aiflags |= AI_TALK; // talk straight away //self->last_talk_time = 0; } self->cast_info.talk_ent = best_cast; return true; } return false; } /* ============= AI_TalkThink Generic talking for all characters (currently only supports males) ============= */ void AI_TalkThink( edict_t *self, qboolean ismale ) { #include "voice_punk.h" #include "voice_bitch.h" edict_t *talk_ent; cast_memory_t *mem; if (level.cut_scene_time) return; if (!(self->cast_info.aiflags & AI_TALK)) return; if (self->enemy) // don't talk normally if we're mad return; if (!(talk_ent = self->cast_info.talk_ent)) return; if (VectorDistance( talk_ent->s.origin, self->s.origin ) > 600 || !directly_infront(talk_ent, self)) { self->cast_info.talk_ent = NULL; return; } if ( (self->cast_info.talk_ent == &g_edicts[1]) && (self->cast_info.talk_ent->last_talk_time < (level.time - TALK_OTHER_DELAY*2)) && (self->last_talk_time > (level.time - TALK_SELF_DELAY))) { return; } // if (last_client_talk && last_client_talk > (level.time - TALK_OTHER_DELAY)) // return; // don't talk too much around the client if (self->leader && VectorDistance( self->s.origin, self->leader->s.origin ) < 384 ) { if ((self->last_talk_time < (level.time - TALK_SELF_DELAY)) && (self->health < self->max_health/2)) // if (self->last_talk_time < level.time - (TALK_SELF_DELAY * 4) && (self->health < 50)) { // we've been hired, only speak when we're hurt if (self->gender == GENDER_MALE) { Voice_Random( self, self->leader, friendlyhurt, NUM_FRIENDLYHURT ); return; } } return; } if ((talk_ent->health <= 0) || !visible(self, talk_ent) || !infront(talk_ent, self)) { self->cast_info.talk_ent = NULL; return; } mem = level.global_cast_memory[self->character_index][talk_ent->character_index]; if (!mem || (mem->flags & MEMORY_NO_TALK)) return; // say something! if ( ( (self->last_talk_time < (level.time - TALK_SELF_DELAY)) // we haven't spoken for a while || ( (talk_ent->client || talk_ent->last_talk_time) // if they haven't spoken yet, don't bother && (talk_ent->last_talk_time > (level.time - TALK_OTHER_DELAY*1.5)) // or they've just said something, and we've allowed some time for them to finish saying it && (talk_ent->cast_info.talk_ent == self) && (self->last_talk_time < talk_ent->last_talk_time) && (self->last_talk_time < (level.time - TALK_OTHER_DELAY)))) && (talk_ent->last_talk_time < (level.time - TALK_OTHER_DELAY))) { if (talk_ent->client) { if (!self->cast_group) { // we're neutral cast_memory_t *mem; mem = level.global_cast_memory[self->character_index][talk_ent->character_index]; if (!mem || !(mem->flags & MEMORY_ASSHOLE)) { if (!EP_EventSpeech( self, talk_ent, say_neutral )) { if (ismale) Voice_Random(self, self->cast_info.talk_ent, neutral_talk, NUM_NEUTRAL_TALK); else Voice_Random(self, self->cast_info.talk_ent, f_neutral_talk, F_NUM_NEUTRAL_TALK); } } else // he's a jerk { if (!EP_EventSpeech( self, talk_ent, say_hostile )) { if (ismale) Voice_Random(self, self->cast_info.talk_ent, neutral_asshole_talk, NUM_NEUTRAL_ASSHOLE_TALK); else Voice_Random(self, self->cast_info.talk_ent, f_profanity_level1, F_NUM_PROFANITY_LEVEL1); } } } else if (self->cast_group != talk_ent->cast_group) { // profanity time if (!EP_EventSpeech( self, talk_ent, say_hostile )) { // JOSEPH 26-MAY-99 // if first sighting, play specific sound if (self->name_index == NAME_NICKIBLANCO) { Voice_Specific(self, self->cast_info.talk_ent, nickiblanco, 10); } else if (self->name_index == NAME_TYRONE) { Voice_Specific(self, self->cast_info.talk_ent, ty_tyrone, 10); } else if (self->name_index == NAME_MOKER) { Voice_Specific(self, self->cast_info.talk_ent, steeltown_moker, 10); } else if (self->name_index == NAME_JESUS) { Voice_Specific(self, self->cast_info.talk_ent, sr_jesus, 10); } else if (self->name_index == NAME_HEILMAN) { Voice_Specific(self, self->cast_info.talk_ent, heilman, 10); } else if (self->name_index == NAME_BLUNT) { Voice_Specific(self, self->cast_info.talk_ent, blunt, 10); } else if (self->name_index == NAME_KINGPIN) { Voice_Specific(self, self->cast_info.talk_ent, kingpin, 10); } else if (ismale && specific[0].last_played < 0.1) { Voice_Specific(self, self->cast_info.talk_ent, specific, 0); // What are you doing on my street } // END JOSEPH else if (!ismale && f_specific[0].last_played < 0.1) { Voice_Random (self, self->cast_info.talk_ent, f_specific, 4); // What are you doing on my street } // check profanity level else if (talk_ent->profanity_level <= 1) { if (ismale) Voice_Random(self, self->cast_info.talk_ent, profanity_level1, NUM_PROFANITY_LEVEL1); else Voice_Random(self, self->cast_info.talk_ent, f_profanity_level1, F_NUM_PROFANITY_LEVEL1); } else if (talk_ent->profanity_level <= 2) { if (ismale) Voice_Random(self, self->cast_info.talk_ent, profanity_level2, NUM_PROFANITY_LEVEL2); else Voice_Random(self, self->cast_info.talk_ent, f_profanity_level2, F_NUM_PROFANITY_LEVEL2); } else if (talk_ent->profanity_level == 3) { // Ridah, 31-may-99, fixed the game crashing everytime you cussed a character 3 times if (ismale) Voice_Random(self, self->cast_info.talk_ent, profanity_level3, NUM_PROFANITY_LEVEL3); // kill that mofo else Voice_Random(self, self->cast_info.talk_ent, f_profanity_level3, F_NUM_PROFANITY_LEVEL3); // kill that mofo // go for them! AI_MakeEnemy(self, self->cast_info.talk_ent, 0); } // allow more of a delay than usual last_client_talk = level.time + 3; } } } else // AI -> AI chat { if (!EP_EventSpeech( self, talk_ent, say_neutral)) { if (ismale) Voice_Random(self, self->cast_info.talk_ent, neutral_converse, NUM_NEUTRAL_CONVERSE); else Voice_Random(self, self->cast_info.talk_ent, f_neutral_converse, F_NUM_NEUTRAL_CONVERSE); } } if (self->last_talk_time == level.time) { if (!directly_infront( self, self->cast_info.talk_ent )) { self->cast_info.avoid( self, self->cast_info.talk_ent, true ); } else if (self->cast_info.currentmove == self->cast_info.move_stand) { // make talking gesture if we're just standing around self->cast_info.talk(self); } } } } #define TAKECOVER_BEHIND 8 #define TAKECOVER_INFRONT 13 #define TAKECOVER_DIRECTLY_INFRONT 18 #define SIGHT_ENEMY_ATTACKTIME 2.0 // attack for at least this duration before fleeing again void CheckStillHiding( edict_t *self ) { if (self->owner->health <= 0) { G_FreeEdict( self ); return; } if (!(self->owner->cast_info.aiflags & AI_TAKE_COVER) || (self->owner->cover_ent && !self->owner->cover_ent->inuse)) { // they're not hiding anymore, stop checking self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; G_FreeEdict( self ); return; } if ((self->owner->cover_ent != self->owner->enemy) && (self->owner->cover_ent->noise_time < (level.time - 5))) { // stop hiding self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; G_FreeEdict( self ); return; } if (self->owner->cast_info.currentmove == self->owner->cast_info.move_evade) { // start running self->owner->cast_info.currentmove = self->owner->cast_info.move_run; self->nextthink = level.time + 1.0; return; } if (self->owner->combat_goalent == self && self->owner->cover_ent && ((VectorDistance(self->owner->s.origin, self->s.origin) > 256) || ValidBoxAtLoc( self->s.origin, self->owner->mins, tv(16, 16, 4), self->owner, MASK_PLAYERSOLID ))) { float dist; vec3_t vec; // turn to face them VectorSubtract( self->owner->cover_ent->s.origin, self->owner->s.origin, vec ); vec[2] = 0; dist = VectorNormalize(vec); // should they keep hiding? if ((self->owner->cast_info.currentmove->frame->aifunc == ai_stand) && (VectorDistance( self->s.origin, self->owner->s.origin ) < 128)) { self->owner->ideal_yaw = vectoyaw(vec); M_ChangeYaw( self->owner ); // make sure we're still aware of them AI_RecordSighting(self->owner, self->owner->cover_ent, dist); if (AI_ClearSight( self->owner->cover_ent, self->owner, false )) { // they can see us, we gotta do something or we're dead meat self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; if (!(self->owner->cast_info.aiflags & AI_MELEE)) self->owner->dont_takecover_time = 99999;//level.time + SIGHT_ENEMY_ATTACKTIME; // allow some time to get some shots off self->owner->cast_info.aiflags &= ~AI_TAKECOVER_IGNOREHEALTH; G_FreeEdict( self ); return; } if (self->owner->cast_info.aiflags & AI_TAKECOVER_IGNOREHEALTH) { // stay here until they see us goto skiptest; } if (self->owner->cast_info.aiflags & AI_MELEE) { route_t route; // Ridah, modified this so hiding Melee guys don't foolishly keep running out into the line of fire for no reason if ( infront(self->owner->cover_ent, self->owner) // we're infront of them && self->owner->cover_ent->client && self->owner->cover_ent->client->pers.weapon && self->owner->cover_ent->client->pers.weapon->ammo) { // they have a weapon, keep hiding goto skiptest; } else if (VectorDistance( self->owner->cover_ent->s.origin, self->owner->s.origin ) > 256) { // if too far away, keep hiding if (NAV_Route_EntityToEntity( self->owner, NULL, self->owner->cover_ent, VIS_PARTIAL, false, &route )) { if (route.dist > 256) goto skiptest; } else // no route, keep hiding { goto skiptest; } } } if (!infront( self->owner->cover_ent, self->owner )) { // they're not looking at us if (self->owner->health > ((MORAL_MAX - self->owner->moral) * TAKECOVER_BEHIND)) { self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; self->owner->dont_takecover_time = 99999;//level.time + SIGHT_ENEMY_ATTACKTIME * (random()+1); G_FreeEdict( self ); return; } } else if (self->owner->cast_group && !directly_infront( self->owner->cover_ent, self->owner )) { // not looking directly at us, but in our general direction if (self->owner->health > ((MORAL_MAX - self->owner->moral) * TAKECOVER_INFRONT)) { self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; self->owner->dont_takecover_time = 99999;//level.time + SIGHT_ENEMY_ATTACKTIME; G_FreeEdict( self ); return; } } else if (self->owner->cast_group) { // looking directly at us if (self->owner->health > ((MORAL_MAX - self->owner->moral) * TAKECOVER_DIRECTLY_INFRONT)) { self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; self->owner->dont_takecover_time = 99999;//level.time + SIGHT_ENEMY_ATTACKTIME; G_FreeEdict( self ); return; } } } skiptest: if ((dist < 2000) && (VectorDistance( self->s.origin, self->owner->s.origin ) < 384) && (self->owner->last_getcombatpos < (level.time - 2.0))) { // look for somewhere else to go to float *pos; if ( (self->owner->cast_info.aiflags & AI_MELEE) || (self->owner->health < ((MORAL_MAX - self->owner->moral) * TAKECOVER_INFRONT))) { // look for somewhere further away if (pos = NAV_GetHidePos( self->owner, self->owner->cover_ent, HIDEPOS_FURTHER )) { edict_t *combatent; if (VectorDistance( pos, self->s.origin ) > 96) { combatent = self; VectorCopy( pos, combatent->s.origin ); self->owner->combat_goalent = combatent; combatent->cast_info.aiflags |= AI_GOAL_RUN; self->owner->cast_info.aiflags |= AI_TAKE_COVER; self->owner->cast_info.aiflags |= AI_RUN_LIKE_HELL; self->owner->cast_info.currentmove = self->owner->cast_info.move_run; self->owner->wait = -1; combatent->owner = self->owner; combatent->think = CheckStillHiding; combatent->nextthink = level.time + 1; // give us some time to get there } return; } } else if ( (self->owner->moral > MORAL_HAPPY) && (self->owner->health > ((MORAL_MAX - self->owner->moral) * TAKECOVER_INFRONT))) { // sneak up on them if (pos = NAV_GetHidePos( self->owner, self->owner->cover_ent, HIDEPOS_CLOSER )) { edict_t *combatent; if (VectorDistance( pos, self->s.origin ) > 96) { combatent = self; VectorCopy( pos, combatent->s.origin ); self->owner->combat_goalent = combatent; combatent->cast_info.aiflags |= AI_GOAL_RUN; self->owner->cast_info.aiflags |= AI_TAKE_COVER; self->owner->cast_info.aiflags |= AI_RUN_LIKE_HELL; self->owner->cast_info.currentmove = self->owner->cast_info.move_run; self->owner->wait = -1; combatent->owner = self->owner; combatent->think = CheckStillHiding; combatent->nextthink = level.time + 1; // give us some time to get there } return; } } } self->nextthink = level.time + 0.2; return; } if (self->owner->combat_goalent == self) { self->owner->combat_goalent = NULL; self->owner->cast_info.aiflags &= ~AI_TAKE_COVER; } // not hiding anymore G_FreeEdict( self ); } /* ============= AI_CheckTakeCover Returns true if we should go hide, and we've found a good hiding position ============= */ void Weapon_Blackjack (edict_t *ent); qboolean AI_CheckTakeCover( edict_t *self ) { float *pos=NULL; qboolean evade=true; // make evade action before fleeing if (self->cast_info.aiflags & AI_NO_TAKE_COVER) return false; if (self->dont_takecover_time > level.time) return false; if (self->leader && !(self->cast_info.aiflags & AI_HOLD_POSITION)) return false; // no hiding when we're following if (self->combat_goalent) // probably heading for a combat pos, let them continue return false; if (self->last_getcombatpos > (level.time - 1)) return false; if (!self->enemy) return false; // Ridah 16-may-99, hired guys don't hide if (self->cast_group == 1 && self->leader) return false; // done. // should we hide? // Ridah, always hide every now and then if we're a firing enemy, since that's what any sane human // being would do if ( !(self->cast_info.aiflags & AI_MELEE) // && ((rand()%MORAL_MAX) < (MORAL_MAX+2 - self->moral)) && !(self->enemy->cast_info.aiflags & AI_MELEE) && (!self->enemy->client || (self->enemy->client->pers.weapon && self->enemy->client->pers.weapon->ammo)) && /*directly_*/infront(self->enemy, self) && AI_ClearSight( self->enemy, self, false )) // && (self->last_gethidepos < (level.time - (1.0 + self->moral/MORAL_MAX)))) { // cast_memory_t *mem; // edict_t *trav; qboolean nohide=false; /* // TEAM AI: if we have a friend around, only take cover if they're happily firing mem = self->cast_info.friend_memory; while (mem) { trav = &g_edicts[mem->cast_ent]; if ( (mem->timestamp > (level.time - 10)) && (trav->health > 0) && (trav->enemy) && (!(trav->cast_info.aiflags & AI_MELEE)) && ( (trav->noise_time < (level.time - 2)) || (trav->cast_info.aiflags & AI_TAKE_COVER))) { nohide = true; break; } mem = mem->next; } */ if (!nohide && (pos = NAV_GetHidePos( self, self->enemy, HIDEPOS_ANY ))) { evade = false; goto done; } } if ( (self->cast_info.aiflags & AI_MELEE) && ( (self->enemy->client) && (self->enemy->client->pers.weapon) && (self->enemy->client->pers.weapon->ammo)) && (random() < 0.3 || VectorDistance( self->enemy->s.origin, self->s.origin ) > 128)) { // don't do any other tests, we REALLY want to hide from this enemy! if (pos = NAV_GetHidePos( self, self->enemy, HIDEPOS_FURTHER )) { goto done; } } else if (infront( self->enemy, self )) { if (directly_infront( self->enemy, self )) { if (self->health > ((MORAL_MAX - self->moral) * TAKECOVER_DIRECTLY_INFRONT)) return false; } else { if (self->health > ((MORAL_MAX - self->moral) * TAKECOVER_INFRONT)) return false; } } else if (self->health > ((MORAL_MAX - self->moral) * TAKECOVER_BEHIND)) return false; // find somewhere to go if (!pos) { if (self->cast_group) { // look for a friend with a gun (assume we know where all our friends are) int i; edict_t *other, *best=NULL; float best_dist=2048, this_dist; route_t route; for (i=0; ihealth <= 0 || !other->inuse) continue; if (self->cast_group != other->cast_group) continue; if (self == other) continue; this_dist = VectorDistance( self->s.origin, other->s.origin ); if ((this_dist < 384) && other->enemy) continue; if (other->client) goto accepted; if (best) { if (best->cast_info.aiflags & AI_MELEE) { if (!(other->cast_info.aiflags & AI_MELEE)) { goto accepted; } } else { if (other->cast_info.aiflags & AI_MELEE) { // no good continue; } } } else if (!(self->cast_info.aiflags & AI_MELEE)) { if (other->cast_info.aiflags & AI_MELEE) { // no good, we have weapon and this friend doesn't continue; } else // is it worth going to get them? { if (!NAV_Route_EntityToEntity( self, NULL, other, VIS_PARTIAL, false, &route )) continue; if (route.dist > 1000) // too far away continue; } } if (this_dist > best_dist) continue; accepted: best = other; best_dist = this_dist; if (other->client) break; } if (best) { //gi.dprintf( "%s fleeing to %s\n", self->name, best->name ); pos = best->s.origin; self->cover_ent = self->enemy; self->last_getcombatpos = level.time; self->last_gethidepos = level.time; goto done; } } pos = NAV_GetHidePos( self, self->enemy, HIDEPOS_FURTHER ); } done: if (pos) { edict_t *combatent; combatent = G_Spawn(); VectorCopy( pos, combatent->s.origin ); self->combat_goalent = combatent; combatent->cast_info.aiflags |= AI_GOAL_RUN; self->cast_info.aiflags |= AI_TAKE_COVER; self->cast_info.aiflags |= AI_RUN_LIKE_HELL; self->wait = -1; combatent->owner = self; combatent->think = CheckStillHiding; combatent->nextthink = level.time + 1; // give us some time to get there /* // make sure we run AWAY from our enemy { node_t *node; if (node = NAV_GetClosestNode( self, VIS_PARTIAL, true, true )) self->nav_data.goal_index = node->index + 1; } */ } if ( evade && (self->last_stand_evade < (level.time - 4)) && (pos || ((rand()%10) < 3)) && (self->moral < 3) && self->cast_info.move_evade && self->maxs[2] == self->cast_info.standing_max_z && directly_infront( self->enemy, self ) && AI_ClearSight(self, self->enemy, false) && self->health > self->max_health/2) { self->last_stand_evade = level.time; self->cast_info.currentmove = self->cast_info.move_evade; if (self->cast_info.backoff) self->cast_info.backoff( self, self->enemy ); } return (pos != NULL); } /* ============= AI_ForceTakeCover ============= */ qboolean AI_ForceTakeCover( edict_t *self, edict_t *enemy, qboolean ignorehealth ) { float *pos; int hidepos_type; if (self->cast_info.aiflags & AI_NO_TAKE_COVER) return false; // Note to Ryan: take a look at this. nasty hack because of crash bug if (!(enemy)) return false; if (enemy->svflags & SVF_MONSTER) hidepos_type = HIDEPOS_FURTHER; else hidepos_type = HIDEPOS_ANY; if (pos = NAV_GetHidePos( self, enemy, hidepos_type )) { edict_t *combatent; combatent = G_Spawn(); VectorCopy( pos, combatent->s.origin ); self->combat_goalent = combatent; combatent->cast_info.aiflags |= AI_GOAL_RUN; combatent->cast_info.aiflags |= AI_RUN_LIKE_HELL; self->cast_info.aiflags |= AI_TAKE_COVER; self->cast_info.aiflags |= AI_RUN_LIKE_HELL; combatent->owner = self; combatent->think = CheckStillHiding; combatent->nextthink = level.time + 1; // give us some time to get there if (ignorehealth) self->cast_info.aiflags |= AI_TAKECOVER_IGNOREHEALTH; if (self->maxs[2] == self->cast_info.standing_max_z) self->cast_info.currentmove = self->cast_info.move_run; /* // make sure we run AWAY from our enemy { node_t *node; if (node = NAV_GetClosestNode( self, VIS_PARTIAL, true, true )) self->nav_data.goal_index = node->index + 1; } */ return true; } return false; } /* ============= AI_TooClose returns true if "goal" is within the AI_TOO_CLOSE_DIST range from "self" mostly used to make sure followers don't obstruct their leader ============= */ qboolean AI_TooClose(edict_t *self, edict_t *goal) { float scale; if (VectorCompare(goal->velocity, vec3_origin)) return false; // allow to walk real close if they're standing on a plat and not moving if (goal->groundentity && goal->groundentity->use && VectorCompare(goal->groundentity->velocity, vec3_origin)) return false; scale = 1.0; if (VectorLength(goal->velocity)) scale = 1.5; return (VectorDistance(self->s.origin, goal->s.origin) < AI_TOO_CLOSE_DIST*scale); } /* ============= AI_FollowLeader modification of AI_TooClose(), returns true if we need to move away from, or towards the goal ============= */ qboolean AI_FollowLeader(edict_t *self, edict_t *goal) { float dist; float scale; dist = VectorDistance(self->s.origin, goal->s.origin); // allow to walk real close if they're standing on a plat and not moving if ( goal->groundentity && goal->groundentity->use) { if (goal->groundentity != self->groundentity) return true; else if (dist < 64 && !VectorCompare(goal->velocity, vec3_origin)) return true; else return false; } scale = 1.0; if (VectorLength(goal->velocity)) scale = 1.5; return ((dist < AI_TOO_CLOSE_DIST*scale) || (dist > scale*(AI_GUARDING_DIST * (infront(goal, self) ? 1 : 0.5)))); } /* ============== AI_End_CrouchStand_Down Goes to the standing animation according to what we're currently doing ============== */ void AI_End_CrouchStand_Down(edict_t *self) { self->cast_info.currentmove = self->cast_info.move_crwalk; } /* ============== AI_End_CrouchStand_Up Goes to the standing animation according to what we're currently doing ============== */ void AI_End_CrouchStand_Up(edict_t *self) { self->cast_info.currentmove = self->cast_info.move_run; } /* ============== AI_EndAttack resume attacking if they so desire, otherwise "stand" usually called at the end of pain, or attacking animations ============== */ void AI_EndAttack(edict_t *self) { mmove_t *oldmove; // hack to turn off the flamethrow effect if (self->s.renderfx2 & RF2_FLAMETHROWER) self->s.renderfx2 &= ~RF2_FLAMETHROWER; if (self->enemy && AI_CheckTakeCover(self)) { if (self->cast_info.currentmove != self->cast_info.move_evade) self->cast_info.currentmove = self->cast_info.move_run; return; } oldmove = self->cast_info.currentmove; if (!self->enemy || !self->cast_info.checkattack(self)) { self->cast_info.currentmove = self->cast_info.move_stand; self->cast_info.talk(self); if (self->enemy) self->cast_info.currentmove = self->cast_info.move_run; } } /* ============== AI_GetOrientation returns one of ORIENTATION_* depending on which side of "self", "other" is on ============== */ int AI_GetOrientation( edict_t *self, edict_t *other ) { vec3_t right, rorg, lorg, rvec, lvec; float diff; // determine right or left side impact AngleVectors(self->s.angles, NULL, right, NULL); VectorMA(self->s.origin, 16, right, rorg); VectorMA(self->s.origin, -16, right, lorg); VectorSubtract(other->s.origin, rorg, rvec); VectorSubtract(other->s.origin, lorg, lvec); diff = VectorLength(rvec) - VectorLength(lvec); if (diff < -8) // right { return ORIENTATION_RIGHT; } else if (diff > 8) // left { return ORIENTATION_LEFT; } // center return ORIENTATION_CENTER; } /* ============== AI_CheckStillInair called while jumping, so we stay on a single jumping frame while in the air ============== */ void AI_CheckStillInair(edict_t *self) { if (!self->groundentity) self->s.frame--; // stay on the current frame else { float dist; dist = self->fall_height - self->s.origin[2]; if ( dist < 48) { // get out of this anim self->cast_info.currentmove->endfunc(self); } else if ( dist > 256 ) { T_Damage( self, world, world, vec3_origin, self->s.origin, vec3_origin, 2 + (int)((0.01*dist)*(0.01*dist)), 0, 0, MOD_FALLING ); } } } /* ============== AI_EndJump called at the end of a jump, so we return to running, or crouch walking frames ============== */ void AI_EndJump(edict_t *self) { if (!self->cast_info.move_run) return; // clear cached nodes // self->nav_data.cache_node = -1; // self->nav_data.goal_index = 0; // resume walking if (self->maxs[2] < self->cast_info.standing_max_z) self->cast_info.currentmove = self->cast_info.move_crwalk; else self->cast_info.currentmove = self->cast_info.move_run; } // JOSEPH 20-NOV-98 /* ============== AI_AfterLife Generic character elimination routine. ============== */ void AI_AfterLife(edict_t *self) { // Seconds dead self->deadticks++; // Set body on floor angle if (self->deadticks <= 1) { /*vec3_t avec, stop; trace_t trace; VectorCopy(self->s.origin, stop); stop[2] -= 16*2; trace = gi.trace (self->s.origin, self->mins, self->maxs, stop, self, MASK_DEADSOLID); if (trace.fraction < 1) { VectorCopy (trace.plane.normal, avec); avec[0] = acos(trace.plane.normal[2])/M_PI*180; avec[2] = atan2(trace.plane.normal[1], trace.plane.normal[0])/M_PI*180; avec[1] = self->s.angles[1]; if (avec[2] == 180) { avec[0] = -avec[0]; avec[2] = 0; } else if (avec[2] == 90) { avec[2] = avec[0]; avec[0] = 0; } else if (avec[2] == -90) { avec[2] = -avec[0]; avec[0] = 0; } VectorCopy (avec, self->s.angles); // Ridah, the angles seem to be opposite? VectorScale( self->s.angles, -1, self->s.angles ); self->s.angles[YAW] = -self->s.angles[YAW]; // return YAW to normal }*/ /*vec3_t avec1, avec2, avect, angles, start, stop, headorg, legsorg; trace_t trace; // Use object_bounds to get head position { vec3_t org, ang, mins, maxs; vec3_t forward, right, up; vec3_t rmins, rmaxs, pmins, pmaxs; VectorCopy (self->s.origin, org); VectorCopy (self->s.angles, ang); VectorCopy (self->s.model_parts[PART_HEAD].object_bounds[0][self->s.frame].mins, mins); VectorCopy (self->s.model_parts[PART_HEAD].object_bounds[0][self->s.frame].maxs, maxs); AngleVectors (ang, forward, right, up); VectorMA (org, ((mins[0] + maxs[0]) * 0.5), forward, org); VectorMA (org, -((mins[1] + maxs[1]) * 0.5), right, org); VectorMA (org, (mins[2] + maxs[2]) * 0.5, up, org); // find rotated positions of mins/maxs, and then build the new min/max VectorScale ( forward, mins[0], rmins); VectorMA (rmins, -mins[1], right, rmins); VectorMA (rmins, mins[2], up, rmins); VectorScale ( forward, maxs[0], rmaxs); VectorMA (rmaxs, -maxs[1], right, rmaxs); VectorMA (rmaxs, maxs[2], up, rmaxs); pmins[0] = (rmins[0] < rmaxs[0] ? rmins[0] : rmaxs[0]); pmins[1] = (rmins[1] < rmaxs[1] ? rmins[1] : rmaxs[1]); pmins[2] = (rmins[2] < rmaxs[2] ? rmins[2] : rmaxs[2]); pmaxs[0] = (rmins[0] > rmaxs[0] ? rmins[0] : rmaxs[0]); pmaxs[1] = (rmins[1] > rmaxs[1] ? rmins[1] : rmaxs[1]); pmaxs[2] = (rmins[2] > rmaxs[2] ? rmins[2] : rmaxs[2]); // now align the mins/maxs with the origin mins[0] = pmins[0] - (0.5*(pmaxs[0] + pmins[0])); mins[1] = pmins[1] - (0.5*(pmaxs[1] + pmins[1])); mins[2] = pmins[2] - (0.5*(pmaxs[2] + pmins[2])); maxs[0] = pmaxs[0] - (0.5*(pmaxs[0] + pmins[0])); maxs[1] = pmaxs[1] - (0.5*(pmaxs[1] + pmins[1])); maxs[2] = pmaxs[2] - (0.5*(pmaxs[2] + pmins[2])); headorg[0] = org[0] + (mins[0] + maxs[0]) / 2; headorg[1] = org[1] + (mins[1] + maxs[1]) / 2; headorg[2] = org[2] + (mins[2] + maxs[2]) / 2; } // Use object_bounds to get legs position { vec3_t org, ang, mins, maxs; vec3_t forward, right, up; vec3_t rmins, rmaxs, pmins, pmaxs; VectorCopy (self->s.origin, org); VectorCopy (self->s.angles, ang); VectorCopy (self->s.model_parts[PART_LEGS].object_bounds[0][self->s.frame].mins, mins); VectorCopy (self->s.model_parts[PART_LEGS].object_bounds[0][self->s.frame].maxs, maxs); AngleVectors (ang, forward, right, up); VectorMA (org, ((mins[0] + maxs[0]) * 0.5), forward, org); VectorMA (org, -((mins[1] + maxs[1]) * 0.5), right, org); VectorMA (org, (mins[2] + maxs[2]) * 0.5, up, org); // find rotated positions of mins/maxs, and then build the new min/max VectorScale ( forward, mins[0], rmins); VectorMA (rmins, -mins[1], right, rmins); VectorMA (rmins, mins[2], up, rmins); VectorScale ( forward, maxs[0], rmaxs); VectorMA (rmaxs, -maxs[1], right, rmaxs); VectorMA (rmaxs, maxs[2], up, rmaxs); pmins[0] = (rmins[0] < rmaxs[0] ? rmins[0] : rmaxs[0]); pmins[1] = (rmins[1] < rmaxs[1] ? rmins[1] : rmaxs[1]); pmins[2] = (rmins[2] < rmaxs[2] ? rmins[2] : rmaxs[2]); pmaxs[0] = (rmins[0] > rmaxs[0] ? rmins[0] : rmaxs[0]); pmaxs[1] = (rmins[1] > rmaxs[1] ? rmins[1] : rmaxs[1]); pmaxs[2] = (rmins[2] > rmaxs[2] ? rmins[2] : rmaxs[2]); // now align the mins/maxs with the origin mins[0] = pmins[0] - (0.5*(pmaxs[0] + pmins[0])); mins[1] = pmins[1] - (0.5*(pmaxs[1] + pmins[1])); mins[2] = pmins[2] - (0.5*(pmaxs[2] + pmins[2])); maxs[0] = pmaxs[0] - (0.5*(pmaxs[0] + pmins[0])); maxs[1] = pmaxs[1] - (0.5*(pmaxs[1] + pmins[1])); maxs[2] = pmaxs[2] - (0.5*(pmaxs[2] + pmins[2])); legsorg[0] = org[0] + (mins[0] + maxs[0]) / 2; legsorg[1] = org[1] + (mins[1] + maxs[1]) / 2; legsorg[2] = org[2] + (mins[2] + maxs[2]) / 2; } VectorCopy(headorg, start); VectorCopy(headorg, stop); stop[2] = start[2] - 16*4; trace = gi.trace (start, NULL, NULL, stop, self, MASK_DEADSOLID); if (trace.fraction < 1) { VectorCopy(trace.endpos, avec1); } else { VectorCopy(start, avec1); } VectorCopy(legsorg, start); VectorCopy(legsorg, stop); stop[2] = start[2] - 16*4; trace = gi.trace (start, NULL, NULL, stop, self, MASK_DEADSOLID); if (trace.fraction < 1) { VectorCopy(trace.endpos, avec2); } else { VectorCopy(start, avec2); } if ((avec1[2] != avec2[2])) { if ((headorg[0] > self->s.origin[0]) || (headorg[1] > self->s.origin[1])) { VectorSubtract(avec2, avec1, avect); } else { VectorSubtract(avec1, avec2, avect); } VectorNormalize(avect); vectoangles(avect, angles); angles[1] = self->s.angles[1]; VectorCopy (angles, self->s.angles); }*/ } // If the body is not to be moved if (!self->deadticks) { self->think = NULL; self->nextthink = -1; self->solid = 0; return; } if (self->deadticks > (60*1)) { edict_t *other=NULL; trace_t tr; vec3_t end, travelvec; byte i; long len; float dir; int dir2; byte BW = SFX_BLOOD_WIDTH; byte BH = SFX_BLOOD_HEIGHT; long rnd1, rnd2; // Ridah, 11/22/98 added distance check and optimization // If no player can see the body // while (other = G_Find(other, FOFS(classname), "player")) for (i=0; i<(int)maxclients->value; i++) { other = &g_edicts[1 + i]; // clients start at g_edicts[1] if (!other->inuse) continue; if (infront(other, self) || (VectorDistance(other->s.origin, self->s.origin) < 1024)) { self->nextthink = level.time + FRAMETIME * 20; return; } } // Determine drag direction dir = -10; dir2 = 0; VectorCopy (self->s.origin, end); end[dir2] += dir * 20; tr = gi.trace (self->s.origin, NULL, NULL, end, self, MASK_SHOT); VectorSubtract(self->s.origin, tr.endpos, travelvec); len = VectorNormalize(travelvec); if (len > 100) goto dragit; dir = 10; dir2 = 0; VectorCopy (self->s.origin, end); end[dir2] += dir * 20; tr = gi.trace (self->s.origin, NULL, NULL, end, self, MASK_SHOT); VectorSubtract(self->s.origin, tr.endpos, travelvec); len = VectorNormalize(travelvec); if (len > 100) goto dragit; dir = -10; dir2 = 1; VectorCopy (self->s.origin, end); end[dir2] += dir * 20; tr = gi.trace (self->s.origin, NULL, NULL, end, self, MASK_SHOT); VectorSubtract(self->s.origin, tr.endpos, travelvec); len = VectorNormalize(travelvec); if (len > 100) goto dragit; dir = 10; dir2 = 1; VectorCopy (self->s.origin, end); end[dir2] += dir * 20; tr = gi.trace (self->s.origin, NULL, NULL, end, self, MASK_SHOT); VectorSubtract(self->s.origin, tr.endpos, travelvec); len = VectorNormalize(travelvec); if (len > 100) goto dragit; else goto forgetit; dragit: rnd1 = ((rand()&7)-4); // Drag the body for (i=0; i <= 10; i++) { VectorCopy (self->s.origin, end); end[2] -= 32+8; tr = gi.trace (self->s.origin, NULL, NULL, end, self, MASK_SHOT); SurfaceSpriteEffect(SFX_SPRITE_SURF_BLOOD1, BW--, BH--, tr.ent, tr.endpos, tr.plane.normal); self->s.origin[dir2] += dir; rnd2 = ((rand()&3)-2); if (dir == 1) self->s.origin[2] += (rnd1 + rnd2); else self->s.origin[1] += (rnd2 + rnd2); } forgetit: // Unload this cast's memory, since we won't be needing it anymore //AI_UnloadCastMemory( self ); // Remove the body self->think = NULL; self->nextthink = -1; self->solid = 0; self->svflags |= SVF_NOCLIENT; return; } self->nextthink = level.time + FRAMETIME * 10; } // END JOSEPH /* trace_t tr; vec3_t end; VectorCopy (self->s.origin, end); end[2] -= 32+8; tr = gi.trace (self->s.origin, NULL, NULL, end, self, MASK_SHOT); SurfaceSpriteEffect(SFX_SPRITE_SURF_BLOOD1, SFX_BLOOD_WIDTH, SFX_BLOOD_HEIGHT, tr.ent, tr.endpos, tr.plane.normal); */ /* ============== AI_EndDeath Generic end of death routine. Should call this for all characters that die, then do any special stuff for that character. ============== */ void AI_EndDeath(edict_t *self) { // Ridah 16-may-99, stop dead bodies from sinking through floor if (!self->groundentity) { return; } // done. // Ridah, 7-5-99, fixes shooting dead bodies // if (!(self->svflags & SVF_DEADMONSTER)) if (self->mins[0] != -64) { // Ridah, had to expand this even further to accomodate the punk death frames VectorSet (self->mins, -64, -64, -24); VectorSet (self->maxs, 64, 64, -4); self->movetype = MOVETYPE_NONE; self->svflags |= SVF_DEADMONSTER; gi.linkentity (self); /* // if this position isn't valid, use the old, smaller box { trace_t tr; tr = gi.trace( self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_SHOT ); if (tr.startsolid) { VectorSet (self->mins, -16, -16, -24); VectorSet (self->maxs, 16, 16, 0); gi.linkentity (self); } } */ } // Ridah, modified this so we only spawn blood once if (!(self->flags & FL_SPAWNED_BLOODPOOL) && VectorCompare( self->velocity, vec3_origin )) { SpawnBloodPool (self); self->flags |= FL_SPAWNED_BLOODPOOL; } AI_AfterLife(self); // END JOSEPH } /* =============== AI_SideTrace Returns a 1 if a RIGHT rotation will point in a direction that is free of obstacles Returns -1 if LEFT, 0 otherwise =============== */ int AI_SideTrace( edict_t *self, float dist, float inyaw, int side ) { trace_t tr; vec3_t angle, vec, end, end2, mins; float yaw; int count=0; VectorCopy( self->mins, mins ); mins[2] += 16; // pick a random direction if ((side == 1) || (!side && (random() <= 0.5))) yaw = inyaw; else yaw = -inyaw; while (1) { VectorCopy(self->s.angles, angle); angle[YAW] += yaw; AngleVectors( angle, vec, NULL, NULL ); // trace in this direction VectorMA( self->s.origin, dist, vec, end ); tr = gi.trace( self->s.origin, mins, self->maxs, end, self, MASK_PLAYERSOLID ); if (tr.fraction == 1) { // make sure we can see our enemy from here if (self->enemy) { vec3_t oldorg; int result; VectorCopy( self->s.origin, oldorg ); VectorCopy( tr.endpos, self->s.origin ); result = AI_ClearSight(self, self->enemy, true); VectorCopy( oldorg, self->s.origin ); if (!result) { goto blocked; } } // check this is on ground VectorCopy( end, end2 ); end2[2] -= 64; tr = gi.trace( end, mins, self->maxs, end2, self, MASK_PLAYERSOLID ); if (tr.fraction < 1) { // go ahead! if (yaw < 0) return -1; // left side else return 1; // right side } } blocked: if (count++ || !inyaw || side) break; yaw *= -1; } return 0; } void AI_CheckStillClimbingLadder( edict_t *self ) { if (self->groundentity) { // we're on ground, so resume moving self->cast_info.currentmove = self->cast_info.move_stand; } } /* =============== AI_EndRun checks to see if we should walk or run =============== */ void AI_EndRun( edict_t *self ) { if (!self->last_goal) return; if (!self->cast_info.move_run) return; if ( (self->enemy != self->last_goal) && (VectorDistance( self->s.origin, self->last_goal->s.origin ) < AI_GUARDING_DIST * 2 ) && (VectorLength( self->last_goal->velocity) < 200)) { // close enough to walk self->cast_info.currentmove = self->cast_info.move_runwalk; } else if ((self->last_goal != self->goal_ent) && (!self->last_goal || self->last_goal != self->guard_ent)) { self->cast_info.currentmove = self->cast_info.move_run; } } /* =============== AI_YawTrace Returns a 1 if a YAW rotation from self's angles will point in a direction that is free of obstacles Returns 0 otherwise =============== */ int AI_YawTrace( edict_t *self, float dist, float inyaw ) { trace_t tr; vec3_t angle, vec, end, end2, mins; int count=0; VectorCopy( self->mins, mins ); mins[2] += 16; VectorCopy(self->s.angles, angle); angle[YAW] = anglemod(angle[YAW] + inyaw); AngleVectors( angle, vec, NULL, NULL ); // trace in this direction VectorMA( self->s.origin, dist, vec, end ); tr = gi.trace( self->s.origin, mins, self->maxs, end, self, MASK_PLAYERSOLID ); if (tr.fraction == 1) { // check this is on ground VectorCopy( end, end2 ); end2[2] -= 64; tr = gi.trace( end, mins, self->maxs, end2, self, MASK_PLAYERSOLID ); if (tr.fraction < 1) { // go ahead! return true; } } return false; } /* ============= AI_StartRun Start us moving ============= */ void AI_StartRun( edict_t *self ) { // start running to them if ( self->maxs[2] > DUCKING_MAX_Z ) self->cast_info.currentmove = self->cast_info.move_run; else self->cast_info.currentmove = self->cast_info.move_crwalk; } void AI_FreeAndClearGoalEnt( edict_t *self ) { if (self->owner->goal_ent == self) self->owner->goal_ent = NULL; G_FreeEdict( self ); } /* ============= ai_stand Used for standing around and looking for TARGETS Distance is for slight position adjustments needed by the animations ============== */ void ai_stand (edict_t *self, float dist) { if (dist) M_walkmove (self, self->s.angles[YAW], dist); if (self->leader && !self->leader->active_node_data) // we're loading up a new map { return; } if (self->cast_info.aiflags & AI_DUCKATTACK) { // return to standing self->cast_info.aiflags &= ~AI_DUCKATTACK; if (self->maxs[2] < self->cast_info.standing_max_z) { self->maxs[2] = self->cast_info.standing_max_z; self->cast_info.currentmove = self->cast_info.move_stand_up; self->s.frame = self->cast_info.currentmove->firstframe; return; } } // Ridah, Check DUCKING/STANDING status if (self->maxs[2] < self->cast_info.standing_max_z) { if (self->cast_info.move_crstand && self->cast_info.currentmove != self->cast_info.move_crstand) { // we should be ducking self->cast_info.currentmove = self->cast_info.move_crstand; self->s.frame = self->cast_info.currentmove->firstframe; } else if (self->leader && !(self->cast_info.aiflags & AI_TAKE_COVER) && (self->leader->maxs[2] > DUCKING_MAX_Z)) // can we stand now? { trace_t tr; self->maxs[2] = self->cast_info.standing_max_z; tr = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_PLAYERSOLID); if (tr.startsolid) // can't safely stand { self->maxs[2] = DUCKING_MAX_Z; } else { self->cast_info.currentmove = self->cast_info.move_stand; self->s.frame = self->cast_info.currentmove->firstframe; } } } else // not physically ducking, so make sure our animation frame reflects this { if ( (self->cast_info.currentmove == self->cast_info.move_crstand) && (self->cast_info.move_crstand != self->cast_info.move_stand)) { // we should be standing self->cast_info.currentmove = self->cast_info.move_stand; self->s.frame = self->cast_info.currentmove->firstframe; } } // Ridah, done. // Ridah, Do pausetime stuff, making sure we stay paused if we have to, or resume moving otherwise if (!self->groundentity) { if (self->cast_info.move_jump && self->cast_info.currentmove != self->cast_info.move_jump) self->cast_info.currentmove = self->cast_info.move_jump; return; } else if (self->groundentity->use && !VectorCompare(self->groundentity->velocity, vec3_origin)) { self->cast_info.pausetime = level.time + 1; } // make sure we don't abort our goal waypoint next time we try to move self->moveinfo.wait = level.time + 3; self->wait = level.time + 3; if (self->cast_info.pausetime > level.time) { // see if we should wait for a lift or door if (self->target_ent) { if (VectorCompare(self->target_ent->velocity, vec3_origin) && VectorCompare(self->target_ent->avelocity, vec3_origin)) { // it's not moving, are we at our destination? if (self->target_ent->use != Use_Plat) { if (self->groundentity != self->target_ent) { // stopped, and we're not standing on it, so proceed self->target_ent = NULL; self->cast_info.pausetime = 0; return; } else if (self->target_ent->nextthink > (level.time + 0.1)) { if (!self->nav_data.goal_index || (VectorDistance(level.node_data->nodes[self->nav_data.goal_index-1]->origin, self->s.origin) < 128)) { self->target_ent = NULL; self->cast_info.pausetime = 0; return; } } else // it's stopped, and not about to go anywhere { self->target_ent = NULL; self->cast_info.pausetime = 0; return; } } else if (self->groundentity == self->target_ent) { if (self->target_ent->moveinfo.state == STATE_TOP) { self->target_ent = NULL; self->cast_info.pausetime = 0; return; } } else if (self->target_ent->moveinfo.state == STATE_BOTTOM) { self->target_ent = NULL; self->cast_info.pausetime = 0; return; } } // keep waiting self->cast_info.pausetime = level.time + 0.2; } if (self->groundentity && self->groundentity->use) { // make sure all corners are on the platform trace_t tr; vec3_t org, orgdest; float x, y; for (x=-16; x<24; x+=32) { for (y=-16; y<24; y+=32) { VectorSet(org, self->s.origin[0]+x, self->s.origin[1]+y, self->s.origin[2]-23); VectorSet(orgdest, self->s.origin[0]+x, self->s.origin[1]+y, self->s.origin[2]-30); tr = gi.trace(org, NULL, NULL, orgdest, self, MASK_SOLID); if ((tr.fraction == 1) || (tr.ent != self->groundentity)) { // set a running frame if (self->cast_info.move_run) self->s.frame = self->cast_info.move_run->firstframe + ( ((int)(level.time * 10)) % (self->cast_info.move_run->lastframe - self->cast_info.move_run->firstframe)); AI_MoveToPlatCenter(self, self->groundentity); return; } } } } // should we attack someonw while waiting? else if (self->enemy || AI_FindTarget(self)) { if (AI_BeginAttack(self)) { self->cast_info.attack( self ); } } // should we get out of the way? else if (self->target_ent && self->target_ent->velocity[2]) { // are we within the XY axis bounds? if ( ( (self->s.origin[0] > (self->target_ent->absmin[0] - 20)) && (self->s.origin[0] < (self->target_ent->absmax[0] + 20))) && ( (self->s.origin[1] > (self->target_ent->absmin[1] - 20)) && (self->s.origin[1] < (self->target_ent->absmax[1] + 20)))) { float *pos; vec3_t mins, maxs; // move out of it's way // set a running frame if (self->cast_info.move_run) self->s.frame = self->cast_info.move_run->firstframe + ( ((int)(level.time * 10)) % (self->cast_info.move_run->lastframe - self->cast_info.move_run->firstframe)); VectorSet( mins, self->target_ent->absmin[0] - 20, self->target_ent->absmin[1] - 20, -9999 ); VectorSet( maxs, self->target_ent->absmax[0] + 20, self->target_ent->absmax[1] + 20, 9999 ); // look for a safe position if (pos = NAV_GetReachableNodeOutsideBounds ( self, mins, maxs )) { vec3_t vec; // face this direction VectorSubtract( pos, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw( vec ); M_ChangeYaw( self ); // move towards it M_walkmove( self, self->ideal_yaw, 10 ); } } } return; } // Ridah, done. // Ridah, check for a reason to do something (if we're too close to our leader, or there is an enemy we should seek) if ((!(self->cast_info.aiflags & AI_HOLD_POSITION) || self->enemy) && self->leader && !(self->cast_info.aiflags & AI_TAKE_COVER)) { if ( !(self->leader->waterlevel) // don't follow a swimming leader && AI_FollowLeader(self, self->leader)) { self->last_goal = self->leader; AI_EndRun(self); // does distance checking, will start us moving if (self->cast_info.currentmove->frame->aifunc != ai_stand && VectorDistance(self->s.origin, self->leader->s.origin) <= AI_TOO_CLOSE_DIST) { // look for a node we can reach AI_GetAvoidDirection( self, self->leader ); self->cast_info.currentmove = self->cast_info.move_avoid_walk; } } } if (self->cover_ent && (self->cast_info.aiflags & AI_TAKE_COVER)) { float goaldist; if ( self->combat_goalent && ( ((goaldist = VectorDistance( self->s.origin, self->combat_goalent->s.origin )) > 64) || ( !ValidBoxAtLoc( self->combat_goalent->s.origin, self->mins, self->maxs, self, MASK_PLAYERSOLID ) && goaldist < 128 ))) { // we should be running to it self->cast_info.currentmove = self->cast_info.move_run; return; } // pretend we're ducking for vis test self->viewheight = DUCKING_MAX_Z - 4; // if they can see us, and we're standing around, abort taking cover if (AI_ClearSight( self->cover_ent, self, false)) { self->cast_info.aiflags &= ~AI_TAKE_COVER; self->combat_goalent = NULL; self->cast_info.currentmove = self->cast_info.move_run; return; } // Ridah, 5-7-99, had to modify this to prevent Betty crouching bug self->viewheight = self->maxs[2] - 4; if (self->maxs[2] > DUCKING_MAX_Z && AI_ClearSight( self->cover_ent, self, false)) { self->maxs[2] = DUCKING_MAX_Z; if (self->cast_info.move_crouch_down) self->cast_info.currentmove = self->cast_info.move_crouch_down; } return; } /* if ( self->goal_ent && !self->leader && !(self->cast_info.aiflags & AI_TAKE_COVER) && ( !self->enemy || (self->cast_info.aiflags & AI_GOAL_IGNOREENEMY))) */ if (self->goal_ent && !self->leader && !(self->cast_info.aiflags & AI_TAKE_COVER)) { float goaldist; if ( (self->cast_info.move_run) && ( (self->cast_info.goal_ent_pausetime < level.time))) // || (self->goal_ent->solid == SOLID_TRIGGER))) { goaldist = VectorDistance(self->s.origin, self->goal_ent->s.origin); if ( (self->goal_ent->touch == path_corner_cast_touch) || (goaldist > 256 )) { // Ridah, fixes jerky movement in SR1 intro if (level.cut_scene_time) { if ((self->goal_ent->touch == path_corner_cast_touch) && (self->name_index == NAME_INTROGUY1) && (goaldist < 32)) { // simulate touching it self->goal_ent->touch( self->goal_ent, self, NULL, NULL ); return; } } self->nav_data.cache_node = -1; self->nav_data.goal_index = 0; self->cast_info.currentmove = self->cast_info.move_runwalk; return; } if (self->goal_ent->target) { // face the target (eg. the Radio in Skidrow points into the room) edict_t *targ; if (targ = G_Find( NULL, FOFS( targetname ), self->goal_ent->target )) { if (!directly_infront( self, targ )) // straighten up { if (self->cast_info.avoid) { self->cast_info.avoid( self, targ, true ); return; } } } else { // gi.dprintf( "ai_stand: Unable to locate 'target' to face\n" ); } } else if (!directly_infront( self, self->goal_ent )) // straighten up { if (self->cast_info.avoid) { self->cast_info.avoid( self, self->goal_ent, true ); return; } } if (!(self->goal_ent->cast_info.aiflags & AI_GOALENT_MANUAL_CLEAR)) { self->goal_ent = NULL; // we made it there } } } // If guarding something, and we've moved out of range, abort the pursuit if (self->guard_ent && !(self->cast_info.aiflags & AI_TAKE_COVER)) { if ( (VectorDistance( self->s.origin, self->guard_ent->s.origin ) > self->guard_ent->guard_radius) || (!AI_ClearSight(self, self->guard_ent, false))) { AI_StartRun ( self ); return; } } // RAFAEL 28-dec-98 if (AI_HearPlayer (self)) { return; } // look for things to be hostile at if (self->enemy) { AI_StartAttack( self, self->enemy ); return; } else if (AI_FindTarget (self)) { return; } if (!(self->cast_info.aiflags & AI_TAKE_COVER) && !(self->spawnflags & 1) && (self->cast_info.idle) && (level.time > self->cast_info.idle_time)) { if (self->cast_info.idle_time) { self->cast_info.idle (self); self->cast_info.idle_time = level.time + 15 + random() * 15; } else { self->cast_info.idle_time = level.time + random() * 15; } } if ( !(self->cast_info.aiflags & AI_TAKE_COVER) && !(self->goal_ent) && (self->target)) // Ridah, removed this optimization, since we are always using trigger spawned characters anyway // && ( (self->spawnflags & SPAWNFLAG_IMMEDIATE_FOLLOW_PATH) // || ( (VectorDistance(self->s.origin, g_edicts[1].s.origin) < 800) // && (gi.inPVS(g_edicts[1].s.origin, self->s.origin))))) // Optimization, so we only walk if the player is close { // resume following our path_corner self->goal_ent = G_PickTarget(self->target); if (!self->goal_ent) { gi.dprintf ("%s can't find target %s at %s\n", self->classname, self->target, vtos(self->s.origin)); self->target = NULL; } } // if we're crouched, and we can stand, then go for it if (self->maxs[2] < self->cast_info.standing_max_z) { if (self->stand_if_idle_time < level.time) { if (self->stand_if_idle_time > (level.time - 0.5)) { self->maxs[2] = self->cast_info.standing_max_z; if (!ValidBoxAtLoc( self->s.origin, self->mins, self->maxs, self, MASK_PLAYERSOLID )) self->maxs[2] = DUCKING_MAX_Z; else if (self->cast_info.move_stand_up) self->cast_info.currentmove = self->cast_info.move_stand_up; } else { self->stand_if_idle_time = level.time + 3; } } } if ((self->cast_info.aiflags & AI_HOLD_POSITION) && !self->enemy) { // Ridah, 6-jun-99, Friendly's should avoid grenades when staying put, then come back if (!(self->cast_info.aiflags & AI_TAKE_COVER) || !(self->cover_ent) || (self->cover_ent->svflags & SVF_MONSTER)) { // we aren't avoiding anything if (VectorDistance( self->s.origin, self->holdpos_ent->s.origin ) >= AI_GUARDING_DIST) { self->cast_info.currentmove = self->cast_info.move_run; } } } } /* ============= ai_walk The monster is walking it's beat ============= */ void ai_walk (edict_t *self, float dist) { M_MoveToGoal (self, dist); // check for noticing an enemy if (AI_FindTarget (self)) return; if ((self->cast_info.search) && (level.time > self->cast_info.idle_time)) { if (self->cast_info.idle_time) { self->cast_info.search (self); self->cast_info.idle_time = level.time + 15 + random() * 15; } else { self->cast_info.idle_time = level.time + random() * 15; } } } /* ============= ai_charge Turns towards target and advances Use this call with a distnace of 0 to replace ai_face ============== */ void ai_charge (edict_t *self, float dist) { vec3_t v; if (self->enemy) { VectorSubtract (self->enemy->s.origin, self->s.origin, v); self->ideal_yaw = vectoyaw(v); M_ChangeYaw (self); } if (dist) if (!M_walkmove (self, self->s.angles[YAW], dist )) M_walkmove (self, self->s.angles[YAW] + 60*(rand()%3 - 1), 0.5 * dist); } qboolean ai_checksafeground( edict_t *self, vec3_t oldpos ) { vec3_t end, start; trace_t tr; float x, y; for (x=-14; x<=14; x+=28) { for (y=-14; y<=14; y+=28) { VectorCopy( self->s.origin, start ); start[0] += x; start[1] += y; VectorCopy( start, end ); end[2] += self->mins[2] - 16; tr = gi.trace( start, vec3_origin, vec3_origin, end, self, MASK_PLAYERSOLID ); if (tr.fraction == 1) { VectorCopy( oldpos, self->s.origin ); gi.linkentity( self ); return false; } } } return true; } /* ============= ai_turn don't move, but turn towards ideal_yaw Distance is for slight position adjustments needed by the animations ============= */ void ai_turn (edict_t *self, float dist) { vec3_t oldpos; // qboolean wassafeground; // wassafeground = ai_checksafeground( self, oldpos ); VectorCopy( self->s.origin, oldpos ); self->cast_info.aiflags &= ~AI_TURN_BLOCKED; if (dist > 32) dist = 32; if (self->groundentity && dist && (!(self->cast_info.aiflags & AI_NOWALK_FACE))) // Ridah 5-8-99, fixes "aiflags 1" not working { if (!M_walkmove (self, self->s.angles[YAW], dist) && !self->leader) { self->cast_info.aiflags |= AI_TURN_BLOCKED; if (self->cast_info.currentmove->endfunc == AI_EndAttack) { // call it now, since we can't move AI_EndAttack( self ); } } } if (self->cast_info.avoid_ent) { // if not above safe ground, go back // if (wassafeground && dist && !ai_checksafeground( self, oldpos )) if (dist && !ai_checksafeground( self, oldpos )) { mmove_t *oldmove; if (self->cast_info.currentmove->endfunc == AI_EndAttack) { // call it now, since we can't move oldmove = self->cast_info.currentmove; AI_EndAttack( self ); if (self->cast_info.currentmove == oldmove) { self->cast_info.currentmove = self->cast_info.move_stand; } goto done; } } if (self->cast_info.last_avoid > (level.time - 1.5)) { vec3_t vec; VectorSubtract( self->cast_info.avoid_ent->s.origin, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw( vec ); if ( (self->cast_info.currentmove->endfunc == AI_EndAttack) && (directly_infront( self, self->cast_info.avoid_ent ))) { // call it now, since we're facing them AI_EndAttack( self ); } } else if (dist) // been too long { self->cast_info.avoid_ent = NULL; } else // just straighten up { vec3_t vec; VectorSubtract( self->cast_info.avoid_ent->s.origin, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw(vec); } } else if (self->enemy) { vec3_t vec; VectorSubtract( self->enemy->s.origin, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw(vec); } done: M_ChangeYaw (self); } void ai_turn2 (edict_t *self, float dist) { /* Ridah, ai_turn does this now // quick fix to face the player if (self->enemy) { vec3_t vec; VectorSubtract( self->enemy->s.origin, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw( vec ); M_ChangeYaw( self ); } */ self->cast_info.avoid_ent = self->enemy; ai_turn (self, dist); } /* ============== ai_sidestep move "dist" to the right ============== */ void ai_sidestep( edict_t *self, float dist) { vec3_t oldpos; VectorCopy( self->s.origin, oldpos ); self->cast_info.aiflags &= ~AI_TURN_BLOCKED; if (dist > 32) dist = 32; if (dist) { if (!M_walkmove (self, self->s.angles[YAW] + 90, dist)) { self->cast_info.aiflags |= AI_TURN_BLOCKED; if (self->cast_info.currentmove->endfunc == AI_EndAttack) { // call it now, since we can't move mmove_t *om; om = self->cast_info.currentmove; AI_EndAttack( self ); if (self->cast_info.currentmove == om) { self->cast_info.currentmove = self->cast_info.move_run; } } } } if (self->cast_info.avoid_ent) { // if not above safe ground, go back if (dist && !ai_checksafeground( self, oldpos )) { if (self->cast_info.currentmove->endfunc == AI_EndAttack) { // call it now, since we can't move AI_EndAttack( self ); } } if (self->cast_info.last_avoid > (level.time - 1.5)) { vec3_t vec; VectorSubtract( self->cast_info.avoid_ent->s.origin, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw( vec ); } else // been too long { self->cast_info.avoid_ent = NULL; } } else if (self->enemy) { vec3_t vec; VectorSubtract( self->enemy->s.origin, self->s.origin, vec ); VectorNormalize( vec ); self->ideal_yaw = vectoyaw(vec); } M_ChangeYaw (self); } /* ============= 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 ============= */ int range (edict_t *self, edict_t *other) { vec3_t v; float len; VectorSubtract (self->s.origin, other->s.origin, v); len = VectorLength (v); if (len < MELEE_DISTANCE) return RANGE_MELEE; if (len < 500) return RANGE_NEAR; if (len < 1000) return RANGE_MID; return RANGE_FAR; } /* ============= visible returns 1 if the entity is visible to self, even if not infront () ============= */ qboolean visible (edict_t *self, edict_t *other) { vec3_t spot1; vec3_t spot2; trace_t trace; VectorCopy (self->s.origin, spot1); spot1[2] += self->viewheight; VectorCopy (other->s.origin, spot2); spot2[2] += other->viewheight; trace = gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, MASK_OPAQUE); if (trace.fraction == 1.0) return true; return false; } /* ============= infront returns 1 if other is in front (in sight) of self ============= */ qboolean infront (edict_t *self, edict_t *other) { vec3_t vec; float dot; vec3_t forward; AngleVectors (self->s.angles, forward, NULL, NULL); VectorSubtract (other->s.origin, self->s.origin, vec); VectorNormalize (vec); dot = DotProduct (vec, forward); if (dot > 0.2) return true; return false; } /* ============= directly_infront returns 1 if other is directly in front of self (pointing at) ============= */ qboolean directly_infront (edict_t *self, edict_t *other) { vec3_t vec; float dot; vec3_t forward, ang; float len; VectorCopy( self->s.angles, ang ); ang[PITCH] = 0; AngleVectors (ang, forward, NULL, NULL); VectorSubtract (other->s.origin, self->s.origin, vec); vec[2] = 0; len = VectorNormalize (vec); dot = DotProduct (vec, forward); if (dot > (0.98 - (len < 1024 ? 0.1 * (1.0 - (len/1024.0)) : 0))) return true; return false; } /* ============= directly_infront_angles returns 1 if other is directly in front of self (pointing at) ============= */ qboolean directly_infront_angle (vec3_t ang1, edict_t *self, edict_t *other) { vec3_t vec; float dot; vec3_t forward;//, ang; // VectorCopy( self->s.angles, ang ); // ang[PITCH] = 0; AngleVectors (ang1, forward, NULL, NULL); VectorSubtract (other->s.origin, self->s.origin, vec); // vec[2] = 0; VectorNormalize (vec); dot = DotProduct (vec, forward); if (dot > 0.98) return true; return false; } //============================================================================= /* ============ FacingIdeal ============ */ qboolean FacingIdeal(edict_t *self) { float delta; delta = anglemod(self->s.angles[YAW] - self->ideal_yaw); if (delta > 45 && delta < 315) return false; return true; } //============================================================================= //============================================================================= extern void button_use (edict_t *self, edict_t *other, edict_t *activator); extern void func_explosive_use(edict_t *self, edict_t *other, edict_t *activator); extern void func_explosive_explode (edict_t *self, edict_t *inflictor, edict_t *attacker, int damage, vec3_t point, int mdx_part, int mdx_subobject); // Ridah, new AI system movements functions /* ================ AI_canmove tries a (CPU friendly) move to the destination point, making sure we don't move inside another solid, into water, or fall off an edge ================ */ qboolean AI_canmove( edict_t *self, vec3_t dest ) { // returns true if the dest box is valid trace_t tr; vec3_t dropdest; float lift = 0; vec3_t dir; VectorSubtract( dest, self->s.origin, dir ); VectorNormalize( dir ); tr = gi.trace(dest, self->mins, self->maxs, dest, self, MASK_PLAYERSOLID|CONTENTS_MONSTERCLIP); if (tr.startsolid || tr.allsolid) { // try lifting up slightly dest[2] += (lift = 24); tr = gi.trace(dest, self->mins, self->maxs, dest, self, MASK_PLAYERSOLID | CONTENTS_MONSTERCLIP); if (tr.startsolid || tr.allsolid) dest[2] -= lift; } if (tr.startsolid || tr.allsolid) { edict_t *activator; if (tr.ent == world || !(tr.ent->svflags & SVF_MONSTER)) { // try a line trace to find a blocking character tr = gi.trace(self->s.origin, NULL, NULL, dest, self, MASK_PLAYERSOLID|CONTENTS_MONSTERCLIP); } // get this character to get out of our way if ( (tr.fraction < 1) && (tr.ent != world) /* && [tr.ent is not an enemy (neutral or friend)]*/ && (tr.ent->cast_info.avoid)) { if (tr.ent->cast_info.last_avoid < (level.time - 2)) if ( (tr.ent->cast_info.currentmove->frame[0].aifunc == ai_stand) || (tr.ent->enemy && (VectorDistance(tr.ent->enemy->s.origin, tr.ent->s.origin) > 64))) { // tell them to move out the way, since they're not doing much tr.ent->cast_info.avoid(tr.ent, self, false); } // stop going for our current node self->nav_data.cache_node = -1; self->nav_data.goal_index = 0; // pause for a bit? if (!self->enemy) { self->cast_info.currentmove = self->cast_info.move_stand; self->cast_info.pausetime = level.time + 1; } return false; } else if ( (tr.ent->use) && (tr.ent->use == button_use) && (tr.ent->targetname) && (!self->activator) && (activator = G_Find( NULL, FOFS(target), tr.ent->targetname ))) { // go for this button self->activator = activator; // stop going for our current node self->nav_data.cache_node = -1; self->nav_data.goal_index = 0; } else if ((tr.ent->die == func_explosive_explode) && (tr.ent->takedamage == DAMAGE_YES)) { // blow it up T_Damage( tr.ent, self, self, dir, tr.endpos, vec3_origin, 50, 0, 0, MOD_UNKNOWN ); } else //if (tr.ent->client || tr.ent->svflags & SVF_MONSTER) { // go around it int res; res = AI_SideTrace( self, 32, 90, 0 ); if (res < 0) self->ideal_yaw = self->s.angles[YAW] - 90; else if (res > 0) self->ideal_yaw = self->s.angles[YAW] - 90; M_ChangeYaw( self ); } return false; } // if (self->waterlevel) // swimming grunt doesn't need to check for onground // return true; VectorCopy(dest, dropdest); dropdest[2] -= (16 + lift); dest[2] += 0.1; tr = gi.trace(dest, self->mins, self->maxs, dropdest, self, MASK_PLAYERSOLID|CONTENTS_MONSTERCLIP); dest[2] -= 0.1; if (!tr.startsolid && !tr.allsolid && (tr.fraction < 1)) { dest[2] = tr.endpos[2]; return true; } else { dest[2] -= lift; return false; } } /* ================= AI_movetogoal More robust movement code, which tries a CPU friendly move first, and then several walkmove()'s allowing for obstructions ================= */ qboolean AI_movetogoal (edict_t *self, edict_t *goal, float dist) { vec3_t dir, oldorg, dest; int yaw, rnd; int aborted=false; float diff, goal_dist; float slide_scale = 1.0; qboolean moved=false; qboolean changezval = false; /* if ( (self->cast_info.currentmove == self->cast_info.move_run) && (self->last_goal && self->last_goal->client) && ( (VectorLength( self->last_goal->velocity ) > dist*10) || (VectorDistance(self->s.origin, self->last_goal->s.origin) > 256))) { dist *= 2; // keep up with the player if (dist > 32) dist = 32; // Ridah, they can run through walls if more than this } */ self->flags &= ~FL_FLY; VectorSubtract(goal->s.origin, self->s.origin, dir); goal_dist = VectorNormalize(dir); self->ideal_yaw = vectoyaw(dir); // if we're walking up a steep slope, make sure we don't try and move too far if ((fabs(dir[2]) > 0.4) && (dist*fabs(dir[2]) > 20.0)) { dist = 20.0 / fabs(dir[2]); } M_ChangeYaw(self); yaw = self->s.angles[YAW]; if (dist > 4 && (diff = fabs(AngleDiff(self->s.angles[YAW], self->ideal_yaw))) > 30) { // dist *= 0.5; // turning, don't go so far // if (dist < 4) // dist = 4; if (diff < 90) dist *= (1 - 0.8*(diff / 90)); else dist *= (1 - 0.8*(90 / 90)); // walk forwards AngleVectors( self->s.angles, dir, NULL, NULL ); VectorMA( self->s.origin, dist, dir, dest ); } // can we simply move towards this position? else if ((dist > 4) || (goal_dist < 32)) { VectorMA(self->s.origin, dist, dir, dest); } else // go forward if walking { vec3_t fwd; AngleVectors( self->s.angles, fwd, NULL, NULL ); VectorMA(self->s.origin, dist, fwd, dest); } yaw = self->s.angles[YAW]; if (self->maxs[2] == DUCKING_MAX_Z) { self->maxs[2] = 8; //4; // Ridah, modified as per requested changezval = true; } moved = AI_canmove(self, dest); if (changezval) { self->maxs[2] = DUCKING_MAX_Z; } if (moved) { VectorCopy(dest, self->s.origin); gi.linkentity(self); G_TouchTriggers (self); return true; } // nope, try walkmove() if ((((int) level.time) % 6) < 3) rnd = -1; else rnd = 1; VectorCopy(self->s.origin, oldorg); self->goalentity = goal; // used by SV_movestep() for falling from air while (dist > 0) { if (M_walkmove(self, yaw, dist)) { moved=true; break; } if (dist > 16) { dist -= 8; continue; } if (!M_walkmove(self, yaw+(60*rnd), dist*0.7)) if (!M_walkmove(self, yaw+(100*rnd), dist*0.5)) if (!M_walkmove(self, yaw+(140*rnd), dist*0.3)) /* if (!M_walkmove(self, yaw-(60*rnd), dist*0.7)) if (!M_walkmove(self, yaw-(100*rnd), dist*0.5)) if (!M_walkmove(self, yaw-(140*rnd), dist*0.3)) */ { dist -= 3; continue; } break; } /* if ((dist <= 0) && (self->maxs[2] > DUCKING_MAX_Z)) { // try ducking self->maxs[2] = DUCKING_MAX_Z; if (!M_walkmove(self, yaw, dist*0.5)) { self->maxs[2] = self->cast_info.standing_max_z; } else { if (self->cast_info.move_crouch_down) self->cast_info.currentmove = self->cast_info.move_crouch_down; } } */ if (aborted) { self->goalentity = NULL; return false; } if (dist <= 0) { return false; } // always face the direction we just went VectorSubtract(self->s.origin, oldorg, dir); VectorNormalize2(dir, dir); self->ideal_yaw = vectoyaw(dir); self->yaw_speed *= 0.25; M_ChangeYaw(self); self->yaw_speed *= 4; return moved; } /* ================= AI_jump Called whenever a character needs to jump somewhere ================= */ void AI_jump (edict_t *self, float dist) { vec3_t forward; if (dist < 0) dist = -dist; // VectorCopy (self->goalentity->s.angles, self->s.angles); AngleVectors (self->s.angles, forward, NULL, NULL); VectorScale (forward, dist, self->velocity); self->velocity[2] = dist; if (self->groundentity) self->groundentity = NULL; } /* ================= AI_climb Called whenever a character is climbing a ladder ================= */ void AI_climb (edict_t *self) { static edict_t *goal = NULL; edict_t *save; if (!goal) goal = G_Spawn(); save = self->goalentity; self->goalentity = goal; VectorCopy (self->s.origin, goal->s.origin); VectorCopy (self->s.angles, goal->s.angles); goal->s.origin[2] += 20; // self->enemy = goal; self->velocity[2] = 200; // ai_move (self, 0); self->wait = level.time + 1; self->moveinfo.wait = level.time + 1; if (self->nav_data.goal_index && (level.node_data->nodes[self->nav_data.goal_index-1]->origin[2] > self->s.origin[2])) { self->flags |= FL_FLY; } else // done climbing { self->flags &= ~FL_FLY; AI_jump (self, 150); self->velocity[2] += 100; self->groundentity = NULL; self->cast_info.aiflags &= ~AI_SKILL_LADDER; self->cast_info.currentmove = self->cast_info.move_stand; self->wait = level.time + 4; self->moveinfo.wait = level.time + 1; level.node_data->nodes[self->nav_data.goal_index-1]->ignore_time = level.time + 3; // don't go back to the landing node, or we may fall down self->nav_data.goal_index = 0; // re-scan for a new path when we land self->nav_data.cache_node = -1; } // self->enemy = NULL; self->goalentity = save; self->s.frame = self->cast_info.move_stand->firstframe; } qboolean ValidBoxAtLoc(vec3_t org, vec3_t mins, vec3_t maxs, edict_t *ignore, int mask) { trace_t tr; tr = gi.trace(org, mins, maxs, org, ignore, mask); return (!tr.allsolid); } void AI_GetAvoidDirection( edict_t *self, edict_t *other ) { vec3_t dir; /* if (AI_SideTrace( self, 64, 0, 1 )) { self->ideal_yaw = entyaw( other, self ); } else */ if (NAV_GetAvoidDirection( self, other, dir )) { self->ideal_yaw = vectoyaw( dir ); } else { self->ideal_yaw = anglemod( entyaw( other, self ) + 1.0*(rand()%90 - 45) ); } } // Ridah, done. //------------------------------------------------------------------------------ // New AI system // // This is used whenever the cast member is doing something that involves moving // - will weigh up all current objectives, and carry out the desired action extern void path_corner_cast_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf); extern void path_corner_touch (edict_t *self, edict_t *other, cplane_t *plane, csurface_t *surf); void ai_runFLASHLIGHT ( edict_t *self, float dist ) { trace_t tr; vec3_t forward; vec3_t end; vec3_t mins, maxs; edict_t *light; vec3_t angles, origin; ai_run (self, dist); VectorCopy (self->s.angles, angles); angles[0] += crandom() * 1.5; angles[1] += crandom() * 1.5; angles[2] += crandom() * 1.5; VectorCopy (self->s.origin, origin); AngleVectors (angles, forward, NULL, NULL); VectorCopy (origin, end); VectorMA (end, 8194, forward, end); VectorSet (mins, -8, -8, -8 ); VectorSet (maxs, 8, 8, 8); tr = gi.trace (self->s.origin, mins, maxs, end, self, MASK_SHOT); if (tr.ent->client || tr.ent->svflags & SVF_MONSTER) if (!(tr.ent->flags & FL_NOTARGET)) EP_EventSpeech (self, tr.ent, say_flashlight); /* if (tr.ent->client || tr.ent->svflags & SVF_MONSTER) { // If this is a friend, go easy on them if (!(tr.ent->cast_group) && (tr.ent->cast_group != self->cast_group)) // they aren't on our side, are they a friend? if (!(cast_memory = level.global_cast_memory[self->character_index][tr.ent->character_index])) { // record this sighting, so we can start to attack them AI_RecordSighting(self, tr.ent, VectorDistance(self->s.origin, tr.ent->s.origin) ); cast_memory = level.global_cast_memory[self->character_index][tr.ent->character_index]; } } */ light = G_Spawn(); VectorCopy (tr.endpos, light->s.origin); light->s.renderfx |= RF_BEAM; light->s.effects |= EF_FLASHLIGHT; light->nextthink = level.time + 0.1; light->think = G_FreeEdict; gi.linkentity (light); } void ai_run ( edict_t *self, float dist ) { static edict_t tempgoal; edict_t **goal=NULL; int len; edict_t tempent, *ptempent; route_t route; node_t *goal_node; vec3_t vec, oldorg; int rval, reached_goal=false; float goaldist, ideal_dist; qboolean moved, avoiding=false; if (self->cast_info.aiflags & AI_RUN_LIKE_HELL) { dist *= 1.5; if (dist > 32) dist = 32; } if (self->cast_info.move_crwalk && (self->maxs[2] < self->cast_info.standing_max_z) && (self->cast_info.currentmove != self->cast_info.move_crwalk)) { // we should be ducking self->cast_info.currentmove = self->cast_info.move_crwalk; self->s.frame = self->cast_info.currentmove->firstframe; } // if ducking, don't stand for at least 5 seconds after we stop if (self->maxs[2] < self->cast_info.standing_max_z) { self->stand_if_idle_time = level.time + 5; } tempgoal.active_node_data = level.node_data; if (fabs(self->s.angles[2]) > 1) self->s.angles[2] *= 0.8; else self->s.angles[2] = 0; if (self->enemy && (self->enemy->health <= 0)) { self->enemy = NULL; if ( !(self->cast_info.aiflags & AI_TAKE_COVER) && !(self->goal_ent) && (self->cast_info.currentmove->frame->aifunc == ai_stand) && (self->start_ent) && (VectorDistance( self->s.origin, self->start_ent->s.origin) > 256)) { // go back to the start pos if we're not doing anything self->goal_ent = self->start_ent; } } if (self->leader && (self->leader->health <= 0)) self->leader = NULL; if (!self->groundentity) { if (self->cast_info.move_jump && self->cast_info.currentmove != self->cast_info.move_jump) self->cast_info.currentmove = self->cast_info.move_jump; return; } if (self->leader && (!self->leader->active_node_data || (self->leader->waterlevel > 1))) { // don't move self->cast_info.currentmove = self->cast_info.move_stand; return; } // if we've been told to stand put, only attack under special conditions if ( (self->leader) && (self->cast_info.aiflags & AI_HOLD_POSITION) && (!(self->cast_info.aiflags & AI_MELEE) || !self->enemy || (VectorDistance(self->s.origin, self->enemy->s.origin) > 96))) { // if running away, let us go if ( !(self->cast_info.aiflags & AI_TAKE_COVER) && (VectorDistance( self->s.origin, self->holdpos_ent->s.origin ) < AI_GUARDING_DIST) && (self->pain_debounce_time < (level.time - 2))) // Ridah, 20-may-99, added this so hired guy's don't stand around while being shot at { self->cast_info.currentmove = self->cast_info.move_stand; self->s.frame = self->cast_info.currentmove->firstframe; self->cast_info.pausetime = level.time + 1.0; return; } } if (self->cast_info.aiflags & AI_SKILL_LADDER) { if (self->groundentity) self->cast_info.aiflags &= ~AI_SKILL_LADDER; else return; } else { self->flags &= ~FL_FLY; } if ((self->cast_info.aiflags & AI_HOLD_POSITION) && !self->enemy) { // Ridah, 6-jun-99, Friendly's should avoid grenades when staying put, then come back if (!(self->cast_info.aiflags & AI_TAKE_COVER) || !(self->cover_ent) || (self->cover_ent->svflags & SVF_MONSTER)) { // we aren't avoiding anything if (VectorDistance( self->s.origin, self->holdpos_ent->s.origin ) < AI_GUARDING_DIST) { // make sure they stop taking cover self->cast_info.aiflags &= ~AI_TAKE_COVER; self->cover_ent = NULL; self->cast_info.currentmove = self->cast_info.move_stand; self->s.frame = self->cast_info.currentmove->firstframe; return; } else // run to our hold position { goal = &self->holdpos_ent; ideal_dist = AI_GUARDING_DIST; goto got_goal; } } else // we are running away from something dangerous, we should return afterwards { } } if (self->cast_info.pausetime > level.time) { self->cast_info.currentmove = self->cast_info.move_stand; return; } if (self->groundentity && self->groundentity->use && !VectorCompare(self->groundentity->velocity, vec3_origin)) { // standing on a plat that's moving self->cast_info.pausetime = level.time + 0.5; self->cast_info.currentmove = self->cast_info.move_stand; } // see if we should wait for a lift or door if (self->target_ent) { if ( !VectorCompare(self->target_ent->velocity, vec3_origin) ) { // it's moving, hang around self->cast_info.currentmove = self->cast_info.move_stand; self->cast_info.pausetime = level.time + 0.2; return; } else if (self->target_ent->nextthink > level.time + 0.1) { // it's not moving, and isn't about to start moving, so stop checking it self->target_ent = NULL; } } //=================================================================================== // // Select a "goal" goal = &self->leader; ideal_dist = AI_GUARDING_DIST; if ( (*goal) && (AI_TooClose(self, self->leader))) { // always get out of leader's way avoiding = true; goto got_goal; } if (*goal && VectorDistance( self->s.origin, (*goal)->s.origin ) > 384) { self->cast_info.aiflags |= AI_RUN_LIKE_HELL; } if (*goal && (*goal)->groundentity && (*goal)->groundentity->use) { if (!(*goal)->velocity[0] && !(*goal)->velocity[1]) { // standing still ideal_dist = AI_TOO_CLOSE_DIST; goto got_goal; } else // it's moving, so we should wait { self->cast_info.pausetime = level.time + 0.2; self->target_ent = (*goal)->groundentity; self->cast_info.currentmove = self->cast_info.move_stand; return; } } // if our leader has recently issued a followme, and is far away, go to them else if (*goal && ((*goal)->order == ORDER_FOLLOWME) && ((*goal)->order_timestamp > (level.time - 20)) && (((*goal)->order_timestamp > (level.time - 5)) || (VectorDistance((*goal)->s.origin, self->s.origin) > 256))) { // abort attack goto got_goal; } enemy_again: if (self->enemy || (self->combat_goalent && self->cover_ent)) { cast_memory_t *mem; edict_t **enemy; // find out what we're going for if (self->enemy) { enemy = &self->enemy; if (!AI_HasLeaderButGoForEnemy(self, self->enemy)) { self->enemy = NULL; goto enemy_again; } } else { enemy = &self->cover_ent; } // make sure we run if (self->maxs[2] == self->cast_info.standing_max_z) self->cast_info.currentmove = self->cast_info.move_run; // make sure we can see them, or have recently seen them if ((*enemy)->svflags & SVF_MONSTER || (*enemy)->client) { // if no memory, stop them being our enemy if (!(mem = level.global_cast_memory[self->character_index][(*enemy)->character_index])) { (*enemy) = NULL; goto enemy_again; } if ( ((!self->combat_goalent) || (self->combat_goalent->think != CheckStillHiding)) && ((*enemy)->client && ((*enemy)->light_level <= 30)) && (mem->timestamp < (level.time - 5))) { // go for the last sighted position // DEMO HACK: send Bernie to Louie if he's alive (drastic times call for drastic measures) if ( (level.episode == EP_SKIDROW) && (self->name_index == NAME_ARNOLD)) { edict_t *louie; route_t route; louie = EP_GetCharacter( NAME_LOUIE ); // if they're near Louie, better go protect him if ( (louie) && (louie->health > 0) && ( !NAV_Route_EntityToEntity((*enemy), NULL, louie, VIS_PARTIAL, false, &route) || (route.dist < 3000))) { self->goal_ent = louie; (*enemy) = NULL; louie->cast_info.aiflags |= AI_GOALENT_MANUAL_CLEAR; goto gonetolouie; } } // done DEMO HACK memset( &tempent, 0, sizeof(edict_t) ); tempent.active_node_data = level.node_data; VectorCopy( mem->last_known_origin, tempent.s.origin ); ptempent = &tempent; goal = &ptempent; VectorCopy( (*enemy)->maxs, tempent.maxs ); VectorCopy( (*enemy)->mins, tempent.mins ); if ((VectorDistance(self->s.origin, (*goal)->s.origin) < 128) && AI_ClearSight( self, (*goal), false )) { (*enemy) = NULL; goto enemy_again; } gonetolouie: goto got_goal; } } goal = &(*enemy); ideal_dist = AI_GUARDING_DIST; if ( self->combat_goalent && (self->combat_goalent->inuse || (self->combat_goalent = NULL))) // clear it if it's been freed { goal = &self->combat_goalent; ideal_dist = 16; // get really close // if we're right infront of our enemy, abort if ( (self->enemy) && !(self->cast_info.aiflags & AI_GOAL_IGNOREENEMY) // Ridah, 17-mar-99, fixes Jesus not going to threshold_target && (VectorDistance( self->s.origin, self->enemy->s.origin ) < 128) && (infront( self, self->enemy )) && (self->last_getcombatpos < (level.time - 3)) && (AI_ClearSight( self->enemy, self, false ))) { // abort self->combat_goalent = NULL; self->cast_info.aiflags &= ~AI_TAKE_COVER; goto enemy_again; } if ( !(self->cast_info.aiflags & AI_MELEE) && !(self->cast_info.aiflags & AI_GOAL_IGNOREENEMY) // Ridah, 17-mar-99, fixes Jesus not going to threshold_target && !(self->cast_info.aiflags & AI_TAKE_COVER) && ((*goal)->think == G_FreeEdict) && AI_ClearSight(self, (*enemy), true)) { if (self->cast_info.checkattack(self)) { self->combat_goalent = NULL; return; // this character has started an attack, and doesn't want to move any further } } if ( (self->cast_info.aiflags & AI_TAKE_COVER) && (AI_ClearSight((*enemy), *goal, false))) { // abort it self->cast_info.aiflags &= ~AI_TAKE_COVER; self->combat_goalent = NULL; self->dont_takecover_time = level.time + 10; goto enemy_again; } } else if (self->cast_info.checkattack) { // do a character specific check for beginning an attack if (AI_CheckTakeCover(self)) { // taking cover // goto (*enemy)_again; self->goalentity = NULL; self->wait = -1; return; } else if (AI_ClearSight(self, (*goal), true)) { if (self->cast_info.checkattack(self)) { self->dont_takecover_time = level.time + 5; // attack for at least a few seconds return; // this character has started an attack, and doesn't want to move any further } } else if (self->last_getcombatpos < (level.time - 2)) { // look for a good combat position float *pos; if (pos = NAV_GetCombatPos( self, (*goal), self->cast_info.aiflags & AI_MELEE )) { edict_t *combatent; combatent = G_Spawn(); VectorCopy( pos, combatent->s.origin ); self->cast_info.aiflags &= ~AI_GOAL_IGNOREENEMY; self->cast_info.aiflags &= ~AI_TAKE_COVER; self->combat_goalent = combatent; combatent->cast_info.aiflags |= AI_GOAL_RUN; combatent->think = G_FreeEdict; combatent->nextthink = level.time + 5; // give us some time to get there // go for it this frame goto enemy_again; } } } } else { // look for things to be hostile at if ((self->s.frame == self->cast_info.currentmove->firstframe) && AI_FindTarget (self)) goto enemy_again; } // If guarding something, and we've moved out of range, abort the pursuit if (self->guard_ent) { if (!(*goal)) { // if not doing anything else, return to the guard_ent if ( (VectorDistance( self->s.origin, self->guard_ent->s.origin ) > self->guard_ent->guard_radius) || (!AI_ClearSight(self, self->guard_ent, false))) // if (VectorDistance( self->s.origin, self->guard_ent->s.origin ) > AI_GUARDING_DIST) { goal = &self->guard_ent; ideal_dist = AI_GUARDING_DIST; } } else if ( (VectorDistance( self->s.origin, self->guard_ent->s.origin ) > self->guard_ent->guard_radius) && (!gi.inPVS(self->s.origin, self->guard_ent->s.origin)) && (!self->enemy || !AI_ClearSight(self, self->enemy, false))) // don't go back if we can see our enemy { cast_memory_t *mem; // always abort an enemy if it exists if (self->enemy) { mem = level.global_cast_memory[self->character_index][self->enemy->character_index]; if (mem) { mem->ignore_time = level.time + ENEMY_SIGHT_DURATION + 0.2; // ignore them for a bit } self->enemy = NULL; } goal = &self->guard_ent; ideal_dist = AI_GUARDING_DIST; } } got_goal: // Ridah, 17-may-99, fixes Jesus not going to threshold_target if ((!(*goal) || (self->cast_info.aiflags & AI_GOAL_IGNOREENEMY)) && self->goal_ent) { goal = &self->goal_ent; if ((*goal)->solid == SOLID_TRIGGER) ideal_dist = 0; else if ((*goal)->dmg_radius) ideal_dist = (*goal)->dmg_radius; else ideal_dist = 128; } // should we press a button? if (self->activator) { if (self->activator->moveinfo.state != STATE_BOTTOM) { // button has been pressed (probably by someone else) self->activator = NULL; } else // go for the button { goal = &self->activator; ideal_dist = 0; } } //=================================================================================== // // move towards this "goal" // always run to our attacker if on fire if ((self->onfiretime > 0) && (self->onfireent)) { goal = &self->onfireent; ideal_dist = 64; } self->last_goal = *goal; if (!(*goal)) { // we don't have a goal, time for a beer self->cast_info.currentmove = self->cast_info.move_stand; return; } else // we have a "goal" { // if it's our leader, make sure they are still reachable (not on a platform) if ( ((*goal) == self->leader) && ((*goal)->groundentity) && ((*goal)->groundentity->use) && (!VectorCompare((*goal)->groundentity->velocity, vec3_origin))) { // They're on a lift, so hang around for a bit self->cast_info.pausetime = level.time + 2; self->cast_info.currentmove = self->cast_info.move_stand; self->s.frame = self->cast_info.currentmove->firstframe; return; } // handle ->goal_ent specially else if ((goal == &self->goal_ent) || (goal == &self->combat_goalent) || ((goal == &self->leader) && !avoiding)) { float goal_dist; goal_dist = VectorDistance( self->s.origin, (*goal)->s.origin ); // Make sure we are running if we should be if (self->maxs[2] == self->cast_info.standing_max_z) { if (self->cast_info.move_runwalk && (goal == &self->leader) && (goal_dist < 256)) { if (self->cast_info.currentmove != self->cast_info.move_runwalk) { self->cast_info.currentmove = self->cast_info.move_runwalk; self->s.frame = self->cast_info.currentmove->firstframe; self->cast_info.aiflags &= ~AI_RUN_LIKE_HELL; } } else if (((*goal) == self->enemy) || ((*goal)->cast_info.aiflags & AI_GOAL_RUN) /*&& (goal_dist > ideal_dist*2)*/) { self->cast_info.currentmove = self->cast_info.move_run; } else if (self->cast_info.move_runwalk) { self->cast_info.currentmove = self->cast_info.move_runwalk; self->cast_info.aiflags &= ~AI_RUN_LIKE_HELL; } } // should we stop moving? if ((*goal)->solid != SOLID_TRIGGER) { // we can't touch it, so just get close to it if (goal_dist < ideal_dist) // FIXME: make this configurable? { if ( (goal != &self->leader) || ( ((*goal)->client == NULL && (*goal)->cast_info.currentmove->frame->aifunc == ai_stand) || ((*goal)->client != NULL && VectorLength((*goal)->velocity) < 50))) { self->cast_info.currentmove = self->cast_info.move_stand; if ((goal == &self->goal_ent) && !(self->goal_ent->cast_info.aiflags & AI_GOALENT_MANUAL_CLEAR)) { self->goal_ent = NULL; // we made it there } if (goal == &self->combat_goalent) // we've reached it, so clear it out { if (!(self->cast_info.aiflags & AI_TAKE_COVER)) { self->combat_goalent = NULL; } } else if ((*goal) == self->start_ent && (*goal) == self->goal_ent) { self->goal_ent->cast_info.aiflags &= ~AI_GOAL_RUN; self->goal_ent = NULL; } return; } } else { if ( ((*goal)->solid != SOLID_BBOX) && (VectorDistance( self->s.origin, (*goal)->s.origin ) < 64) && (!ValidBoxAtLoc( (*goal)->s.origin, self->mins, self->maxs, self, MASK_PLAYERSOLID ))) { // we can't get there self->cast_info.aiflags &= ~AI_TAKE_COVER; *goal = NULL; return; } } } // is it dangerous to go for it? else if ( (self->enemy) && !(self->cast_info.aiflags & AI_GOAL_IGNOREENEMY) // Ridah, 17-mar-99, fixes Jesus not going to threshold_target && ((*goal)->touch == path_corner_cast_touch) && ( (VectorDistance( self->enemy->s.origin, (*goal)->s.origin ) < 256) || ( (VectorDistance( self->enemy->s.origin, self->s.origin ) < 512) && (infront(self->enemy, self))))) { // simulate touching the path_corner (*goal)->touch( (*goal), self, NULL, NULL ); return; } // can we walk straight towards it? else if ( (goal_dist < ideal_dist*2) && (NAV_Visible( self->s.origin, (*goal)->s.origin, VIS_PARTIAL, self->maxs[2] < self->cast_info.standing_max_z )) && (NAV_Reachable( self->s.origin, (*goal)->s.origin, (byte)self->waterlevel, (byte)(*goal)->waterlevel, (self->maxs[2] < self->cast_info.standing_max_z), REACHABLE_POOR ))) { self->nav_data.cache_node = -1; self->nav_data.goal_index = 0; if (goal_dist < ideal_dist/2) { if ((*goal)->target && ((*goal)->touch == path_corner_cast_touch)) { // simulate touching the path_corner (*goal)->touch( (*goal), self, NULL, NULL ); } else { if (!strcmp( (*goal)->classname, "cast_origin" )) { goal = NULL; } self->cast_info.currentmove = self->cast_info.move_stand; return; } } } // NAV_DrawLine( self->s.origin, (*goal)->s.origin ); } else if (goal == &self->guard_ent) { // just walk back there if ( (self->maxs[2] == self->cast_info.standing_max_z) && (self->cast_info.currentmove == self->cast_info.move_run)) { self->cast_info.currentmove = self->cast_info.move_runwalk; } } } // are we within the ideal range of our goal? if ( ((*goal) != self->enemy) && !((*goal)->touch) && (ideal_dist > 0.1) && ((goaldist = VectorDistance (self->s.origin, (*goal)->s.origin)) < ideal_dist) && NAV_Visible( self->s.origin, (*goal)->s.origin, VIS_LINE, self->maxs[2] < self->cast_info.standing_max_z )) // && AI_ClearSight(self, (*goal))) { self->cast_info.aiflags &= ~AI_IGNORE_ENEMY; if ( (!self->leader || ((*goal) != self->leader->moveout_ent)) && ( ((*goal) != self->leader) || (!(*goal)->groundentity) || (!(*goal)->groundentity->use) || (!VectorCompare((*goal)->velocity, vec3_origin))) && ( (AI_FollowLeader(self, (*goal)) // if they are on a lift, and trying to move, we should avoid them, otherwise stay real close && ( !((*goal)->groundentity) || !((*goal)->groundentity->use) || !VectorCompare((*goal)->velocity, vec3_origin))))) { // move back, away from goal VectorSubtract(self->s.origin, (*goal)->s.origin, vec); VectorNormalize2(vec, vec); VectorScale(vec, 64, vec); VectorAdd(self->s.origin, vec, tempgoal.s.origin); AI_movetogoal(self, &tempgoal, dist); return; } self->cast_info.currentmove = self->cast_info.move_stand; return; } // Check for fast running (same as player) // Ridah, if the guy is a friendly, and he's hurt, don't run as fast if (self->cast_group != 1 || self->health > self->max_health/2) { if (((*goal)->client || ((*goal)->svflags & SVF_MONSTER)) || ((*goal)->cast_info.aiflags & AI_RUN_LIKE_HELL)) { if (self->cast_info.aiflags & AI_RUN_LIKE_HELL) { dist*=2.5; if (dist > 32) dist = 32; // Ridah, they can run through walls if more than this } // RAFAEL 30-DEC-98 if (self->cast_info.aiflags & AI_RUSH_THE_PLAYER) { dist*=2.5; if (dist > 32) dist = 32; } } } // need to put this here because the dist for new models are > 32 if (dist > 32) dist = 32; //=================================================================================== // // We need to move somewhere tempgoal.nav_data.goal_index = self->nav_data.goal_index; goal_node = NULL; // find a path to the player if (self->nav_data.goal_index > 0) { goal_node = level.node_data->nodes[self->nav_data.goal_index-1]; rval = ROUTE_INDIRECT; } else { rval = NAV_Route_EntityToEntity(self, goal_node, (*goal), VIS_PARTIAL, false, &route); // update current node self->nav_data.goal_index = tempgoal.nav_data.goal_index = route.path+1; if (rval == ROUTE_INDIRECT) { goal_node = level.node_data->nodes[self->nav_data.goal_index-1]; // is it a crouching node? if (goal_node->node_type & NODE_DUCKING) { if (self->maxs[2] > DUCKING_MAX_Z) { if (self->cast_info.move_crouch_down) self->cast_info.currentmove = self->cast_info.move_crouch_down; self->maxs[2] = DUCKING_MAX_Z; return; } } // allow 5 seconds to reach the new target self->wait = level.time + 5; } else if (rval == ROUTE_DIRECT) { // Disabled this, if we have a direct route, then we don't need to change this /* if (((*goal)->solid == SOLID_BBOX) && ((*goal)->maxs[2] < (*goal)->cast_info.standing_max_z) && (self->maxs[2] > DUCKING_MAX_Z)) { if (self->cast_info.move_crouch_down) self->cast_info.currentmove = self->cast_info.move_crouch_down; self->maxs[2] = DUCKING_MAX_Z; return; } */ self->wait = level.time + 3.5; } } self->last_rval = rval; if (!rval) { // no route int max_cnt=50; if ((goal != &self->enemy) || !(self->cast_info.aiflags & AI_MELEE) || (self->cast_info.aiflags & AI_NO_TAKE_COVER)) { // resort to id's movement code (YUCK!) self->goalentity = (*goal); M_MoveToGoal (self, dist); } else // hide until we can reach them again { AI_ForceTakeCover( self, self->enemy, true ); self->cast_info.aiflags &= ~AI_TAKECOVER_IGNOREHEALTH; } if (nav_aipath->value && (*goal)) { gi.dprintf( "%s", self->classname ); if (self->name) gi.dprintf( " (%s)", self->name ); gi.dprintf( " has no route to %s\n", self->classname, (*goal)->classname ); } if (gi.inPVS( g_edicts[1].s.origin, self->s.origin )) { max_cnt = 5; } if (self->noroute_count++ > max_cnt) // keep trying for some time { if (/*(*goal) ==*/ self->enemy) // don't bother going for them if there isn't a route { self->combat_goalent = NULL; AI_ForceTakeCover( self, self->enemy, false ); // if (self->cast_info.aiflags & AI_TAKE_COVER) return; } else if (self->combat_goalent && (*goal) == self->combat_goalent) { self->combat_goalent = NULL; return; } // stop going for whatever we were going for self->cast_info.pausetime = level.time + 2; self->cast_info.currentmove = self->cast_info.move_stand; self->s.frame = self->cast_info.currentmove->firstframe; return; } goto done; } else { self->noroute_count = 0; } // successfully found a route if (rval == ROUTE_DIRECT) { // we can reach the "goal" directly float goal_dist; goal_dist = VectorDistance( self->s.origin, (*goal)->s.origin ); if (goal_dist < dist) dist = goal_dist; self->goalentity = (*goal); AI_movetogoal(self, (*goal), dist); if (!(*goal)) // it's been cleared somewhere return; if (nav_aipath->value) { NAV_DrawLine( self->s.origin, (*goal)->s.origin ); } goto done; } // indirect route // ============================================================================================= // FIXME: if goal is a character, and we are not within gi.PHS() or gi.PVS(), then we can't // follow them, since we can't see or hear them. Therefore, we should either go to their last // sighted position, or give up on them. // ============================================================================================= VectorCopy(goal_node->origin, tempgoal.s.origin); tempgoal.s.origin[2] = self->s.origin[2]; len = VectorDistance(self->s.origin, tempgoal.s.origin); tempgoal.s.origin[2] = goal_node->origin[2]; if (len <= dist*1.2) { // reached the waypoint reached_goal = true; dist = len; } if (nav_aipath->value) { NAV_DrawLine( self->s.origin, tempgoal.s.origin ); NAV_Debug_DrawPath( &tempgoal, (*goal) ); } //=================================================================================== // // are we about to reach the "goal"? if (reached_goal) { // should we JUMP? if (goal_node->node_type & NODE_JUMP) { // do we really want to jump here? if ( (NAV_Route_NodeToEntity(goal_node, (*goal), VIS_PARTIAL, &route) == ROUTE_INDIRECT) && (route.path == goal_node->goal_index)) { if ((level.node_data->nodes[goal_node->goal_index]->origin[2] - 54) > goal_node->origin[2]) { // must be a ladder // note to Ryan jun-07-99 // The dogs can go up and down ladders. Can this be stopped? // move us to the ladder if possible, if not abort climbing /* if (ValidBoxAtLoc( tempgoal.s.origin, self->mins, self->maxs, self, MASK_PLAYERSOLID )) { VectorCopy( tempgoal.s.origin, self->s.origin ); self->s.angles[YAW] = self->ideal_yaw = (float) goal_node->yaw; self->cast_info.aiflags |= AI_SKILL_LADDER; self->nav_data.goal_index = route.path+1; AI_climb (self); if (self->cast_info.move_start_climb) self->cast_info.currentmove = self->cast_info.move_start_climb; } else // abort running for a bit { self->cast_info.currentmove = self->cast_info.move_stand; self->cast_info.pausetime = level.time + 1.0; self->nav_data.goal_index = 0; return; } */ if ((self->gender) && ValidBoxAtLoc( tempgoal.s.origin, self->mins, self->maxs, self, MASK_PLAYERSOLID )) { VectorCopy( tempgoal.s.origin, self->s.origin ); self->s.angles[YAW] = self->ideal_yaw = (float) goal_node->yaw; self->cast_info.aiflags |= AI_SKILL_LADDER; self->nav_data.goal_index = route.path+1; AI_climb (self); if (self->cast_info.move_start_climb) self->cast_info.currentmove = self->cast_info.move_start_climb; } else // abort running for a bit { self->cast_info.currentmove = self->cast_info.move_stand; self->cast_info.pausetime = level.time + 1.0; self->nav_data.goal_index = 0; return; } } else // normal jump { vec3_t neworg; float speed; self->flags &= ~FL_FLY; VectorSubtract(level.node_data->nodes[goal_node->goal_index]->origin, goal_node->origin, vec); VectorNormalize2(vec, vec); self->s.angles[YAW] = vectoyaw(vec); if ((speed = VectorLength(goal_node->jump_vel)) > 250) { vec3_t vec; float length; AI_jump (self, 200); VectorCopy(goal_node->jump_vel, vec); self->velocity[2] = 0; vec[2] = 0; length = VectorLength( vec ); if (length < 20) { VectorNormalize( vec ); VectorScale( vec, 20, vec ); goal_node->jump_vel[0] = vec[0]; goal_node->jump_vel[1] = vec[1]; level.node_data->modified = true; } else { VectorNormalize( self->velocity ); VectorScale( self->velocity, length, self->velocity ); } self->velocity[2] = goal_node->jump_vel[2]; if (goal_node->jump_vel[2] > 200) self->velocity[2] = 300; else if (goal_node->jump_vel[2] < 180) self->velocity[2] = 180; } else { AI_jump (self, 200); self->velocity[2] = 200; } VectorCopy(self->s.origin, neworg); neworg[2] += 1; if (ValidBoxAtLoc(neworg, self->mins, self->maxs, self, MASK_PLAYERSOLID)) self->s.origin[2] += 1; self->groundentity = NULL; if (self->cast_info.move_jump) self->cast_info.currentmove = self->cast_info.move_jump; // we need a way of playing character specific jump sounds //gi.sound(self, CHAN_VOICE, gi.soundindex("*jump1.wav"), 0.8, ATTN_NORM, 0); gi.linkentity(self); } self->nav_data.goal_index = goal_node->goal_index + 1; reached_goal = false; goto done; } } } //=================================================================================== // // Perform the move towards the goal node VectorCopy(self->s.origin, oldorg); // move towards the node moved = AI_movetogoal(self, &tempgoal, dist); //................................................................................... // Evaluate the move // did we get closer? if (moved) { self->moveinfo.wait = level.time + 0.4; } else if ( (self->moveinfo.wait < level.time) || (self->wait < level.time) || (!ValidBoxAtLoc( tempgoal.s.origin, self->mins, self->maxs, self, MASK_PLAYERSOLID))) { // abort this node, we've been trying to get to it for long enough goal_node->ignore_time = level.time + 3; self->nav_data.cache_node = -1; self->moveinfo.wait = level.time + 0.7; // give some time on the next node self->wait = level.time + 3; self->nav_data.goal_index = 0; reached_goal = false; } else // will a jump be useful here? { if ((self->s.origin[2] + 16 < (&tempgoal)->s.origin[2])) { vec3_t start, mins, dir, org; trace_t trace; VectorCopy(self->s.origin, start); AngleVectors(self->s.angles, dir, NULL, NULL); VectorScale(dir, 32, dir); VectorAdd(start, dir, org); VectorCopy(self->mins, mins); mins[2] += 8; trace = gi.trace(start, mins, self->maxs, org, self, MASK_PLAYERSOLID); if ((trace.fraction < 1) && !(trace.ent->svflags & SVF_MONSTER) && (fabs(trace.plane.normal[2]) < 0.1)) { // set ideal velocity dir[2] = 310; // see if clear at head start[2] += 32; org[2] += 32; trace = gi.trace(start, NULL, NULL, org, self, MASK_SOLID); if (trace.fraction == 1) { // safe to jump AI_jump (self, 150); self->velocity[2] += 100; if (self->cast_info.move_jump) self->cast_info.currentmove = self->cast_info.move_jump; } } } } done: if ( (self->maxs[2] < self->cast_info.standing_max_z) && ( (goal_node && !(goal_node->node_type & NODE_DUCKING)) || (rval == ROUTE_DIRECT))) { // we should return to standing, if possible trace_t tr; self->maxs[2] = self->cast_info.standing_max_z; tr = gi.trace(self->s.origin, self->mins, self->maxs, self->s.origin, self, MASK_PLAYERSOLID); if ( tr.startsolid || (goal_node && !NAV_Visible( self->s.origin, goal_node->origin, VIS_PARTIAL, false )) || ((rval == ROUTE_DIRECT) && (*goal) && !NAV_Visible( self->s.origin, (*goal)->s.origin, VIS_PARTIAL, false ))) { // can't safely stand self->maxs[2] = DUCKING_MAX_Z; } else if (self->cast_info.move_stand_up) // ok to stand { //gi.dprintf("2\n"); self->cast_info.currentmove = self->cast_info.move_stand_up; self->s.frame = self->cast_info.currentmove->firstframe; return; } else { self->cast_info.currentmove = self->cast_info.move_run; } } if (reached_goal) { // press a button? if ( (goal_node->node_type & NODE_BUTTON) && (goal_node->goal_ent) && ((goal_node->goal_ent->timestamp < (level.time - 4)) || (goal_node->goal_ent->activator != self)) && (goal_node->goal_ent->use)) { // simulate touching it goal_node->goal_ent->use( goal_node->goal_ent, self, self ); self->cast_info.pausetime = level.time + 2; // wait for it to do something // walk backwards self->cast_info.currentmove = self->cast_info.move_avoid_reverse_walk; self->ideal_yaw = (float)goal_node->yaw; // don't turn self->activator = NULL; // stop going for a button return; } if ( (goal_node->node_type & NODE_PLAT) && (self->groundentity == goal_node->goal_ent) && (VectorCompare( self->groundentity->velocity, vec3_origin ))) { // we're standing on it, so trigger it if (self->groundentity->targetname) { edict_t *btn=NULL, *btn2=NULL; char *targetname; targetname = self->groundentity->targetname; while (btn = G_Find( btn, FOFS( target ), targetname )) { if (!strcmp(btn->classname, "trigger_relay") && btn->targetname) { // check this target btn2 = NULL; while (btn2 = G_Find( btn2, FOFS( target ), btn->targetname )) { if (fabs( btn2->absmin[2] - self->s.origin[2] ) < 64) { btn2->use( btn2, self, self ); break; } } } else if (btn->solid == SOLID_BSP) { if (fabs( btn->absmin[2] - self->s.origin[2] ) < 64) { btn->use( btn, self, self ); break; } } } } else { self->groundentity->use( self->groundentity, self, self ); } return; } if (!(*goal)) { // it's been cleared return; } // find the next waypoint rval = NAV_Route_EntityToEntity( self, goal_node, (*goal), VIS_PARTIAL, false, &route); self->nav_data.goal_index = tempgoal.nav_data.goal_index = route.path+1; if (rval == ROUTE_INDIRECT) { int i, temp_rval, rval2; node_t *temp_node; // check for a plat along the route that's not ready temp_node = goal_node; i = 0; while ((temp_rval = NAV_Route_NodeToEntity( temp_node, (*goal), VIS_PARTIAL, &route)) && (i++ < 3)) { if (temp_rval == ROUTE_DIRECT) break; temp_node = level.node_data->nodes[route.path]; if (temp_node->node_type & NODE_PLAT) { if ( temp_node->goal_ent && VectorCompare(temp_node->goal_ent->velocity, vec3_origin)) { // platform is stationary trace_t tr; vec3_t src, dest; // make sure this lift will take us to the goal if ( !(rval2 = NAV_Route_NodeToEntity( temp_node, (*goal), VIS_PARTIAL, &route)) || (rval2 == ROUTE_DIRECT) || (route.path != temp_node->goal_index)) { break; // we don't want to go on the lift } // make sure it is waiting for us VectorCopy(temp_node->origin, dest); dest[2] -= 1; VectorCopy(temp_node->origin, src); src[2] += 1; tr = gi.trace(src, self->mins, self->maxs, dest, self, MASK_SOLID); if (!tr.startsolid && (tr.ent == temp_node->goal_ent) && (tr.fraction < 1)) break; // cool, it's there waiting for us // hmm, it's not waiting, should we press a button for it to come back down? if (temp_node->goal_ent->targetname) { edict_t *btn=NULL, *btn2=NULL; char *targetname; targetname = temp_node->goal_ent->targetname; while (btn = G_Find( btn, FOFS( target ), targetname )) { if (!strcmp(btn->classname, "trigger_relay") && btn->targetname) { // check this target btn2 = NULL; while (btn2 = G_Find( btn2, FOFS( target ), btn->targetname )) { if (fabs( btn2->absmin[2] - self->s.origin[2] ) < 64) { btn2->use( btn2, self, self ); break; } } } else if (btn->solid == SOLID_BSP) { if (fabs( btn->absmin[2] - self->s.origin[2] ) < 64) { btn->use( btn, self, self ); break; } } } } } // damn, route is broken, so hang around for a bit self->cast_info.pausetime = level.time + 2; self->target_ent = temp_node->goal_ent; self->cast_info.currentmove = self->cast_info.move_stand; self->nav_data.goal_index = tempgoal.nav_data.goal_index = goal_node->index+1; // check again when done waiting return; } } self->wait = level.time + 3; goal_node = level.node_data->nodes[self->nav_data.goal_index-1]; // is it a crouching node? if (goal_node->node_type & NODE_DUCKING) { if (self->maxs[2] == self->cast_info.standing_max_z) { if (self->cast_info.move_crouch_down) self->cast_info.currentmove = self->cast_info.move_crouch_down; self->maxs[2] = DUCKING_MAX_Z; } } } } else if (goal_node && !(goal_node->node_type & NODE_DUCKING) && (self->maxs[2] < self->cast_info.standing_max_z)) { // can we stand? if (NAV_Visible( self->s.origin, goal_node->origin, VIS_PARTIAL, false )) { self->maxs[2] = self->cast_info.standing_max_z; if (self->cast_info.move_stand_up) { self->cast_info.currentmove = self->cast_info.move_stand_up; self->s.frame = self->cast_info.currentmove->firstframe; return; } } } self->goalentity = (*goal); } void ai_runDOKEY (edict_t *self, float dist) { AI_movetogoal(self, self->goal_ent, dist); }