// g_ai.c #include #include "g_local.h" #include "g_monster.h" #include "Random.h" #include "vector.h" #include "buoy.h" #include "m_stats.h" #include "p_anims.h" qboolean FindTarget (edict_t *self); extern cvar_t *maxclients; qboolean ai_checkattack (edict_t *self, float dist); //void gkrokon_maintain_waypoints(edict_t *self, float mintel, float foo1, float foo2); void SV_NewChaseDir (edict_t *actor, edict_t *enemy, float dist); //void gkrokon_hopdown(edict_t *self); void ssithraCheckJump (edict_t *self); void ssithraStartle (edict_t *self); void ssithraLookRight (edict_t *self); void seraph_startle(edict_t *self); void SV_FixCheckBottom (edict_t *ent); qboolean clear_visible (edict_t *self, edict_t *other); trace_t MG_WalkMove (edict_t *self, float yaw, float dist); qboolean MG_MoveToGoal (edict_t *self, float dist); float MG_ChangeYaw (edict_t *self); void MG_BuoyNavigate(edict_t *self); void MG_Pathfind(edict_t *self, qboolean check_clear_path); float MG_FaceGoal (edict_t *self, qboolean doturn); qboolean ok_to_wake (edict_t *monster, qboolean gorgon_roar, qboolean ignore_ambush); qboolean EqualAngle(float angle1, float angle2, float leniency); // AI Targeting Globals qboolean enemy_vis; // TRUE if enemy is visible qboolean enemy_infront; // TRUE if enemy is in front int enemy_range; // range from enemy RANGE_MELEE, RANGE_NEAR, RANGE_MID, RANGE_FAR float enemy_yaw; // ideal yaw to face enemy //============================================================================ // **************************************************************************** // ai_trystep // // Like other functions to attempt a step, but this will return exactly // what happened to make it fail. // **************************************************************************** int ai_trystep(edict_t *ent, vec3_t move) { vec3_t oldorg, neworg, copyorg, end, inf_mins, inf_maxs; trace_t trace; float stepsize = 18; vec3_t test; int contents; // try the move VectorCopy (ent->s.origin, oldorg); VectorCopy (ent->s.origin, copyorg); VectorCopy (ent->mins, inf_mins); VectorCopy (ent->maxs, inf_maxs); VectorAdd (ent->s.origin, move, neworg); VectorScale(inf_mins, 2, inf_mins); VectorScale(inf_maxs, 2, inf_maxs); // push down from a step height above the wished position neworg[2] += stepsize; VectorCopy (neworg, end); end[2] -= stepsize*2; gi.trace (neworg, inf_mins, inf_maxs, end, ent, MASK_MONSTERSOLID,&trace); if (trace.allsolid) return TRYSTEP_ALLSOLID; if (trace.startsolid) { neworg[2] -= stepsize; gi.trace (neworg, ent->mins, ent->maxs, end, ent, MASK_MONSTERSOLID,&trace); if (trace.allsolid || trace.startsolid) return TRYSTEP_STARTSOLID; } // don't go in to water unless only 40% hieght deep if (ent->waterlevel == 0) { test[0] = trace.endpos[0]; test[1] = trace.endpos[1]; test[2] = trace.endpos[2] + ent->mins[2];// + 1; test[2] += (ent->maxs[2] - ent->mins[2]) * 0.4; contents = gi.pointcontents(test); if (contents & MASK_WATER) return TRYSTEP_INWATER; } if (trace.fraction == 1) { // if monster had the ground pulled out, go ahead and fall if ( ent->flags & FL_PARTIALGROUND ) { VectorAdd (ent->s.origin, move, ent->s.origin); ent->groundentity = NULL; return TRYSTEP_OK; } // walked off an edge return TRYSTEP_OFFEDGE; } // check point traces down for dangling corners VectorCopy (trace.endpos, ent->s.origin); if (!M_CheckBottom (ent)) { if ( ent->flags & FL_PARTIALGROUND ) { VectorCopy (oldorg, ent->s.origin); return TRYSTEP_OK; } VectorCopy (oldorg, ent->s.origin); return TRYSTEP_NOSUPPORT; } if ( ent->flags & FL_PARTIALGROUND ) ent->flags &= ~FL_PARTIALGROUND; ent->groundentity = trace.ent; ent->groundentity_linkcount = trace.ent->linkcount; VectorCopy (oldorg, ent->s.origin); return TRYSTEP_OK; } /* ================= AI_SetSightClient Called once each frame to set level.sight_client to the player to be checked for in findtarget. If all clients are either dead or in notarget, sight_client will be null. In coop games, sight_client will cycle between the clients. ================= */ void AI_SetSightClient (void) { edict_t *ent; int start, check; if (level.sight_client == NULL) start = 1; else start = level.sight_client - g_edicts; check = start; while (1) { check++; if (check > game.maxclients) check = 1; ent = &g_edicts[check]; if (ent->inuse && ent->health > 0 && !(ent->flags & FL_NOTARGET) ) { level.sight_client = ent; return; // got one } if (check == start) { level.sight_client = NULL; return; // nobody to see } } } // **************************************************************************** // ai_maintain_waypoints // // checks all waypoints and corrects all buoys // **************************************************************************** void ai_maintain_waypoints(edict_t *self, float mintel, float foo1, float foo2) { vec3_t vec; float len; if (self->enemy) { if (visible(self, self->enemy)) { VectorCopy(self->pos1, self->pos2); VectorCopy(self->enemy->s.origin, self->pos1); } } if (self->monsterinfo.searchType == SEARCH_COMMON) return; else if (self->monsterinfo.searchType == SEARCH_BUOY) { VectorSubtract(self->s.origin, self->monsterinfo.nav_goal, vec); len = VectorLength(vec); if (len < 24) { //gi.dprintf("gkrokon_maintain_waypoints: arrived at target buoy\n"); if ((self->monsterinfo.stepState == PATHDIR_FORWARD && self->goalentity->enemy == NULL) || (self->monsterinfo.stepState == PATHDIR_BACKWARD && self->goalentity->owner == NULL)) { //gi.dprintf("gkrokon_maintain_waypoints: path exhausted, seeking target"); FindTarget(self); self->monsterinfo.searchType = SEARCH_COMMON; return; } if (self->monsterinfo.stepState == PATHDIR_FORWARD) { //gi.dprintf("gkrokon_maintain_waypoints: forward targetting %s\n", self->goalentity->targetname); self->enemy = self->movetarget = self->goalentity = self->goalentity->enemy; VectorCopy(self->goalentity->s.origin, self->monsterinfo.nav_goal); } else if (self->monsterinfo.stepState == PATHDIR_BACKWARD) { //gi.dprintf("gkrokon_maintain_waypoints: reverse targetting %s\n", self->goalentity->owner->targetname); self->enemy = self->movetarget = self->goalentity = self->goalentity->owner; VectorCopy(self->goalentity->s.origin, self->monsterinfo.nav_goal); } /*else gi.dprintf("gkrokon_maintain_waypoints: volatile assignment\n");*/ } } } // **************************************************************************** // ai_hopdown // // Checks to see if entity can hop down safely // **************************************************************************** int ai_hopdown(edict_t *self, vec3_t goalpos, float height_max) { vec3_t vf, source, source2; vec3_t maxs; trace_t trace; //Setup the trace VectorCopy(self->maxs, maxs); VectorCopy(self->s.origin, source); AngleVectors(self->s.angles, vf, NULL, NULL); VectorMA(source, 128, vf, source); maxs[2] += 128; gi.trace (self->s.origin, self->mins, maxs, source, self, MASK_ALL,&trace); if (trace.fraction == 1) { VectorCopy(source, source2); source2[2] -= height_max; gi.trace (source, self->mins, self->maxs, source2, self, MASK_ALL,&trace); if (trace.allsolid || trace.startsolid) return false; if (trace.fraction == 1) { if (stricmp(trace.ent->classname, "worldspawn")) return false; } else { if (trace.ent == (struct edict_s *)-1) return false; if (trace.contents != CONTENTS_SOLID) return false; else { VectorSubtract(trace.endpos, self->s.origin, source2); VectorNormalize(source2); self->ideal_yaw = vectoyaw(source2); return true; } } } return false; } //============================================================================ /* ============= ai_eat Monster will eat until harm is done to him or the player moves within range ============== */ void ai_eat (edict_t *self, float dist) { self->enemy = NULL; FindTarget (self); } /* ============= 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) { MG_WalkMove (self, self->s.angles[YAW], dist); } /* ============= ai_stand Used for standing around and looking for players Distance is for slight position adjustments needed by the animations ============== */ void ai_stand (edict_t *self, float dist) { vec3_t v; if (dist) M_walkmove (self, self->s.angles[YAW], dist); if(self->enemy) return; if (self->monsterinfo.aiflags & AI_STAND_GROUND) { if (self->enemy) { VectorSubtract (self->enemy->s.origin, self->s.origin, v); self->ideal_yaw = vectoyaw(v); if (self->s.angles[YAW] != self->ideal_yaw && self->monsterinfo.aiflags & AI_TEMP_STAND_GROUND) { self->monsterinfo.aiflags &= ~(AI_STAND_GROUND | AI_TEMP_STAND_GROUND); QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL); } M_ChangeYaw (self); ai_checkattack (self, 0); } else FindTarget (self); return; } if (FindTarget (self)) return; //FIXME: walking a beat monsters, but that may not be working, need to test! //check for target also? if (self->monsterinfo.pausetime == -1) { self->spawnflags |= MSF_WANDER; self->ai_mood = AI_MOOD_WANDER; QPostMessage(self, MSG_CHECK_MOOD, PRI_DIRECTIVE, "i", AI_MOOD_WANDER); return; } else if (level.time > self->monsterinfo.pausetime) { self->ai_mood = AI_MOOD_WALK; QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL); return; } if (!(self->spawnflags & MSF_AMBUSH) && (self->monsterinfo.idle) && (level.time > self->monsterinfo.idle_time)) { if (self->monsterinfo.idle_time) { self->monsterinfo.idle (self); self->monsterinfo.idle_time = level.time + flrand(15.0F, 30.0F); } else { self->monsterinfo.idle_time = level.time + flrand(0.0F, 15.0F); } } } /* ============= ai_walk The monster is walking it's beat ============= */ qboolean MG_ReachedBuoy (edict_t *self, vec3_t pspot); void ai_walk (edict_t *self, float dist) { if (FindTarget (self)) { QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL); return; } if(self->monsterinfo.searchType == SEARCH_BUOY) { if(!(self->monsterinfo.aiflags&AI_STRAIGHT_TO_ENEMY)) { MG_BuoyNavigate(self);//only called if already wandering MG_Pathfind(self, false); } } else if(!self->enemy && !self->pathtarget) { if(self->movetarget) { if(self->movetarget->classname && strcmp(self->movetarget->classname, "path_corner")) { if(MG_ReachedBuoy(self, self->movetarget->s.origin)) { self->movetarget = NULL; QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); } } } else if(!irand(0, 30) || (Vec3NotZero(self->monsterinfo.nav_goal) && MG_ReachedBuoy(self, self->monsterinfo.nav_goal))) { self->monsterinfo.pausetime = 0; QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); } } MG_MoveToGoal (self, dist); } /* ============= ai_charge Turns towards enemy 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) { if(MGAI_DEBUG) gi.dprintf("ERROR: AI_CHARGE at a NULL enemy!\n"); MG_FaceGoal(self, true);//get ideal yaw and turn return;//send stand MSG? } VectorSubtract (self->enemy->s.origin, self->s.origin, v); self->ideal_yaw = vectoyaw(v); MG_ChangeYaw (self); if(self->spawnflags&MSF_FIXED) return; if (dist) MG_WalkMove (self, self->s.angles[YAW], dist); } /* ============= ai_moveright simple sliding right (or left if dist <0) ============== */ void ai_moveright (edict_t *self, float dist) { vec3_t right; float movedir; if(self->spawnflags&MSF_FIXED) return; if (!dist) return; AngleVectors (self->s.angles, NULL, right, NULL); movedir = vectoyaw(right); MG_WalkMove (self, movedir, dist); } /* ============= ai_goal_charge Turns towards target and advances Use this call with a distnace of 0 to replace ai_face ============== */ void ai_goal_charge (edict_t *self, float dist) { MG_FaceGoal(self, true); if(self->spawnflags&MSF_FIXED) return; if (dist) MG_WalkMove (self, self->s.angles[YAW], dist); } /* ============= ai_charge2 Turns towards target and advances Use this call with a distnace of 0 to replace ai_face ============== */ void ai_charge2 (edict_t *self, float dist) { vec3_t v; if(!self->enemy) return; VectorSubtract (self->enemy->s.origin, self->s.origin, v); self->ideal_yaw = vectoyaw(v); MG_ChangeYaw (self); if(self->spawnflags&MSF_FIXED) return; if (dist) MG_WalkMove (self, self->s.angles[YAW], dist); } /* ============= 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) { if(!(self->spawnflags&MSF_FIXED)) if (dist) M_walkmove (self, self->s.angles[YAW], dist); if (FindTarget (self)) return; M_ChangeYaw (self); } /* .enemy Will be world if not currently angry at anyone. .movetarget The next path spot to walk toward. If .enemy, ignore .movetarget. When an enemy is killed, the monster will try to return to it's path. .hunt_time Set to time + something when the player is in sight, but movement straight for him is blocked. This causes the monster to use wall following code for movement direction instead of sighting on the player. .ideal_yaw A yaw angle of the intended direction, which will be turned towards at up to 45 deg / state. If the enemy is in view and hunt_time is not active, this will be the exact line towards the enemy. .pausetime A monster will leave it's stand state and head towards it's .movetarget when time > .pausetime. walkmove(angle, speed) primitive is all or nothing */ int categorize_range (edict_t *self, edict_t *other, float len) { float dist; if (self->monsterinfo.aiflags & AI_EATING) // Eating { if (len < MELEE_DISTANCE) // Melee distance { if ( len < (self->maxs[0] + other->maxs[0] + 15)) return RANGE_MELEE; } if (len < 175) // Attacking distance return RANGE_NEAR; if (len < 350) // Hissing distance return RANGE_MID; return RANGE_FAR; } else // Not eating { if (len < MELEE_DISTANCE) { if ( len < (self->maxs[0] + other->maxs[0] + 25)) return RANGE_MELEE; } if (len < 500) return RANGE_NEAR; if(self->wakeup_distance) dist = self->wakeup_distance; else dist = MAX_SIGHT_PLAYER_DIST; if (len < dist) return RANGE_MID; return RANGE_FAR; } } /* ============= 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); return categorize_range(self, other, len); } /* ============= 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; if (!other) return false; if (!self) return false; VectorCopy (self->s.origin, spot1); spot1[2] += self->viewheight; if(self->classID == CID_TBEAST) { vec3_t forward; AngleVectors(self->s.angles, forward, NULL, NULL); VectorMA(spot1, self->maxs[0], forward, spot1); } VectorCopy (other->s.origin, spot2); spot2[2] += other->viewheight; gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, MASK_OPAQUE,&trace); if (trace.fraction == 1.0) return true; return false; } /* ============= clear_visible returns 1 if the entity is visible, but not through transparencies ============= */ qboolean clear_visible (edict_t *self, edict_t *other) { vec3_t spot1; vec3_t spot2; trace_t trace; if (!other) return false; if (!self) return false; VectorCopy (self->s.origin, spot1); spot1[2] += self->viewheight; VectorCopy (other->s.origin, spot2); spot2[2] += other->viewheight; gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, MASK_SOLID,&trace); if (trace.fraction == 1.0) return true; return false; } /* ============= infront returns 1 if the entity is in front (in sight) of self ============= */ qboolean infront (edict_t *self, edict_t *other) { vec3_t vec; float dot; vec3_t forward, check_angles; if(Vec3NotZero(self->v_angle_ofs)) VectorAdd(self->v_angle_ofs,self->s.angles,check_angles); else VectorCopy(self->s.angles,check_angles); AngleVectors (check_angles, forward, NULL, NULL); VectorSubtract (other->s.origin, self->s.origin, vec); VectorNormalize (vec); dot = DotProduct (vec, forward); if (dot > 0.3) return true; return false; } //============================================================================ /* Alerted Checks and see if an alert entity is capable of waking up a monster */ void alert_timed_out(alertent_t *self); qboolean Alerted (edict_t *self) {//This alert entity wakes up monsters, let's see what's up... edict_t *enemy = NULL; alertent_t *alerter; vec3_t e_spot, viewspot, dir; qboolean saw_it = false; float dist; if(self->monsterinfo.aiflags&AI_NO_ALERT) return false; //start the search from the most recent alert to the oldest alerter = level.last_alert;//OOPS, SKIPS LAST alertloop: if(self->enemy) goto loopagain; //alerter is gone if(!alerter) goto loopagain; if(!alerter->inuse)//loading a saved game invalidates all alerts goto loopagain; if(alerter->lifetime < level.time) {//alert timed out, remove from list alert_timed_out(alerter); goto loopagain; } if(self->last_alert)//don't be woken up by the same alert twice { if(self->last_alert->inuse) { if(alerter == self->last_alert && self->last_alert->inuse) goto loopagain; } else self->last_alert = NULL; } //alerter's enemy is gone if(!alerter->enemy) goto loopagain; if(alerter->enemy->client) {//no alerts for notarget players if(alerter->enemy->flags & FL_NOTARGET) goto loopagain; } //NEVER alert ambush monsters? //if(!(self->spawnflags & MSF_AMBUSH)) // goto loopagain; if(!(self->svflags&SVF_MONSTER)) goto loopagain; if(self->health<=0) goto loopagain; //eating or in a cinematic or not awake, leave them alone if(!ok_to_wake(self, false, true)) goto loopagain; //should we keep track of owner in case they move to move the alert with them? Only for monsters //if(alerter->owner) // VectorCopy(alerter->owner->s.origin, alerter->s.origin); VectorSubtract(self->s.origin, alerter->origin, dir); dist = VectorLength(dir); //if monster's wakeup_distance is shorter than dist to alerter, leave it alone if(dist > self->wakeup_distance) goto loopagain; //closer means better chance to alert //problem - different alerts might be more likely to be heard/seen... if(dist > flrand(100, self->wakeup_distance))//if within 100 always wake up? goto loopagain; //if not a player, a player's missile or a monster, goto loopagain? //get center of alert enemy enemy = alerter->enemy; VectorAdd(enemy->s.origin, enemy->mins, e_spot); VectorMA(e_spot, 0.5, enemy->size, e_spot); //get my view spot VectorCopy(self->s.origin, viewspot); viewspot[2] += self->viewheight; //if being alerted by a monster and not waiting to ambush if(alerter->alert_svflags&SVF_MONSTER && !(self->spawnflags&MSF_AMBUSH)) {//can "see" the owner of the alert even around a corner saw_it = gi.inPVS(e_spot, viewspot); } else {//alerter not a monster, a projectile or player //can I see (even through translucent brushes) the owner of the alert? //if so, ok even if I'm an ambush monster saw_it = visible_pos(self, e_spot); if(!saw_it&&!(self->spawnflags&MSF_AMBUSH)) {//no line of sight and not an ambush monster if(gi.inPVS(viewspot, alerter->origin)) {//25% chance will see impact(alerter) and detect alert owner anyway if(!irand(0,3)) saw_it = true;//go ahead and go for it } } } if(!saw_it) goto loopagain; if(!self->monsterinfo.alert) goto loopagain; self->last_alert = alerter; return self->monsterinfo.alert(self, alerter, enemy); loopagain: if(alerter) { alerter = alerter->prev_alert; if(alerter) goto alertloop; } return false; } /* =========== HuntTarget - a target has been found, it is visible, so do we attack it, watch it, or stand there? ============ */ void HuntTarget (edict_t *self) { vec3_t vec; int r; self->goalentity = self->enemy; if (self->monsterinfo.aiflags & AI_STAND_GROUND) { QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); } else { r = range(self,self->enemy); if ((self->monsterinfo.aiflags & AI_EATING) && (r == RANGE_MID)) { QPostMessage(self, MSG_WATCH, PRI_DIRECTIVE, NULL); } else { QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL); } } VectorSubtract (self->enemy->s.origin, self->s.origin, vec); self->ideal_yaw = vectoyaw(vec); // wait a while before first attack if (!(self->monsterinfo.aiflags & AI_STAND_GROUND)) AttackFinished (self, 1); } /* =========== FoundTarget - a target has been found, let other monsters know this. Then hunt it ============ */ void FoundTarget (edict_t *self, qboolean setsightent) { char *o_target; // let other monsters see this monster for a while if (!self->enemy) return; if(self->classID == CID_OGLE) self->spawnflags = 0; self->monsterinfo.awake = true; self->spawnflags &= ~MSF_AMBUSH; self->targetname = NULL; self->monsterinfo.pausetime = -1;//was 0; if(self->wakeup_target) { o_target = self->target; self->target = self->wakeup_target; G_UseTargets(self, self->enemy); self->target = o_target; self->wakeup_target = NULL; } if (self->enemy->client && setsightent) { level.sight_entity = self; level.sight_entity_framenum = level.framenum; level.sight_entity->light_level = 128; } self->show_hostile = level.time + 1; // wake up other monsters VectorCopy(self->enemy->s.origin, self->monsterinfo.last_sighting); if (!self->combattarget ) // Not going for a combat point? { // dont want to do this if we are a fish if (self->classID != CID_FISH) HuntTarget (self); if (!self->oldenemy) { if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) ) QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_SOUND_TARGET, self->enemy); else QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_VISIBLE_TARGET, self->enemy); } self->spawnflags &= ~MSF_AMBUSH; return; } self->goalentity = self->movetarget = G_PickTarget(self->combattarget); if (!self->movetarget) { self->goalentity = self->movetarget = self->enemy; // dont want to do this if we are a fish if (self->classID != CID_FISH) HuntTarget (self); if (!self->oldenemy) { if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) ) QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_SOUND_TARGET, self->enemy); else QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_VISIBLE_TARGET, self->enemy); } // gi.dprintf("%s at %s, combattarget %s not found\n", self->classname, vtos(self->s.origin), self->combattarget); self->spawnflags &= ~MSF_AMBUSH; return; } // clear out our combattarget, these are a one shot deal self->combattarget = NULL; self->monsterinfo.aiflags |= AI_COMBAT_POINT; // clear the targetname, that point is ours! self->movetarget->targetname = NULL; // run for it , assuming we aren't a fish if (self->classID != CID_FISH) QPostMessage(self, MSG_RUN, PRI_DIRECTIVE, NULL); //Make a sight sound if (!self->oldenemy) { if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) ) QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_SOUND_TARGET, self->enemy); else QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "be", SIGHT_VISIBLE_TARGET, self->enemy); } self->spawnflags &= ~MSF_AMBUSH; } /* qboolean ok_to_wake (edict_t *monster, qboolean gorgon_roar) Can this monster be woken up by something other than direct line of sight to player */ qboolean ok_to_wake (edict_t *monster, qboolean gorgon_roar, qboolean ignore_ambush) { if(gorgon_roar) { if(monster->monsterinfo.c_mode) return false; } else if(monster->monsterinfo.aiflags & AI_EATING ||//eating or perching monster->targetname ||//a monster that's supposed to be triggered - problem, one a monster is used and woken up, won't respond to alerts like others...? monster->monsterinfo.c_mode ||//cinematic monster->spawnflags & MSF_ASLEEP ||//shouldn't happen, but just in case (monster->spawnflags & MSF_AMBUSH && !ignore_ambush)) return false; return true; } qboolean PlayerCreeping(playerinfo_t *playerinfo) { if(playerinfo->upperseq == ASEQ_CREEPF || playerinfo->upperseq == ASEQ_STAND || playerinfo->upperseq == ASEQ_CREEPB || playerinfo->upperseq == ASEQ_CREEPB_END || playerinfo->upperseq == ASEQ_CROUCH_GO || playerinfo->upperseq == ASEQ_CROUCH || playerinfo->upperseq == ASEQ_CROUCH_END || playerinfo->upperseq == ASEQ_CROUCH_WALK_F || playerinfo->upperseq == ASEQ_CROUCH_WALK_B || playerinfo->upperseq == ASEQ_CROUCH_WALK_L || playerinfo->upperseq == ASEQ_CROUCH_WALK_R || playerinfo->lowerseq == ASEQ_CREEPF || playerinfo->lowerseq == ASEQ_STAND || playerinfo->lowerseq == ASEQ_CREEPB || playerinfo->lowerseq == ASEQ_CREEPB_END || playerinfo->lowerseq == ASEQ_CROUCH_GO || playerinfo->lowerseq == ASEQ_CROUCH || playerinfo->lowerseq == ASEQ_CROUCH_END|| playerinfo->lowerseq == ASEQ_CROUCH_WALK_F || playerinfo->lowerseq == ASEQ_CROUCH_WALK_B || playerinfo->lowerseq == ASEQ_CROUCH_WALK_L || playerinfo->lowerseq == ASEQ_CROUCH_WALK_R) return (true); return (false); } /* =========== FindTarget Self is currently not attacking anything, so try to find a target Returns TRUE if an enemy was sighted When a player fires a missile or does other things to make noise, the point of impact becomes an alertent so that monsters that see the impact will respond as if they had seen the player. Since FindTarget is not called every frame for monsters (average about once every 3 frames per monster), this does two potential checks. First it checks against the current sight_client which cycles through the players. If that check fails, it will check for all the secondary alerts and enemies. if it can't find any, it will check for another player if it can find one other than the first one it checked. ============ */ qboolean ogle_findtarget (edict_t *self); qboolean FindTarget (edict_t *self) { edict_t *client, *firstclient; qboolean heardit = false; int r; edict_t *ent; int flag; qboolean clientonly = true; qboolean e_infront = false; vec3_t v; float dist; //FIXME: wakeup_distance -1 never look? if(self->classID == CID_OGLE) return ogle_findtarget(self); if (self->monsterinfo.aiflags & AI_GOOD_GUY) { if (self->goalentity) { if (strcmp(self->goalentity->classname, "target_actor") == 0) return false; } //FIXME look for monsters? return false; } // if we're going to a combat point, just proceed if (self->monsterinfo.aiflags & AI_COMBAT_POINT) return false; if(self->ai_mood_flags&AI_MOOD_FLAG_IGNORE_ENEMY) {//being forced to use buoys, and ignore enemy until get to forced_buoy return false; } // if the first spawnflag bit is set, the monster will only wake up on // really seeing the player, not another monster getting angry or hearing // something // revised behavior so they will wake up if they "see" a player make a noise // but not weapon impact/explosion noises startcheck: flag = 1; if(clientonly) {//look oly at the level.sight_client firstclient = client = level.sight_client; } else { if(ANARCHY) {//crazy monsters mode int checkcnt = 0; client = self; while((!client || !client->inuse || !(client->svflags & SVF_MONSTER)||client->health<=0||client == self) && checkcnt < globals.num_edicts) { client = &g_edicts[irand(0, globals.num_edicts)]; checkcnt++; } } else { if(level.sight_entity == self) { level.sight_entity = NULL; return false; } if (level.sight_entity && (level.sight_entity_framenum >= (level.framenum - 1)) && !(self->spawnflags & MSF_AMBUSH) ) {//go after the enemy another monster saw saw, but only if not in ambush client = level.sight_entity; if (client->enemy == self->enemy) { return false; } } else if (Alerted(self)) {//picked up an enemy from an alert return true; } else { client = NULL; // Looking for secondary enemies if ((self->monsterinfo.otherenemyname) && (self->monsterinfo.chase_finished < level.time)) { ent = NULL; while((ent=findradius(ent,self->s.origin,175)) != NULL) { if (!strcmp(ent->classname,self->monsterinfo.otherenemyname)&&ent!=self) { flag = 0; client = ent; break; } } } // Look at the sight client if (!client) {//found no non-clients, cycle to next client and check it for second check AI_SetSightClient(); if(firstclient == level.sight_client) return false;//same as first check, and that failed if we're here, so return. client = level.sight_client; } } } } if (!client) goto nextcheck; // no clients to get mad at // if the entity went away, forget it if (!client->inuse) goto nextcheck; if (client == self) goto nextcheck; //???? if (client == self->enemy) return true; // JDC false; if (self->monsterinfo.otherenemyname) { if (!strcmp(client->classname,self->monsterinfo.otherenemyname)) client->light_level = 128; // Let it be seen } // if we are a fish - is the target in the water - have to be at least waist deep? if (self->classID == CID_FISH && client->waterlevel < 2) goto nextcheck; //???? if (client->client) { if (client->flags & FL_NOTARGET) goto nextcheck; } else if (client->svflags & SVF_MONSTER) { if(flag) {//not a secondary enemy if(!ANARCHY) { if (ok_to_wake(self, false, true)) {//eating or in a cinematic or not awake or targeted, leave them alone goto nextcheck; } } if (!client->enemy) { if(!ANARCHY) goto nextcheck; } else { if (client->enemy->health<0 && !ANARCHY) goto nextcheck; if (client->enemy->flags & FL_NOTARGET) goto nextcheck; } if(!visible(self, client)) goto nextcheck; if(!ANARCHY) self->enemy = client->enemy; else self->enemy = client; if(client->ai_mood == AI_FLEE) FoundTarget(self, false);//let them stay the sight entity else FoundTarget(self, true);//make me the sight entity /*if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET)) QPostMessage(self, MSG_VOICE_SIGHT, PRI_DIRECTIVE, "e", self->enemy);*/ //self->monsterinfo.sight (self, self->enemy); return true; } } else if (heardit) { if (client->owner->flags & FL_NOTARGET) goto nextcheck; } else goto nextcheck; if (!heardit) { if(self->classID == CID_ASSASSIN) e_infront = true; else e_infront = infront(self, client); if(!e_infront && client->client) { if(PlayerCreeping(&client->client->playerinfo)) goto nextcheck; } VectorSubtract(client->s.origin, self->s.origin, v); dist = VectorLength(v); if(dist > self->wakeup_distance) goto nextcheck; r = categorize_range (self, client, dist); if (r == RANGE_FAR) goto nextcheck; if ((self->monsterinfo.aiflags & AI_EATING) && (r > RANGE_MID)) { self->enemy = client; goto nextcheck; } // this is where we would check invisibility // is client in an spot too dark to be seen? if (client->light_level <= 5) goto nextcheck; if(self->svflags&SVF_MONSTER && client->client) { if(skill->value < 2.0 && !(self->monsterinfo.aiflags & AI_NIGHTVISION)) { if(client->light_level < flrand(6, 77)) { goto nextcheck; } } } if (r == RANGE_NEAR) { if (client->show_hostile < level.time && !e_infront) { goto nextcheck; } } else if (r == RANGE_MID) { if (!e_infront) { goto nextcheck; } } //sfs--this check is much less trivial than infront: prolly wasn't a noticeable deal, // since RANGE_FAR was first rejection check, but it's still better to try the // dotproduct before the traceline if (!visible (self, client)) { goto nextcheck; } self->enemy = client; flag=1; if (self->monsterinfo.otherenemyname) { if (strcmp(self->enemy->classname, self->monsterinfo.otherenemyname) == 0) // This is a secondary enemy { self->monsterinfo.chase_finished = level.time + 15; flag=0; } } if (flag) // This is not a secondary enemy { self->monsterinfo.aiflags &= ~AI_SOUND_TARGET; if (!self->enemy->client) { self->enemy = self->enemy->enemy; if (!self->enemy->client) { self->enemy = NULL; goto nextcheck; } } } } else // heardit { goto nextcheck; } // // got one // FoundTarget (self, true); return true; nextcheck: if(clientonly) { clientonly = false; goto startcheck; } else 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; } //============================================================================= qboolean M_CheckAttack (edict_t *self) { vec3_t spot1, spot2; float chance; trace_t tr; if (self->enemy->health > 0) { // see if any entities are in the way of the shot VectorCopy (self->s.origin, spot1); spot1[2] += self->viewheight; VectorCopy (self->enemy->s.origin, spot2); spot2[2] += self->enemy->viewheight; gi.trace (spot1, vec3_origin, vec3_origin, spot2, self, CONTENTS_SOLID|CONTENTS_MONSTER|CONTENTS_SLIME|CONTENTS_LAVA,&tr); // do we have a clear shot? if (tr.ent != self->enemy) return false; } // melee attack if (enemy_range == RANGE_MELEE) { // don't always melee in easy mode if (skill->value == 0 && irand(0, 3) ) return false; if (classStatics[self->classID].msgReceivers[MSG_MELEE]) { self->monsterinfo.attack_state = AS_MELEE; } else { self->monsterinfo.attack_state = AS_MISSILE; } return true; } // missile attack if (!classStatics[self->classID].msgReceivers[MSG_MISSILE]) return false; if (level.time < self->monsterinfo.attack_finished) return false; if (enemy_range == RANGE_FAR) return false; if (self->monsterinfo.aiflags & AI_STAND_GROUND) { chance = 0.4; } else if (enemy_range == RANGE_MELEE) { chance = 0.2; } else if (enemy_range == RANGE_NEAR) { chance = 0.1; } else if (enemy_range == RANGE_MID) { chance = 0.02; } else { return false; } if (skill->value == 0) chance *= 0.5; else if (skill->value >= 2) chance *= 2; if (flrand(0.0, 1.0) < chance) { self->monsterinfo.attack_state = AS_MISSILE; self->monsterinfo.attack_finished = level.time + flrand(0.0, 2.0); return true; } if (self->flags & FL_FLY) { if (!irand(0, 2)) self->monsterinfo.attack_state = AS_SLIDING; else self->monsterinfo.attack_state = AS_STRAIGHT; } return false; } /* ============= ai_run_melee Turn and close until within an angle to launch a melee attack ============= */ void ai_run_melee(edict_t *self) { self->ideal_yaw = enemy_yaw; M_ChangeYaw (self); if (FacingIdeal(self)) { QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL); self->monsterinfo.attack_state = AS_STRAIGHT; } } /* ============= ai_run_missile Turn in place until within an angle to launch a missile attack ============= */ void ai_run_missile(edict_t *self) { self->ideal_yaw = enemy_yaw; M_ChangeYaw (self); if (FacingIdeal(self)) { QPostMessage(self, MSG_MISSILE, PRI_DIRECTIVE, NULL); self->monsterinfo.attack_state = AS_STRAIGHT; } }; /* ============= ai_run_slide Strafe sideways, but stay at aproximately the same range ============= */ void ai_run_slide(edict_t *self, float distance) { float ofs; self->ideal_yaw = enemy_yaw; M_ChangeYaw (self); if (self->monsterinfo.lefty) ofs = 90; else ofs = -90; if (M_walkmove (self, self->ideal_yaw + ofs, distance)) return; self->monsterinfo.lefty = 1 - self->monsterinfo.lefty; M_walkmove (self, self->ideal_yaw - ofs, distance); } /* ============= ai_checkattack Decides if we're going to attack or do something else used by ai_run, and ai_stand ============= */ qboolean ai_checkattack (edict_t *self, float dist) { vec3_t temp; qboolean hesDeadJim; if ((self->monsterinfo.aiflags & AI_FLEE)||(self->monsterinfo.aiflags & AI_COWARD)) // He's running away, not attacking { return false; } enemy_vis = false; // see if the enemy is dead hesDeadJim = false; if ((!self->enemy) || (!self->enemy->inuse)) { hesDeadJim = true; } else { if (self->monsterinfo.aiflags & AI_BRUTAL) { if (self->enemy->health <= -80) hesDeadJim = true; } else { if (self->enemy->health <= 0) hesDeadJim = true; } } if (hesDeadJim) { self->enemy = NULL; // FIXME: look all around for other targets if (self->oldenemy && self->oldenemy->health > 0) { self->enemy = self->oldenemy; self->oldenemy = NULL; HuntTarget (self); } else { if (self->movetarget) { self->goalentity = self->movetarget; QPostMessage(self, MSG_WALK, PRI_DIRECTIVE, NULL); } else { // we need the pausetime otherwise the stand code // will just revert to walking with no target and // the monsters will wonder around aimlessly trying // to hunt the world entity self->monsterinfo.pausetime = level.time + 100000000; QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); } return true; } } self->show_hostile = level.time + 1; // wake up other monsters // check knowledge of enemy enemy_vis = clear_visible(self, self->enemy); if (enemy_vis) { self->monsterinfo.search_time = level.time + 5; VectorCopy (self->enemy->s.origin, self->monsterinfo.last_sighting); } // look for other coop players here // if (coop && self->monsterinfo.search_time < level.time) // { // if (FindTarget (self)) // return true; // } enemy_infront = infront(self, self->enemy); enemy_range = range(self, self->enemy); VectorSubtract (self->enemy->s.origin, self->s.origin, temp); enemy_yaw = vectoyaw(temp); // JDC self->ideal_yaw = enemy_yaw; if (self->monsterinfo.attack_state == AS_MISSILE) { ai_run_missile (self); return true; } if (self->monsterinfo.attack_state == AS_MELEE) { ai_run_melee (self); return true; } // if enemy is not currently visible, we will never attack if (!enemy_vis) return false; return self->monsterinfo.checkattack (self); } qboolean ai_inpack(edict_t *self) { edict_t *ent; ent = NULL; while ((ent = findradius(ent, self->s.origin, 1024)) != NULL) { if (ent == self) continue; if (stricmp(ent->classname, self->classname)) continue; return true; } return false; } /* ============= ai_runaway The monster has an enemy it is trying to flee ============= */ void ai_runaway (edict_t *self, float dist) { vec3_t move; vec3_t vec, source, vf; qboolean goleft = false; vec3_t mins, maxs; trace_t trace; vec3_t na, nvr; float yaw; int ret; if (!self->enemy) return; // gi.dprintf("%s running away from %s!\n", self->classname, self->enemy->classname); //Setup the trace VectorCopy(self->mins, mins); VectorCopy(self->maxs, maxs); VectorCopy(self->s.origin, source); AngleVectors(self->s.angles, vf, NULL, NULL); VectorMA(source, dist*2, vf, source); //Account for STEPSIZE mins[2] += 18; gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SHOT,&trace); //We hit something if (trace.fraction < 1) { vectoangles(trace.plane.normal,na); AngleVectors(na,NULL,nvr,NULL); if(DotProduct(nvr,vf)>0) self->ideal_yaw=vectoyaw(nvr); else { VectorScale(nvr, -1, nvr); self->ideal_yaw=vectoyaw(nvr); } VectorCopy(self->mins, mins); VectorCopy(self->maxs, maxs); VectorCopy(self->s.origin, source); AngleVectors(self->s.angles, vf, NULL, NULL); VectorMA(source, dist*4, nvr, source); //Account for STEPSIZE mins[2] += 18; gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SHOT,&trace); if (trace.fraction < 1) { //gi.dprintf("Failed next move!\n"); VectorScale(nvr, -1, nvr); self->ideal_yaw=vectoyaw(nvr); } M_ChangeYaw (self); if (abs(self->s.angles[YAW] - self->ideal_yaw) < self->yaw_speed) { yaw = self->s.angles[YAW]; yaw = yaw*M_PI*2 / 360; move[0] = cos(yaw)*dist * 4; move[1] = sin(yaw)*dist * 4; move[2] = 0; ret = ai_trystep(self, move); if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time)) { self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0); SV_NewChaseDir(self, self->enemy, dist); } else if (!M_walkmove (self, self->s.angles[YAW], dist)) { self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0); SV_NewChaseDir(self, self->enemy, dist); } } return; } VectorSubtract(self->enemy->s.origin, self->s.origin, vec); if (self->monsterinfo.idle_time < level.time) { self->ideal_yaw = vectoyaw(vec); self->ideal_yaw += 180; M_ChangeYaw (self); } if (dist) //Going somewhere? { if(self->classID == CID_SSITHRA) ssithraCheckJump(self); if (!M_walkmove (self, self->s.angles[YAW], dist)) { if (self->monsterinfo.searchType == SEARCH_BUOY) return; yaw = self->s.angles[YAW]; yaw = yaw*M_PI*2 / 360; move[0] = cos(yaw)*dist; move[1] = sin(yaw)*dist; move[2] = 0; ret = ai_trystep(self, move); if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time)) { self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0); SV_NewChaseDir(self, self->enemy, dist); } else { M_ChangeYaw(self); if (!M_walkmove (self, self->s.angles[YAW], dist/2)&&(self->classID != CID_SSITHRA)) { if (ai_hopdown(self, self->enemy->s.origin, 1024)) { if (self->monsterinfo.jump_time < level.time) { //FIXME: Re-implement this! //gkrokon_hopdown(self); self->monsterinfo.jump_time = level.time + 1; } else return; } } } } } } float ai_face_goal (edict_t *self) { vec3_t vec; if (self->monsterinfo.searchType == SEARCH_BUOY) VectorSubtract(self->monsterinfo.nav_goal, self->s.origin, vec); else if(self->goalentity) VectorSubtract(self->goalentity->s.origin, self->s.origin, vec); else if(self->enemy) VectorSubtract(self->enemy->s.origin, self->s.origin, vec); else return false; if (self->monsterinfo.idle_time < level.time) { self->ideal_yaw = vectoyaw(vec); return M_ChangeYaw(self); } else return false; } void old_ai_run (edict_t *self, float dist) {//FIXME: only ssithra in water use this, let;s junk it! vec3_t move; vec3_t vec, source, vf; qboolean goleft = false; vec3_t mins, maxs; trace_t trace; vec3_t na, nvr;//, v; float yaw, turnamt, distloss;//,bbox int ret, oldyaw; if (!self->enemy) return; if(self->classID == CID_GORGON) { if(visible(self, self->enemy)) { if(Vec3IsZero(self->enemy->velocity)) { VectorCopy(self->enemy->s.origin, self->pos1); } else VectorClear(self->pos1); } else VectorClear(self->pos1); } if (self->monsterinfo.attack_state == AS_SLIDING) { ai_run_slide (self, dist); return; } // He's done fleeing, time to stand and see what's happening if ((self->monsterinfo.aiflags & AI_FLEE) && (self->monsterinfo.flee_finished < level.time)) { self->monsterinfo.aiflags &= ~AI_FLEE; if(irand(0,10<2)) { self->enemy = NULL; QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); } return; } if (self->monsterinfo.aiflags & AI_FLEE||self->monsterinfo.aiflags & AI_COWARD) { VectorSubtract (self->enemy->s.origin, self->s.origin, vec); self->ideal_yaw = vectoyaw(vec); self->ideal_yaw += 180; ai_runaway(self, dist); return; } if (self->monsterinfo.aiflags & AI_STAND_GROUND) { turnamt = Q_fabs(ai_face_goal(self)); return; } if (self->monsterinfo.aiflags & AI_SOUND_TARGET) { self->monsterinfo.aiflags &= ~AI_SOUND_TARGET; if (!FindTarget (self)) { QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); return; } } if (!self->enemy) { QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); return; } // ai_maintain_waypoints(self, 0, 0, 0); //Setup the trace VectorCopy(self->mins, mins); VectorCopy(self->maxs, maxs); VectorCopy(self->s.origin, source); AngleVectors(self->s.angles, vf, NULL, NULL); /* if (self->maxs[0] > self->maxs[1]) // Add in monster's bounding box bbox = self->maxs[0]; else bbox = self->maxs[1];*/ VectorMA(source, (dist*2)/*+bbox*/, vf, source); //Account for STEPSIZE mins[2] += 18; gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SHOT,&trace); //We hit something if (trace.fraction < 1) { if (trace.ent == self->enemy) { QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL); return; } else if(self->monsterinfo.otherenemyname) { if(!stricmp(trace.ent->classname, self->monsterinfo.otherenemyname))//&&irand(0,10)<3) { QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL); return; } } if(trace.ent->takedamage && self->monsterinfo.aiflags & AI_SHOVE && trace.fraction <= 0.5) {//hurt them some too? if(self->classID == CID_GORGON && self->s.scale >1.5 && trace.ent->classID!=self->classID && trace.ent->health <= 200&& infront(self, self->enemy))//chomp! { // gi.dprintf("Chomp!\n"); self->oldenemy = self->enemy; self->enemy = trace.ent; QPostMessage(self, MSG_MELEE, PRI_DIRECTIVE, NULL); } else { // gi.dprintf("Shove!\n"); VectorSubtract(trace.ent->s.origin,self->s.origin,vec); if(vec[2]<30) vec[2] = 30; VectorNormalize(vec); VectorMA(trace.ent->velocity,200,vec,trace.ent->velocity); trace.ent->groundentity = NULL; } } vectoangles(trace.plane.normal,na); AngleVectors(na,NULL,nvr,NULL); if(DotProduct(nvr,vf)>0) { self->ideal_yaw=vectoyaw(nvr); } else { VectorScale(nvr, -1, nvr); self->ideal_yaw=vectoyaw(nvr); } VectorCopy(self->mins, mins); VectorCopy(self->maxs, maxs); VectorCopy(self->s.origin, source); AngleVectors(self->s.angles, vf, NULL, NULL); VectorMA(source, dist, nvr, source);//was 4? //Account for STEPSIZE mins[2] += 18; gi.trace (self->s.origin, mins, self->maxs, source, self, MASK_SOLID,&trace);//was MASK_SHOT if (trace.fraction < 1||trace.allsolid||trace.startsolid) { VectorScale(nvr, -1, nvr); self->ideal_yaw=vectoyaw(nvr); } oldyaw = self->s.angles[YAW]; turnamt = Q_fabs(M_ChangeYaw(self)); if (abs(anglemod(self->s.angles[YAW] - self->ideal_yaw)) < self->yaw_speed) { yaw = self->s.angles[YAW]; yaw = yaw*M_PI*2 / 360; move[0] = cos(yaw)*dist * 2;//4 move[1] = sin(yaw)*dist * 2;//4 move[2] = 0; ret = ai_trystep(self, move); if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time)) { self->ideal_yaw = -vectoyaw(nvr); turnamt = Q_fabs(M_ChangeYaw(self)); distloss = turnamt/self->yaw_speed * 0.3; dist -= (dist * distloss); if (!M_walkmove (self, self->s.angles[YAW], dist)) { self->monsterinfo.idle_time = level.time + flrand(0.5, 1.2); SV_NewChaseDir(self, self->enemy, dist); } } else { distloss = turnamt/self->yaw_speed * 0.3; dist -= (dist * distloss); M_walkmove (self, self->s.angles[YAW], dist); } //actually tried move return; } } else turnamt = Q_fabs(ai_face_goal(self)); //greater the turn, lesser the dist, full yawspeed turn is 50% dist //FIXME: also make this dist less if turn is high and close to enemy? //(so you don't run around it in a circle)? distloss = turnamt/self->yaw_speed * 0.3; dist -= (dist * distloss); if (dist) //Going somewhere? { if(self->classID == CID_SSITHRA) ssithraCheckJump(self); if (!M_walkmove (self, self->s.angles[YAW], dist)) { yaw = self->s.angles[YAW]; yaw = yaw*M_PI*2 / 360; move[0] = cos(yaw)*dist; move[1] = sin(yaw)*dist; move[2] = 0; ret = ai_trystep(self, move); if ((ret != TRYSTEP_OK) && (self->monsterinfo.idle_time < level.time)) { //Jump down? if ((self->enemy->s.origin[2] < self->s.origin[2])&&(self->classID != CID_SSITHRA)) { if (ai_hopdown(self, self->enemy->s.origin, 1024)) { if (self->monsterinfo.jump_time < level.time) { self->monsterinfo.jump_time = level.time + 1; return; } else return; } else { self->monsterinfo.idle_time = level.time + flrand(1.0, 2.0); SV_NewChaseDir(self, self->enemy, dist); } } else { self->monsterinfo.idle_time = level.time + flrand(0.5, 1.2); SV_NewChaseDir(self, self->enemy, dist); } } else { turnamt = Q_fabs(M_ChangeYaw(self)); if (!M_walkmove (self, self->s.angles[YAW], dist/2)&&(self->classID != CID_SSITHRA)) { if (ai_hopdown(self, self->enemy->s.origin, 1024)) { if (self->monsterinfo.jump_time < level.time) { self->monsterinfo.jump_time = level.time + 1; return; } else return; } } } } } } /* ============= ai_flee The monster has an enemy it is trying to get away from ============= */ void ai_flee (edict_t *self, float dist) { vec3_t vec; if (self->enemy) { VectorSubtract (self->enemy->s.origin, self->s.origin, vec); self->ideal_yaw = vectoyaw(vec); self->ideal_yaw = anglemod(self->ideal_yaw + self->best_move_yaw); M_ChangeYaw(self); if(!M_walkmove(self, self->s.angles[YAW], dist) && EqualAngle(self->s.angles[YAW], self->ideal_yaw, 5)) self->best_move_yaw = flrand(60, 300); else self->best_move_yaw = 180; /* VectorSubtract(self->s.origin, self->enemy->s.origin, vec); self->ideal_yaw = vectoyaw(vec); M_ChangeYaw(self); M_MoveAwayFromGoal (self, dist);*/ } } /* ============================================================= void extrapolateFiredir (edict_t *self,vec3_t p1,float pspeed,edict_t *targ,float accept,vec3_t vec2) MG Estimates where the "targ" will be by the time a projectile travelling at "pspeed" leaving "org" arrives at "targ"'s origin. It then calculates a new spot to shoot at so that the projectile will arrive at such spot at the same time as "targ". Will return '0 0 0' (FALSE) if there is not a clear line of fire to the spot or if the new vector is out of the acceptable range (based on dot product of original vec and the new vec). PROPOSAL: Factor in skill->value? 0 = no leading, 4 = perfect leading, 1-3 innaccuracy levels for leading ============================================================= */ void extrapolateFiredir (edict_t *self,vec3_t p1,float pspeed,edict_t *targ,float accept,vec3_t vec2) { float dist1, dist2, tspeed, dot, eta1, eta2, eta_delta, tdist; qboolean failed = false; vec3_t p2, p3, targ_dir, vec1, tempv; trace_t trace; float offset; if(!targ) { VectorClear(vec2); return; } tdist = vhlen(targ->s.origin, self->s.origin); if(!skill->value) {//poor shot, take forward and screw it up AngleVectors(self->s.angles, tempv, NULL, NULL); VectorMA(p1, pspeed, tempv, p2); if(tdist < 128) offset = 48 * tdist/128; else offset = 48; p2[0] += flrand(-offset, offset); p2[1] += flrand(-offset, offset); p2[2] += flrand(-offset/2, offset * 0.666); VectorSubtract(p2, p1, vec2); VectorNormalize(vec2); return; } offset = 2 - skill->value;//skill >= 2 = perfect aim, skill 1 is very poor if(tdist < 128) offset *= tdist/128; if(offset<0) offset = 0; if(skill->value < 2.0 && !(self->monsterinfo.aiflags & AI_NIGHTVISION)) { if(targ->client) { offset += targ->light_level/32; } } VectorCopy(targ->s.origin, p2); if(offset) { p2[0] += flrand(-offset*12, offset*12); p2[1] += flrand(-offset*12, offset*12); p2[2] += flrand(-offset*8, offset*8); } VectorSubtract(p2, p1, vec1); dist1 = VectorNormalize(vec1); VectorCopy(targ->velocity, targ_dir); tspeed = VectorNormalize(targ_dir); eta1 = dist1/pspeed; //Estimated time of arrival of projectile to p2 VectorMA(p2, tspeed*eta1, targ_dir, p3); VectorSubtract(p3, p1, tempv); dist2 = VectorNormalize(tempv); eta2 = dist2/pspeed; //ETA of projectile to p3 eta_delta = eta2-eta1; //change in ETA's VectorMA(p3, tspeed*eta_delta*flrand(0, 1), targ_dir, p3); //careful, above version does not modify targ_dir gi.trace(p1, vec3_origin, vec3_origin, p3, self, MASK_SOLID,&trace); if(trace.fraction<1.0) failed = true; VectorSubtract(p3, p1, vec2); VectorNormalize(vec2); dot = DotProduct(vec1, vec2); if(dotprev_alert) { if(self->next_alert) self->prev_alert->next_alert = self->next_alert; else {//I'm the last one! level.last_alert = self->prev_alert; self->prev_alert->next_alert = NULL; } } else {//I'm the first one! if(self->next_alert) level.alert_entity = self->next_alert; else level.last_alert = level.alert_entity = NULL; } //Clear out all fields self->next_alert = NULL; self->prev_alert = NULL; self->enemy = NULL; VectorClear(self->origin); self->alert_svflags = 0; self->lifetime = 0; self->inuse = false; ClearLastAlerts(self); level.num_alert_ents--; } /* GetFirstEmptyAlertInList returns the first alert in the level.alertents list that isn;t in use */ alertent_t *GetFirstEmptyAlertInList(void) { int i; //have max number of alerts, remove the first one if(level.num_alert_ents >= MAX_ALERT_ENTS) alert_timed_out(level.alert_entity); for(i = 0; i < MAX_ALERT_ENTS; i++) { if(!level.alertents[i].inuse) { return &level.alertents[i]; } } return NULL; } /* AlertMonsters allots an alertent monsters will check during FindTarget to see if they should be alerted by it self = used for sv_flags info and positioning of the alert entity enemy = entity to make the monsters mad at if they're alerted (float)lifetime = how many seconds the alert exists for (qboolean)ignore_shadows = this alert gives away enemy's position, even if he is in shadows (I use this for staff hits on the floor and any other effect the player makes at his own location(like shooting a weapon), not for projectiles impacting) */ void AlertMonsters (edict_t *self, edict_t *enemy, float lifetime, qboolean ignore_shadows) {//FIXME: if and stick new one at the end alertent_t *alerter = level.alert_entity; alertent_t *last_alert = NULL; if (deathmatch->value) // Don't need this if no monsters... return; if(!lifetime) lifetime = 1.0;//stick around for 1 second //stick into the level's alerter chain if(alerter) {//go down the list and find an empty slot //fixme: just store the entnum? while(alerter->next_alert) { last_alert = alerter; alerter = alerter->next_alert; } level.last_alert = alerter = GetFirstEmptyAlertInList(); } else//we're the first one! level.alert_entity = level.last_alert = alerter = GetFirstEmptyAlertInList(); if(!alerter) { #ifdef _DEVEL gi.dprintf("Error: out of alerts and can't find any empty slots!\n"); #endif return; } //I'm active, don't let my slot be used until I'm freed alerter->inuse = true; //point to the previous alerter, if any alerter->prev_alert = last_alert; //make the previous alerter point to me as the next one if(alerter->prev_alert) alerter->prev_alert->next_alert = alerter; //put me in the "self"'s spot VectorCopy(self->s.origin, alerter->origin); alerter->enemy = enemy; //should we keep track of owner in case they move to move the alert with them? Only for monsters //alerter->owner = self; alerter->alert_svflags = self->svflags; if(ignore_shadows)//whatever happened would give away enemy's position, even in shadows alerter->alert_svflags |= SVF_ALERT_NO_SHADE; //stick around until after this point in time alerter->lifetime = level.time + lifetime; level.num_alert_ents++; } void ai_spin (edict_t *self, float amount) { self->s.angles[YAW] += amount; } qboolean ai_have_enemy (edict_t *self) { qboolean enemy_gone = false; if(!self->enemy) enemy_gone = true; else if(self->enemy->health <= 0) enemy_gone = true; if(!enemy_gone) return true; else { if(self->oldenemy) { if(self->oldenemy->health>0) { self->enemy=self->oldenemy; self->oldenemy = NULL; // gi.dprintf("Going for old enemy\n"); return true; } } } QPostMessage(self, MSG_STAND, PRI_DIRECTIVE, NULL); // gi.dprintf("Lost enemies\n"); return false; } qboolean movable (edict_t *ent) { if( ent->movetype!=PHYSICSTYPE_NONE && ent->movetype!=MOVETYPE_PUSH && ent->movetype!=PHYSICSTYPE_PUSH) return true; return false; }