void Bot::CheckAlertSpots(const StaticVector<edict_t *, MAX_CLIENTS> &visibleTargets) { float scores[MAX_ALERT_SPOTS]; // First compute scores (good for instruction cache) for (unsigned i = 0; i < alertSpots.size(); ++i) { float score = 0.0f; const auto &alertSpot = alertSpots[i]; const float squareRadius = alertSpot.radius * alertSpot.radius; const float invRadius = 1.0f / alertSpot.radius; for (const edict_t *ent: visibleTargets) { float squareDistance = DistanceSquared(ent->s.origin, alertSpot.origin.Data()); if (squareDistance > squareRadius) continue; float distance = Q_RSqrt(squareDistance + 0.001f); score += 1.0f - distance * invRadius; // Put likely case first if (!(ent->s.effects & EF_CARRIER)) score *= alertSpot.regularEnemyInfluenceScale; else score *= alertSpot.carrierEnemyInfluenceScale; } // Clamp score by a max value clamp_high(score, 3.0f); // Convert score to [0, 1] range score /= 3.0f; // Get a square root of score (values closer to 0 gets scaled more than ones closer to 1) score = 1.0f / Q_RSqrt(score + 0.001f); // Sanitize clamp(score, 0.0f, 1.0f); scores[i] = score; } // Then call callbacks const unsigned levelTime = level.time; for (unsigned i = 0; i < alertSpots.size(); ++i) { auto &alertSpot = alertSpots[i]; unsigned nonReportedFor = levelTime - alertSpot.lastReportedAt; if (nonReportedFor >= 1000) alertSpot.lastReportedScore = 0.0f; // Since scores are sanitized, they are in range [0.0f, 1.0f], and abs(scoreDelta) is in range [-1.0f, 1.0f]; float scoreDelta = scores[i] - alertSpot.lastReportedScore; if (scoreDelta >= 0) { if (nonReportedFor >= 1000 - scoreDelta * 500) alertSpot.Alert(this, scores[i]); } else { if (nonReportedFor >= 500 - scoreDelta * 500) alertSpot.Alert(this, scores[i]); } } }
bool Bot::MayKeepRunningInCombat() const { if (!HasEnemy()) FailWith("MayKeepRunningInCombat(): there is no enemy"); Vec3 enemyToBotDir = Vec3(self->s.origin) - EnemyOrigin(); bool enemyMayHit = true; if (IsEnemyAStaticSpot()) { enemyMayHit = false; } else if (EnemyFireDelay() > 300) { enemyMayHit = false; } else { Vec3 enemyLookDir = EnemyLookDir(); float squaredDistance = enemyToBotDir.SquaredLength(); if (squaredDistance > 1) { float distance = 1.0f / Q_RSqrt(squaredDistance); enemyToBotDir *= 1.0f / distance; // Compute a cosine of angle between enemy look dir and enemy to bot dir float cosPhi = enemyLookDir.Dot(enemyToBotDir); // Be aware of RL splash on this range if (distance < 150.0f) enemyMayHit = cosPhi > 0.3; else if (cosPhi <= 0.3) enemyMayHit = false; else { float cotPhi = Q_RSqrt((1.0f / (cosPhi * cosPhi)) - 1); float sideMiss = distance / cotPhi; // Use hitbox height plus a bit as a worst case float hitboxLargestSectionSide = 8.0f + playerbox_stand_maxs[2] - playerbox_stand_mins[2]; enemyMayHit = sideMiss < hitboxLargestSectionSide; } } } if (enemyMayHit) return false; vec3_t botLookDir; AngleVectors(self->s.angles, botLookDir, nullptr, nullptr); // Check whether the bot may hit while running return ((-enemyToBotDir).Dot(botLookDir) > 0.99); }
BotItemsSelector::ItemAndGoalWeights BotItemsSelector::ComputeAmmoWeights( const gsitem_t *item ) const { if( Inventory()[item->tag] < item->inventory_max ) { float quantityFactor = 1.0f - Inventory()[item->tag] / (float)item->inventory_max; if( quantityFactor > 0 ) { quantityFactor = 1.0f / Q_RSqrt( quantityFactor ); } for( int weapon = WEAP_GUNBLADE; weapon < WEAP_TOTAL; weapon++ ) { // TODO: Preache const gsitem_t *weaponItem = GS_FindItemByTag( weapon ); if( weaponItem->ammo_tag == item->tag ) { if( Inventory()[weaponItem->tag] ) { switch( weaponItem->tag ) { case WEAP_ELECTROBOLT: return ItemAndGoalWeights( quantityFactor, quantityFactor ); case WEAP_LASERGUN: return ItemAndGoalWeights( quantityFactor * 1.1f, quantityFactor ); case WEAP_PLASMAGUN: return ItemAndGoalWeights( quantityFactor * 1.1f, quantityFactor ); case WEAP_ROCKETLAUNCHER: return ItemAndGoalWeights( quantityFactor, quantityFactor ); default: return ItemAndGoalWeights( 0.5f * quantityFactor, quantityFactor ); } } return ItemAndGoalWeights( quantityFactor * 0.33f, quantityFactor * 0.5f ); } } } return ItemAndGoalWeights( 0.0, 0.0f ); }
BotItemsSelector::ItemAndGoalWeights BotItemsSelector::ComputeWeaponWeights( const gsitem_t *item, bool onlyGotGB ) const { if( Inventory()[item->tag] ) { // TODO: Precache const gsitem_t *ammo = GS_FindItemByTag( item->ammo_tag ); if( Inventory()[ammo->tag] >= ammo->inventory_max ) { return ItemAndGoalWeights( 0, 0 ); } float ammoQuantityFactor = 1.0f - Inventory()[ammo->tag] / (float)ammo->inventory_max; if( ammoQuantityFactor > 0 ) { ammoQuantityFactor = 1.0f / Q_RSqrt( ammoQuantityFactor ); } switch( item->tag ) { case WEAP_ELECTROBOLT: return ItemAndGoalWeights( ammoQuantityFactor, 0.5f * ammoQuantityFactor ); case WEAP_LASERGUN: return ItemAndGoalWeights( ammoQuantityFactor * 1.1f, 0.6f * ammoQuantityFactor ); case WEAP_PLASMAGUN: return ItemAndGoalWeights( ammoQuantityFactor * 1.1f, 0.6f * ammoQuantityFactor ); case WEAP_ROCKETLAUNCHER: return ItemAndGoalWeights( ammoQuantityFactor, 0.5f * ammoQuantityFactor ); default: return ItemAndGoalWeights( 0.75f * ammoQuantityFactor, 0.75f * ammoQuantityFactor ); } } // We may consider plasmagun in a bot's hand as a top tier weapon too const int topTierWeapons[4] = { WEAP_ELECTROBOLT, WEAP_LASERGUN, WEAP_ROCKETLAUNCHER, WEAP_PLASMAGUN }; // TODO: Precompute float topTierWeaponGreed = 0.0f; for( int i = 0; i < 4; ++i ) { if( !Inventory()[topTierWeapons[i]] ) { topTierWeaponGreed += 1.0f; } } for( int i = 0; i < 4; ++i ) { if( topTierWeapons[i] == item->tag ) { float weight = ( onlyGotGB ? 2.0f : 0.9f ) + ( topTierWeaponGreed - 1.0f ) / 3.0f; return ItemAndGoalWeights( weight, weight ); } } return onlyGotGB ? ItemAndGoalWeights( 1.5f, 2.0f ) : ItemAndGoalWeights( 0.75f, 0.75f ); }
void TacticalSpotsDetector::SortByVisAndOtherFactors(const OriginParams ¶ms, TraceCheckedAreas &areas) { const aas_area_t *worldAreas = AiAasWorld::Instance()->Areas(); const aas_areasettings_t *worldAreaSettings = AiAasWorld::Instance()->AreaSettings(); const float originZ = params.origin[2]; const float searchRadius = params.searchRadius; // A matrix of trace results: // -1 means that trace has not been computed // 0 means that trace.fraction < 1.0f // 1 means that trace.fraction == 1.0f signed char traceResultCache[areas.capacity() * areas.capacity()]; std::fill(traceResultCache, traceResultCache + areas.capacity() * areas.capacity(), -1); // Compute area points to avoid doing it in the trace loop. vec3_t areaPoints[areas.capacity()]; for (unsigned i = 0; i < areas.size(); ++i) { const aas_area_t &area = worldAreas[areas[i].areaNum]; VectorCopy(area.center, areaPoints[i]); areaPoints[i][2] = area.mins[2] + PLAYER_VIEW_GROUND_OFFSET; } trace_t trace; for (unsigned i = 0; i < areas.size(); ++i) { // Avoid fp <-> int conversions in a loop int numVisAreas = 0; for (unsigned j = 0; j < i; ++j) { int cachedTraceResult = traceResultCache[areas.capacity() * i + j]; if (cachedTraceResult >= 0) { numVisAreas += cachedTraceResult; continue; } G_Trace(&trace, areaPoints[i], nullptr, nullptr, areaPoints[j], nullptr, MASK_AISOLID); // Omit fractional part by intention signed char visibility = (signed char)trace.fraction; traceResultCache[areas.capacity() * i + j] = visibility; traceResultCache[areas.capacity() * j + i] = visibility; numVisAreas += visibility; } // Skip a trace from an area to itself for (unsigned j = i + 1; j < areas.size(); ++j) { int cachedTraceResult = traceResultCache[areas.capacity() * i + j]; if (cachedTraceResult >= 0) { numVisAreas += cachedTraceResult; continue; } G_Trace(&trace, areaPoints[i], nullptr, nullptr, areaPoints[j], nullptr, MASK_AISOLID); // Omit fractional part by intention signed char visibility = (signed char)trace.fraction; traceResultCache[areas.capacity() * i + j] = visibility; traceResultCache[areas.capacity() * j + i] = visibility; numVisAreas += visibility; } float visFactor = numVisAreas / (float)areas.size(); visFactor = 1.0f / Q_RSqrt(visFactor); areas[i].score *= 0.1f + 0.9f * visFactor; // We should modify the final score by following factors. // These factors should be checked in earlier calls but mainly for early rejection of non-suitable areas. const int areaNum = areas[i].areaNum; float height = (worldAreas[areaNum].mins[2] - originZ - minHeightAdvantage); float heightFactor = BoundedFraction(height, searchRadius - minHeightAdvantage); areas[i].score = ApplyFactor(areas[i].score, heightFactor, heightInfluence); const aas_areasettings_t &areaSettings = worldAreaSettings[areaNum]; if (areaSettings.areaflags & AREA_WALL) areas[i].score *= wallPenalty; if (areaSettings.areaflags & AREA_LEDGE) areas[i].score *= ledgePenalty; } // Sort results so best score areas are first std::sort(areas.begin(), areas.end()); }
void BotTacticalSpotsCache::FindReachableClassEntities( const Vec3 &origin, float radius, const char *classname, BotTacticalSpotsCache::ReachableEntities &result ) { int *triggerEntities; int numEntities = FindNearbyEntities( origin, radius, &triggerEntities ); ReachableEntities candidateEntities; // Copy to locals for faster access (a compiler might be paranoid about aliasing) edict_t *gameEdicts = game.edicts; if( numEntities > (int)candidateEntities.capacity() ) { for( int i = 0; i < numEntities; ++i ) { edict_t *ent = gameEdicts + triggerEntities[i]; // Specify expected strcmp() result explicitly to avoid misinterpreting the condition // (Strings are equal if an strcmp() result is zero) if( strcmp( ent->classname, classname ) != 0 ) { continue; } float distance = DistanceFast( origin.Data(), ent->s.origin ); candidateEntities.push_back( EntAndScore( triggerEntities[i], radius - distance ) ); if( candidateEntities.size() == candidateEntities.capacity() ) { break; } } } else { for( int i = 0; i < numEntities; ++i ) { edict_t *ent = gameEdicts + triggerEntities[i]; if( strcmp( ent->classname, classname ) != 0 ) { continue; } float distance = DistanceFast( origin.Data(), ent->s.origin ); candidateEntities.push_back( EntAndScore( triggerEntities[i], radius - distance ) ); } } const AiAasWorld *aasWorld = AiAasWorld::Instance(); AiAasRouteCache *routeCache = self->ai->botRef->routeCache; bool testTwoCurrAreas = false; int fromAreaNum = 0; // If an origin matches actual bot origin if( ( origin - self->s.origin ).SquaredLength() < WorldState::OriginVar::MAX_ROUNDING_SQUARE_DISTANCE_ERROR ) { // Try testing both areas if( self->ai->botRef->CurrAreaNum() != self->ai->botRef->DroppedToFloorAreaNum() ) { testTwoCurrAreas = true; } else { fromAreaNum = self->ai->botRef->CurrAreaNum(); } } else { fromAreaNum = aasWorld->FindAreaNum( origin ); } if( testTwoCurrAreas ) { int fromAreaNums[2] = { self->ai->botRef->CurrAreaNum(), self->ai->botRef->DroppedToFloorAreaNum() }; for( EntAndScore &candidate: candidateEntities ) { edict_t *ent = gameEdicts + candidate.entNum; int toAreaNum = FindMostFeasibleEntityAasArea( ent, aasWorld ); if( !toAreaNum ) { continue; } int travelTime = 0; for( int i = 0; i < 2; ++i ) { travelTime = routeCache->TravelTimeToGoalArea( fromAreaNums[i], toAreaNum, Bot::ALLOWED_TRAVEL_FLAGS ); if( travelTime ) { break; } } if( !travelTime ) { continue; } // AAS travel time is in seconds^-2 float factor = 1.0f / Q_RSqrt( 1.0001f - BoundedFraction( travelTime, 200 ) ); result.push_back( EntAndScore( candidate.entNum, candidate.score * factor ) ); } } else { for( EntAndScore &candidate: candidateEntities ) { edict_t *ent = gameEdicts + candidate.entNum; int toAreaNum = FindMostFeasibleEntityAasArea( ent, aasWorld ); if( !toAreaNum ) { continue; } int travelTime = routeCache->TravelTimeToGoalArea( fromAreaNum, toAreaNum, Bot::ALLOWED_TRAVEL_FLAGS ); if( !travelTime ) { continue; } float factor = 1.0f / Q_RSqrt( 1.0001f - BoundedFraction( travelTime, 200 ) ); result.push_back( EntAndScore( candidate.entNum, candidate.score * factor ) ); } } // Sort entities so best entities are first std::sort( result.begin(), result.end() ); }
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); }