//========================================== // AI_PickShortRangeGoal // Pick best goal based on importance and range. This function // overrides the long range goal selection for items that // are very close to the bot and are reachable. //========================================== static void AI_PickShortRangeGoal( edict_t *self ) { edict_t *bestGoal = NULL; float bestWeight = 0; nav_ents_t *goalEnt; const gsitem_t *item; bool canPickupItems; int i; if( !self->r.client || G_ISGHOSTING( self ) ) return; if( self->ai->state_combat_timeout > level.time ) { self->ai->shortRangeGoalTimeout = self->ai->state_combat_timeout; return; } if( self->ai->shortRangeGoalTimeout > level.time ) return; canPickupItems = (self->r.client->ps.pmove.stats[PM_STAT_FEATURES] & PMFEAT_ITEMPICK) != 0 ? true : false; self->ai->shortRangeGoalTimeout = level.time + AI_SHORT_RANGE_GOAL_DELAY; self->movetarget = NULL; FOREACH_GOALENT( goalEnt ) { float dist; i = goalEnt->id; if( !goalEnt->ent->r.inuse || goalEnt->ent->r.solid == SOLID_NOT ) continue; if( goalEnt->ent->r.client ) continue; if( self->ai->status.entityWeights[i] <= 0.0f ) continue; item = goalEnt->ent->item; if( canPickupItems && item ) { if( !G_Gametype_CanPickUpItem( item ) || !( item->flags & ITFLAG_PICKABLE ) ) { continue; } } dist = DistanceFast( self->s.origin, goalEnt->ent->s.origin ); if( goalEnt == self->ai->goalEnt ) { if( dist > AI_GOAL_SR_LR_RADIUS ) continue; } else { if( dist > AI_GOAL_SR_RADIUS ) continue; } clamp_low( dist, 0.01f ); if( AI_ShortRangeReachable( self, goalEnt->ent->s.origin ) ) { float weight; bool in_front = G_InFront( self, goalEnt->ent ); // Long range goal gets top priority if( in_front && goalEnt == self->ai->goalEnt ) { bestGoal = goalEnt->ent; break; } // get the one with the best weight weight = self->ai->status.entityWeights[i] / dist * (in_front ? 1.0f : 0.5f); if( weight > bestWeight ) { bestWeight = weight; bestGoal = goalEnt->ent; } } } if( bestGoal ) { self->movetarget = bestGoal; if( nav.debugMode && bot_showsrgoal->integer ) G_PrintChasersf( self, "%i %s: selected a %s for SR goal.\n", level.framenum, self->ai->pers.netname, self->movetarget->classname ); } else { // got nothing else to do so keep scanning self->ai->shortRangeGoalTimeout = level.time + AI_SHORT_RANGE_GOAL_DELAY_IDLE; } }
//========================================== // BOT_DMclass_FireWeapon // Fire if needed //========================================== static bool BOT_DMclass_FireWeapon( edict_t *self, usercmd_t *ucmd ) { #define WFAC_GENERIC_PROJECTILE 300.0 #define WFAC_GENERIC_INSTANT 150.0 float firedelay; vec3_t target; int weapon, i; float wfac; vec3_t fire_origin; trace_t trace; bool continuous_fire = false; firedef_t *firedef = GS_FiredefForPlayerState( &self->r.client->ps, self->r.client->ps.stats[STAT_WEAPON] ); if( !self->enemy ) return false; weapon = self->s.weapon; if( weapon < 0 || weapon >= WEAP_TOTAL ) weapon = 0; if( !firedef ) return false; // Aim to center of the box for( i = 0; i < 3; i++ ) target[i] = self->enemy->s.origin[i] + ( 0.5f * ( self->enemy->r.maxs[i] + self->enemy->r.mins[i] ) ); fire_origin[0] = self->s.origin[0]; fire_origin[1] = self->s.origin[1]; fire_origin[2] = self->s.origin[2] + self->viewheight; if( self->s.weapon == WEAP_LASERGUN || self->s.weapon == WEAP_PLASMAGUN ) continuous_fire = true; if( !continuous_fire && !BOT_DMclass_CheckShot( self, target ) ) return false; // find out our weapon AIM style if( AIWeapons[weapon].aimType == AI_AIMSTYLE_PREDICTION_EXPLOSIVE ) { // in the lowest skill level, don't predict projectiles if( self->ai->pers.skillLevel >= 0.33f ) BOT_DMclass_PredictProjectileShot( self, fire_origin, firedef->speed, target, self->enemy->velocity ); wfac = WFAC_GENERIC_PROJECTILE * 1.3; // aim to the feet when enemy isn't higher if( fire_origin[2] > ( target[2] + ( self->enemy->r.mins[2] * 0.8 ) ) ) { vec3_t checktarget; VectorSet( checktarget, self->enemy->s.origin[0], self->enemy->s.origin[1], self->enemy->s.origin[2] + self->enemy->r.mins[2] + 4 ); G_Trace( &trace, fire_origin, vec3_origin, vec3_origin, checktarget, self, MASK_SHOT ); if( trace.fraction == 1.0f || ( trace.ent > 0 && game.edicts[trace.ent].takedamage ) ) VectorCopy( checktarget, target ); } else if( !AI_IsStep( self->enemy ) ) wfac *= 2.5; // more imprecise for air rockets } else if( AIWeapons[weapon].aimType == AI_AIMSTYLE_PREDICTION ) { if( self->s.weapon == WEAP_PLASMAGUN ) wfac = WFAC_GENERIC_PROJECTILE * 0.5; else wfac = WFAC_GENERIC_PROJECTILE; // in the lowest skill level, don't predict projectiles if( self->ai->pers.skillLevel >= 0.33f ) BOT_DMclass_PredictProjectileShot( self, fire_origin, firedef->speed, target, self->enemy->velocity ); } else if( AIWeapons[weapon].aimType == AI_AIMSTYLE_DROP ) { //jalToDo wfac = WFAC_GENERIC_PROJECTILE; // in the lowest skill level, don't predict projectiles if( self->ai->pers.skillLevel >= 0.33f ) BOT_DMclass_PredictProjectileShot( self, fire_origin, firedef->speed, target, self->enemy->velocity ); } else // AI_AIMSTYLE_INSTANTHIT { if( self->s.weapon == WEAP_ELECTROBOLT ) wfac = WFAC_GENERIC_INSTANT; else if( self->s.weapon == WEAP_LASERGUN ) wfac = WFAC_GENERIC_INSTANT * 1.5; else wfac = WFAC_GENERIC_INSTANT; } wfac = 25 + wfac * ( 1.0f - self->ai->pers.skillLevel ); // look to target VectorSubtract( target, fire_origin, self->ai->move_vector ); if( self->r.client->ps.weaponState == WEAPON_STATE_READY || self->r.client->ps.weaponState == WEAPON_STATE_REFIRE || self->r.client->ps.weaponState == WEAPON_STATE_REFIRESTRONG ) { // in continuous fire weapons don't add delays if( self->s.weapon == WEAP_LASERGUN || self->s.weapon == WEAP_PLASMAGUN ) firedelay = 1.0f; else firedelay = ( 1.0f - self->ai->pers.skillLevel ) - ( random()-0.25f ); if( firedelay > 0.0f ) { if( G_InFront( self, self->enemy ) ) { ucmd->buttons |= BUTTON_ATTACK; // could fire, but wants to? } // mess up angles only in the attacking frames if( self->r.client->ps.weaponState == WEAPON_STATE_READY || self->r.client->ps.weaponState == WEAPON_STATE_REFIRE || self->r.client->ps.weaponState == WEAPON_STATE_REFIRESTRONG ) { if( (self->s.weapon == WEAP_LASERGUN) || (self->s.weapon == WEAP_PLASMAGUN) ) { target[0] += sinf( (float)level.time/100.0) * wfac; target[1] += cosf( (float)level.time/100.0) * wfac; } else { target[0] += ( random()-0.5f ) * wfac; target[1] += ( random()-0.5f ) * wfac; } } } } //update angles VectorSubtract( target, fire_origin, self->ai->move_vector ); AI_ChangeAngle( self ); if( nav.debugMode && bot_showcombat->integer ) G_PrintChasersf( self, "%s: attacking %s\n", self->ai->pers.netname, self->enemy->r.client ? self->enemy->r.client->netname : self->classname ); return true; }
/* ============= ai_checkattack Decides if we're going to attack or do something else used by ai_run and ai_stand ============= */ bool ai_checkattack (edict_t *self, float dist) { vec3_t temp; bool hesDeadJim; // this causes monsters to run blindly to the combat point w/o firing if (self->goalentity) { if (self->monsterinfo.aiflags & AI_COMBAT_POINT) return false; if (self->monsterinfo.aiflags & AI_SOUND_TARGET) { if (level.time - self->enemy->teleport_time > 5000) { if (self->goalentity == self->enemy) { if (self->movetarget) self->goalentity = self->movetarget; else self->goalentity = NULL; } self->monsterinfo.aiflags &= ~AI_SOUND_TARGET; if (self->monsterinfo.aiflags & AI_TEMP_STAND_GROUND) self->monsterinfo.aiflags &= ~(AI_STAND_GROUND | AI_TEMP_STAND_GROUND); } else { self->show_hostile = level.time + 1000; return false; } } } enemy_vis = false; // see if the enemy is dead hesDeadJim = false; if ((!self->enemy) || (!self->enemy->r.inuse)) { hesDeadJim = true; } else if (self->monsterinfo.aiflags & AI_MEDIC) { if (self->enemy->health > 0) { hesDeadJim = true; self->monsterinfo.aiflags &= ~AI_MEDIC; } } 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; self->monsterinfo.walk (self); } 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; self->monsterinfo.stand (self); } return true; } } self->show_hostile = level.time + 1000; // wake up other monsters // check knowledge of enemy enemy_vis = G_Visible(self, self->enemy); if (enemy_vis) { self->monsterinfo.search_time = level.time + 5000; 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 = G_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); }
//========================================== // BOT_DMclass_FindEnemy // Scan for enemy (simplifed for now to just pick any visible enemy) //========================================== void BOT_DMclass_FindEnemy( edict_t *self ) { #define WEIGHT_MAXDISTANCE_FACTOR 15000 nav_ents_t *goalEnt; edict_t *bestTarget = NULL; float dist, weight, bestWeight = 9999999; int i; if( G_ISGHOSTING( self ) || GS_MatchState() == MATCH_STATE_COUNTDOWN || GS_ShootingDisabled() ) { self->ai->enemyReactionDelay = 0; self->enemy = self->ai->latched_enemy = NULL; return; } // we also latch NULL enemies, so the bot can loose them if( self->ai->enemyReactionDelay > 0 ) { self->ai->enemyReactionDelay -= game.frametime; return; } self->enemy = self->ai->latched_enemy; FOREACH_GOALENT( goalEnt ) { i = goalEnt->id; if( !goalEnt->ent || !goalEnt->ent->r.inuse ) continue; if( !goalEnt->ent->r.client ) // this may be changed, there could be enemies which aren't clients continue; if( G_ISGHOSTING( goalEnt->ent ) ) continue; if( self->ai->status.entityWeights[i] <= 0 || goalEnt->ent->flags & FL_NOTARGET ) continue; if( GS_TeamBasedGametype() && goalEnt->ent->s.team == self->s.team ) continue; dist = DistanceFast( self->s.origin, goalEnt->ent->s.origin ); // ignore very soft weighted enemies unless they are in your face if( dist > 500 && self->ai->status.entityWeights[i] <= 0.1f ) continue; if( dist > 700 && dist > WEIGHT_MAXDISTANCE_FACTOR * self->ai->status.entityWeights[i] ) continue; if( trap_inPVS( self->s.origin, goalEnt->ent->s.origin ) && G_Visible( self, goalEnt->ent ) ) { weight = dist / self->ai->status.entityWeights[i]; if( ( dist < 350 ) || G_InFront( self, goalEnt->ent ) ) { if( weight < bestWeight ) { bestWeight = weight; bestTarget = goalEnt->ent; } } } } AI_NewEnemyInView( self, bestTarget ); #undef WEIGHT_MAXDISTANCE_FACTOR }
/* =========== 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, the point of impact becomes a fakeplayer so that monsters that see the impact will respond as if they had seen the player. To avoid spending too much time, only a single client (or fakeclient) is checked each frame. This means multi player games will have slightly slower noticing monsters. ============ */ bool FindTarget (edict_t *self) { edict_t *client; bool heardit; bool noise; int r; if (self->monsterinfo.aiflags & AI_GOOD_GUY) { if (self->goalentity && self->goalentity->r.inuse && self->goalentity->classname) { 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 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 heardit = false; if ((level.sight_entity_framenum+1 >= level.framenum) && !(self->spawnflags & 1) ) { client = level.sight_entity; if (client->enemy == self->enemy) { return false; } } else if (level.sound_entity_framenum+1 >= level.framenum) { client = level.sound_entity; heardit = true; } else if (!(self->enemy) && (level.sound2_entity_framenum+1 >= level.framenum) && !(self->spawnflags & 1) ) { client = level.sound2_entity; heardit = true; } else { client = level.sight_client; if (!client) return false; // no clients to get mad at } // if the entity went away, forget it if (!client->r.inuse) return false; if (client == self->enemy) return true; // JDC false; noise = strcmp(client->classname, "player_noise") == 0; if (!noise && (client->r.svflags & SVF_NOCLIENT)) return false; if (client->r.client) { if (client->flags & FL_NOTARGET) return false; } else if (client->r.svflags & SVF_MONSTER) { if (!client->enemy) return false; if (client->enemy->flags & FL_NOTARGET) return false; } else if (heardit) { if (client->r.owner->flags & FL_NOTARGET) return false; } else return false; if (!heardit) { r = range (self, client); if (r == RANGE_FAR) return false; // this is where we would check invisibility // is client in an spot too dark to be seen? // if (client->light_level <= 5) // return false; if (!G_Visible (self, client)) { return false; } if (r == RANGE_NEAR) { if (client->show_hostile < level.time && !G_InFront (self, client)) { return false; } } else if (r == RANGE_MID) { if (!G_InFront (self, client)) { return false; } } self->enemy = client; if (!noise) { self->monsterinfo.aiflags &= ~AI_SOUND_TARGET; if (!self->enemy->r.client) { self->enemy = self->enemy->enemy; if (!self->enemy->r.client) { self->enemy = NULL; return false; } } } } else // heardit { vec3_t temp; if (self->spawnflags & 1) { if (!G_Visible (self, client)) return false; } VectorSubtract (client->s.origin, self->s.origin, temp); if (VectorLength(temp) > 1000) // too far to hear { return false; } // check area portals - if they are different and not connected then we can't hear it if (client->r.areanum != self->r.areanum) if (!trap_CM_AreasConnected(self->r.areanum, client->r.areanum)) return false; self->ideal_yaw = vectoyaw(temp); M_ChangeYaw (self); // hunt the sound for a bit; hopefully find the real player self->monsterinfo.aiflags |= AI_SOUND_TARGET; self->enemy = client; } // // got one // FoundTarget (self); if (!(self->monsterinfo.aiflags & AI_SOUND_TARGET) && (self->monsterinfo.sight)) self->monsterinfo.sight (self, self->enemy); return true; }