//========================================== // 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; vec3_t forward, vec; 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|FL_BUSY) ) 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; weight = dist / self->ai->status.entityWeights[i]; if( weight < bestWeight ) { if( trap_inPVS( self->s.origin, goalEnt->ent->s.origin ) && G_Visible( self, goalEnt->ent ) ) { bool close = dist < 2000 || goalEnt->ent == self->ai->last_attacker; if( !close ) { VectorSubtract( goalEnt->ent->s.origin, self->s.origin, vec ); VectorNormalize( vec ); close = DotProduct( vec, forward ) > 0.3; } if( close ) { bestWeight = weight; bestTarget = goalEnt->ent; } } } } AI_NewEnemyInView( self, bestTarget ); #undef WEIGHT_MAXDISTANCE_FACTOR }
void Bot::RegisterVisibleEnemies() { if(G_ISGHOSTING(self) || GS_MatchState() == MATCH_STATE_COUNTDOWN || GS_ShootingDisabled()) return; CheckIsInThinkFrame(__FUNCTION__); // Compute look dir before loop vec3_t lookDir; AngleVectors(self->s.angles, lookDir, nullptr, nullptr); float fov = 110.0f + 69.0f * Skill(); float dotFactor = cosf((float)DEG2RAD(fov / 2)); struct EntAndDistance { int entNum; float distance; EntAndDistance(int entNum_, float distance_): entNum(entNum_), distance(distance_) {} bool operator<(const EntAndDistance &that) const { return distance < that.distance; } }; // Do not call inPVS() and G_Visible() for potential targets inside a loop for all clients. // In worst case when all bots may see each other we get N^2 traces and PVS tests // First, select all candidate targets along with distance to a bot. // Then choose not more than BotBrain::maxTrackedEnemies nearest enemies for calling OnEnemyViewed() // It may cause data loss (far enemies may have higher logical priority), // but in a common good case (when there are few visible enemies) it preserves data, // and in the worst case mentioned above it does not act weird from player POV and prevents server hang up. // Note: non-client entities also may be candidate targets. StaticVector<EntAndDistance, MAX_EDICTS> candidateTargets; for (int i = 1; i < game.numentities; ++i) { edict_t *ent = game.edicts + i; if (botBrain.MayNotBeFeasibleEnemy(ent)) continue; // Reject targets quickly by fov Vec3 toTarget(ent->s.origin); toTarget -= self->s.origin; float squareDistance = toTarget.SquaredLength(); if (squareDistance < 1) continue; float invDistance = Q_RSqrt(squareDistance); toTarget *= invDistance; if (toTarget.Dot(lookDir) < dotFactor) continue; // It seams to be more instruction cache-friendly to just add an entity to a plain array // and sort it once after the loop instead of pushing an entity in a heap on each iteration candidateTargets.emplace_back(EntAndDistance(ENTNUM(ent), 1.0f / invDistance)); } std::sort(candidateTargets.begin(), candidateTargets.end()); // Select inPVS/visible targets first to aid instruction cache, do not call callbacks in loop StaticVector<edict_t *, MAX_CLIENTS> targetsInPVS; StaticVector<edict_t *, MAX_CLIENTS> visibleTargets; static_assert(AiBaseEnemyPool::MAX_TRACKED_ENEMIES <= MAX_CLIENTS, "targetsInPVS capacity may be exceeded"); for (int i = 0, end = std::min(candidateTargets.size(), botBrain.MaxTrackedEnemies()); i < end; ++i) { edict_t *ent = game.edicts + candidateTargets[i].entNum; if (trap_inPVS(self->s.origin, ent->s.origin)) targetsInPVS.push_back(ent); } for (auto ent: targetsInPVS) if (G_Visible(self, ent)) visibleTargets.push_back(ent); // Call bot brain callbacks on visible targets for (auto ent: visibleTargets) botBrain.OnEnemyViewed(ent); botBrain.AfterAllEnemiesViewed(); CheckAlertSpots(visibleTargets); }
/* * GS_ThinkPlayerWeapon */ int GS_ThinkPlayerWeapon( player_state_t *playerState, int buttons, int msecs, int timeDelta ) { firedef_t *firedef; qboolean refire = qfalse; assert( playerState->stats[STAT_PENDING_WEAPON] >= 0 && playerState->stats[STAT_PENDING_WEAPON] < WEAP_TOTAL ); if( GS_MatchPaused() ) return playerState->stats[STAT_WEAPON]; if( playerState->pmove.pm_type != PM_NORMAL ) { playerState->weaponState = WEAPON_STATE_READY; playerState->stats[STAT_PENDING_WEAPON] = playerState->stats[STAT_WEAPON] = WEAP_NONE; playerState->stats[STAT_WEAPON_TIME] = 0; return playerState->stats[STAT_WEAPON]; } if( playerState->pmove.stats[PM_STAT_NOUSERCONTROL] > 0 ) buttons = 0; if( !msecs ) goto done; if( playerState->stats[STAT_WEAPON_TIME] > 0 ) playerState->stats[STAT_WEAPON_TIME] -= msecs; else playerState->stats[STAT_WEAPON_TIME] = 0; firedef = GS_FiredefForPlayerState( playerState, playerState->stats[STAT_WEAPON] ); // during cool-down time it can shoot again or go into reload time if( playerState->weaponState == WEAPON_STATE_REFIRE || playerState->weaponState == WEAPON_STATE_REFIRESTRONG ) { int last_firemode; if( playerState->stats[STAT_WEAPON_TIME] > 0 ) goto done; last_firemode = ( playerState->weaponState == WEAPON_STATE_REFIRESTRONG ) ? FIRE_MODE_STRONG : FIRE_MODE_WEAK; if( last_firemode == firedef->fire_mode ) refire = qtrue; playerState->weaponState = WEAPON_STATE_READY; } // nothing can be done during reload time if( playerState->weaponState == WEAPON_STATE_RELOADING ) { if( playerState->stats[STAT_WEAPON_TIME] > 0 ) goto done; playerState->weaponState = WEAPON_STATE_READY; } if( playerState->weaponState == WEAPON_STATE_NOAMMOCLICK ) { if( playerState->stats[STAT_WEAPON_TIME] > 0 ) goto done; if( playerState->stats[STAT_WEAPON] != playerState->stats[STAT_PENDING_WEAPON] ) playerState->weaponState = WEAPON_STATE_READY; } // there is a weapon to be changed if( playerState->stats[STAT_WEAPON] != playerState->stats[STAT_PENDING_WEAPON] ) { if( ( playerState->weaponState == WEAPON_STATE_READY ) || ( playerState->weaponState == WEAPON_STATE_DROPPING ) || ( playerState->weaponState == WEAPON_STATE_ACTIVATING ) ) { if( playerState->weaponState != WEAPON_STATE_DROPPING ) { playerState->weaponState = WEAPON_STATE_DROPPING; playerState->stats[STAT_WEAPON_TIME] += firedef->weapondown_time; if( firedef->weapondown_time ) module_PredictedEvent( playerState->POVnum, EV_WEAPONDROP, 0 ); } } } // do the change if( playerState->weaponState == WEAPON_STATE_DROPPING ) { if( playerState->stats[STAT_WEAPON_TIME] > 0 ) goto done; playerState->stats[STAT_WEAPON] = playerState->stats[STAT_PENDING_WEAPON]; // update the firedef firedef = GS_FiredefForPlayerState( playerState, playerState->stats[STAT_WEAPON] ); playerState->weaponState = WEAPON_STATE_ACTIVATING; playerState->stats[STAT_WEAPON_TIME] += firedef->weaponup_time; module_PredictedEvent( playerState->POVnum, EV_WEAPONACTIVATE, playerState->stats[STAT_WEAPON] ); } if( playerState->weaponState == WEAPON_STATE_ACTIVATING ) { if( playerState->stats[STAT_WEAPON_TIME] > 0 ) goto done; playerState->weaponState = WEAPON_STATE_READY; } if( ( playerState->weaponState == WEAPON_STATE_READY ) || ( playerState->weaponState == WEAPON_STATE_NOAMMOCLICK ) ) { if( playerState->stats[STAT_WEAPON_TIME] > 0 ) goto done; if( !GS_ShootingDisabled() ) { if( buttons & BUTTON_ATTACK ) { if( GS_CheckAmmoInWeapon( playerState, playerState->stats[STAT_WEAPON] ) ) { playerState->weaponState = WEAPON_STATE_FIRING; } else { // player has no ammo nor clips if( playerState->weaponState == WEAPON_STATE_NOAMMOCLICK ) { playerState->weaponState = WEAPON_STATE_RELOADING; playerState->stats[STAT_WEAPON_TIME] += NOAMMOCLICK_AUTOSWITCH; if( playerState->stats[STAT_PENDING_WEAPON] == playerState->stats[STAT_WEAPON] ) playerState->stats[STAT_PENDING_WEAPON] = GS_SelectBestWeapon( playerState ); } else { playerState->weaponState = WEAPON_STATE_NOAMMOCLICK; playerState->stats[STAT_WEAPON_TIME] += NOAMMOCLICK_PENALTY; module_PredictedEvent( playerState->POVnum, EV_NOAMMOCLICK, 0 ); goto done; } } } // gunblade auto attack is special else if( playerState->stats[STAT_WEAPON] == WEAP_GUNBLADE && playerState->pmove.stats[PM_STAT_NOUSERCONTROL] <= 0 && playerState->pmove.stats[PM_STAT_NOAUTOATTACK] <= 0 && GS_CheckBladeAutoAttack( playerState, timeDelta ) ) { firedef = &GS_GetWeaponDef( WEAP_GUNBLADE )->firedef_weak; playerState->weaponState = WEAPON_STATE_FIRING; } } } if( playerState->weaponState == WEAPON_STATE_FIRING ) { int parm = playerState->stats[STAT_WEAPON]; if( firedef->fire_mode == FIRE_MODE_STRONG ) parm |= EV_INVERSE; playerState->stats[STAT_WEAPON_TIME] += firedef->reload_time; if( firedef->fire_mode == FIRE_MODE_STRONG ) playerState->weaponState = WEAPON_STATE_REFIRESTRONG; else playerState->weaponState = WEAPON_STATE_REFIRE; if( refire && firedef->smooth_refire ) module_PredictedEvent( playerState->POVnum, EV_SMOOTHREFIREWEAPON, parm ); else module_PredictedEvent( playerState->POVnum, EV_FIREWEAPON, parm ); // waste ammo if( !GS_InfiniteAmmo() ) { if( firedef->ammo_id != AMMO_NONE && firedef->usage_count ) playerState->inventory[firedef->ammo_id] -= firedef->usage_count; } } done: return playerState->stats[STAT_WEAPON]; }
//========================================== // 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; for( i = 0; i < nav.num_goalEnts; i++ ) { goalEnt = &nav.goalEnts[i]; 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->ai.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 }