bool TurretComponent::TargetCanBeHit() { if (!target) return false; Vec3 aimDirection = RelativeAnglesToDirection(relativeAimAngles); Vec3 traceStart = Vec3::Load(entity.oldEnt->s.pos.trBase); Vec3 traceEnd = traceStart + range * aimDirection; trace_t tr; trap_Trace(&tr, traceStart.Data(), nullptr, nullptr, traceEnd.Data(), entity.oldEnt->s.number, MASK_SHOT, 0); return (tr.entityNum == target->s.number); }
bool BotTacticalSpotsCache::FindMiddleRangeTacticalSpot( const Vec3 &origin, const Vec3 &enemyOrigin, vec3_t result ) { TacticalSpotsRegistry::AdvantageProblemParams problemParams( enemyOrigin.Data() ); problemParams.SetMinSpotDistanceToEntity( WorldState::CLOSE_RANGE_MAX ); problemParams.SetMaxSpotDistanceToEntity( WorldState::MIDDLE_RANGE_MAX ); problemParams.SetOriginDistanceInfluence( 0.3f ); problemParams.SetEntityDistanceInfluence( 0.4f ); problemParams.SetEntityWeightFalloffDistanceRatio( 0.5f ); problemParams.SetTravelTimeInfluence( 0.7f ); problemParams.SetMinHeightAdvantageOverOrigin( -16.0f ); problemParams.SetMinHeightAdvantageOverEntity( -64.0f ); problemParams.SetHeightOverOriginInfluence( 0.6f ); problemParams.SetHeightOverEntityInfluence( 0.8f ); problemParams.SetCheckToAndBackReachability( false ); float searchRadius = WorldState::MIDDLE_RANGE_MAX; float distanceToEnemy = ( origin - enemyOrigin ).LengthFast(); if( distanceToEnemy < WorldState::CLOSE_RANGE_MAX ) { searchRadius += WorldState::CLOSE_RANGE_MAX; } else if( distanceToEnemy > WorldState::MIDDLE_RANGE_MAX ) { searchRadius += distanceToEnemy - WorldState::MIDDLE_RANGE_MAX; } else { searchRadius *= 1.0f + 0.5f * Skill(); } return FindForOrigin( problemParams, origin, searchRadius, result ); }
bool BotTacticalSpotsCache::FindCloseRangeTacticalSpot( const Vec3 &origin, const Vec3 &enemyOrigin, vec3_t result ) { TacticalSpotsRegistry::AdvantageProblemParams problemParams( enemyOrigin.Data() ); float meleeRange = GS_GetWeaponDef( WEAP_GUNBLADE )->firedef_weak.timeout; problemParams.SetMinSpotDistanceToEntity( meleeRange ); problemParams.SetMaxSpotDistanceToEntity( WorldState::CLOSE_RANGE_MAX ); problemParams.SetOriginDistanceInfluence( 0.0f ); problemParams.SetEntityDistanceInfluence( 0.7f ); problemParams.SetEntityWeightFalloffDistanceRatio( 0.8f ); problemParams.SetTravelTimeInfluence( 0.0f ); problemParams.SetMinHeightAdvantageOverOrigin( -16.0f ); problemParams.SetMinHeightAdvantageOverEntity( -16.0f ); problemParams.SetHeightOverOriginInfluence( 0.4f ); problemParams.SetHeightOverEntityInfluence( 0.9f ); // Bot should be able to retreat from close combat problemParams.SetCheckToAndBackReachability( true ); float searchRadius = WorldState::CLOSE_RANGE_MAX * 2; float distanceToEnemy = ( origin - enemyOrigin ).LengthFast(); if( distanceToEnemy > WorldState::CLOSE_RANGE_MAX ) { searchRadius += distanceToEnemy - WorldState::CLOSE_RANGE_MAX; // On this range retreating to an old position makes little sense if( distanceToEnemy > 0.5f * WorldState::MIDDLE_RANGE_MAX ) { problemParams.SetCheckToAndBackReachability( false ); } } return FindForOrigin( problemParams, origin, searchRadius, result ); }
Vec3 Ai::GetNewViewAngles( const vec3_t oldAngles, const Vec3 &desiredDirection, unsigned frameTime, float angularSpeedMultiplier ) const { // Based on turret script code // For those trying to learn working with angles // Vec3.x is the PITCH angle (up and down rotation) // Vec3.y is the YAW angle (left and right rotation) // Vec3.z is the ROLL angle (left and right inclination) vec3_t newAngles, desiredAngles; VecToAngles( desiredDirection.Data(), desiredAngles ); // Normalize180 all angles so they can be compared for( int i = 0; i < 3; ++i ) { newAngles[i] = AngleNormalize180( oldAngles[i] ); desiredAngles[i] = AngleNormalize180( desiredAngles[i] ); } // Rotate the entity angles to the desired angles if( !VectorCompare( newAngles, desiredAngles ) ) { for( auto angleNum: { YAW, PITCH } ) { newAngles[angleNum] = GetChangedAngle( newAngles[angleNum], desiredAngles[angleNum], frameTime, angularSpeedMultiplier, angleNum ); } } return Vec3( newAngles ); }
bool BotTacticalSpotsCache::FindFarRangeTacticalSpot( const Vec3 &origin, const Vec3 &enemyOrigin, vec3_t result ) { TacticalSpotsRegistry::AdvantageProblemParams problemParams( enemyOrigin.Data() ); problemParams.SetMinSpotDistanceToEntity( WorldState::MIDDLE_RANGE_MAX ); problemParams.SetMaxSpotDistanceToEntity( WorldState::FAR_RANGE_MAX ); problemParams.SetOriginDistanceInfluence( 0.0f ); problemParams.SetEntityDistanceInfluence( 0.3f ); problemParams.SetEntityWeightFalloffDistanceRatio( 0.25f ); problemParams.SetTravelTimeInfluence( 0.8f ); problemParams.SetMinHeightAdvantageOverOrigin( -192.0f ); problemParams.SetMinHeightAdvantageOverEntity( -512.0f ); problemParams.SetHeightOverOriginInfluence( 0.3f ); problemParams.SetHeightOverEntityInfluence( 0.5f ); problemParams.SetCheckToAndBackReachability( false ); float searchRadius = 192.0f + 768.0f * Skill(); float distanceToEnemy = ( origin - enemyOrigin ).LengthFast(); float minSearchDistanceToEnemy = distanceToEnemy - searchRadius; float maxSearchDistanceToEnemy = distanceToEnemy + searchRadius; if( minSearchDistanceToEnemy < WorldState::MIDDLE_RANGE_MAX ) { searchRadius += WorldState::MIDDLE_RANGE_MAX - minSearchDistanceToEnemy; } else if( maxSearchDistanceToEnemy > WorldState::FAR_RANGE_MAX ) { searchRadius += maxSearchDistanceToEnemy - WorldState::FAR_RANGE_MAX; } return FindForOrigin( problemParams, origin, searchRadius, result ); }
bool BotTacticalSpotsCache::FindCoverSpot( const Vec3 &origin, const Vec3 &enemyOrigin, vec3_t result ) { const float searchRadius = 192.0f + 512.0f * Skill(); TacticalSpotsRegistry::CoverProblemParams problemParams( enemyOrigin.Data(), 32.0f ); problemParams.SetOriginDistanceInfluence( 0.0f ); problemParams.SetTravelTimeInfluence( 0.9f ); problemParams.SetMinHeightAdvantageOverOrigin( -searchRadius ); problemParams.SetHeightOverOriginInfluence( 0.3f ); problemParams.SetCheckToAndBackReachability( false ); auto *tacticalSpotsRegistry = TacticalSpotsRegistry::Instance(); if( BotHasAlmostSameOrigin( origin ) ) { TacticalSpotsRegistry::OriginParams originParams( self, searchRadius, RouteCache() ); return tacticalSpotsRegistry->FindCoverSpots( originParams, problemParams, (vec3_t *)result, 1 ) != 0; } TacticalSpotsRegistry::OriginParams originParams( origin.Data(), searchRadius, RouteCache() ); return tacticalSpotsRegistry->FindCoverSpots( originParams, problemParams, (vec3_t *)result, 1 ) != 0; }
inline bool BotTacticalSpotsCache::FindForOrigin( const ProblemParams &problemParams, const Vec3 &origin, float searchRadius, vec3_t result ) { vec3_t *spots = (vec3_t *)result; const TacticalSpotsRegistry *tacticalSpotsRegistry = TacticalSpotsRegistry::Instance(); if( BotHasAlmostSameOrigin( origin ) ) { // Provide a bot entity to aid trace checks TacticalSpotsRegistry::OriginParams originParams( self, searchRadius, RouteCache() ); return tacticalSpotsRegistry->FindPositionalAdvantageSpots( originParams, problemParams, spots, 1 ) > 0; } TacticalSpotsRegistry::OriginParams originParams( origin.Data(), searchRadius, RouteCache() ); return tacticalSpotsRegistry->FindPositionalAdvantageSpots( originParams, problemParams, spots, 1 ) > 0; }
void TurretComponent::SetBaseDirection() { vec3_t torsoDirectionOldVec; AngleVectors(entity.oldEnt->s.angles, torsoDirectionOldVec, nullptr, nullptr); Vec3 torsoDirection = Math::Normalize(Vec3::Load(torsoDirectionOldVec)); Vec3 traceStart = Vec3::Load(entity.oldEnt->s.pos.trBase); Vec3 traceEnd = traceStart + MINIMUM_CLEARANCE * torsoDirection; trace_t tr; trap_Trace(&tr, traceStart.Data(), nullptr, nullptr, traceEnd.Data(), entity.oldEnt->s.number, MASK_SHOT, 0); // TODO: Check the presence of a PhysicsComponent to decide whether the obstacle is permanent. if (tr.entityNum == ENTITYNUM_WORLD || g_entities[tr.entityNum].entity->Get<BuildableComponent>()) { baseDirection = -torsoDirection; } else { baseDirection = torsoDirection; } turretLogger.Verbose("Base direction set to %s.", baseDirection); }
int Ai::CheckTravelTimeMillis( const Vec3& from, const Vec3 &to, bool allowUnreachable ) { // We try to use the same checks the TacticalSpotsRegistry performs to find spots. // If a spot is not reachable, it is an bug, // because a reachability must have been checked by the spots registry first in a few preceeding calls. int fromAreaNum; constexpr float squareDistanceError = WorldState::OriginVar::MAX_ROUNDING_SQUARE_DISTANCE_ERROR; if( ( from - self->s.origin ).SquaredLength() < squareDistanceError ) { fromAreaNum = aasWorld->FindAreaNum( self ); } else { fromAreaNum = aasWorld->FindAreaNum( from ); } if( !fromAreaNum ) { if( allowUnreachable ) { return 0; } FailWith( "CheckTravelTimeMillis(): Can't find `from` AAS area" ); } const int toAreaNum = aasWorld->FindAreaNum( to.Data() ); if( !toAreaNum ) { if( allowUnreachable ) { return 0; } FailWith( "CheckTravelTimeMillis(): Can't find `to` AAS area" ); } for( int flags: { self->ai->aiRef->PreferredTravelFlags(), self->ai->aiRef->AllowedTravelFlags() } ) { if( int aasTravelTime = routeCache->TravelTimeToGoalArea( fromAreaNum, toAreaNum, flags ) ) { return 10U * aasTravelTime; } } if( allowUnreachable ) { return 0; } FailWith( "CheckTravelTimeMillis(): Can't find travel time %d->%d\n", fromAreaNum, toAreaNum ); }
bool BotTacticalSpotsCache::FindSniperRangeTacticalSpot( const Vec3 &origin, const Vec3 &enemyOrigin, vec3_t result ) { TacticalSpotsRegistry::AdvantageProblemParams problemParams( enemyOrigin.Data() ); problemParams.SetMinSpotDistanceToEntity( WorldState::FAR_RANGE_MAX ); problemParams.SetOriginDistanceInfluence( 0.0f ); problemParams.SetTravelTimeInfluence( 0.8f ); problemParams.SetMinHeightAdvantageOverOrigin( -1024.0f ); problemParams.SetMinHeightAdvantageOverEntity( -1024.0f ); problemParams.SetHeightOverOriginInfluence( 0.3f ); problemParams.SetHeightOverEntityInfluence( 0.1f ); problemParams.SetCheckToAndBackReachability( false ); float searchRadius = 192.0f + 768.0f * Skill(); float distanceToEnemy = ( origin - enemyOrigin ).LengthFast(); // If bot is not on sniper range, increase search radius (otherwise a point in a sniper range can't be found). if( distanceToEnemy - searchRadius < WorldState::FAR_RANGE_MAX ) { searchRadius += WorldState::FAR_RANGE_MAX - distanceToEnemy + searchRadius; } return FindForOrigin( problemParams, origin, searchRadius, result ); }
// TODO: Use a proper trajectory trace, once available, to ensure friendly buildables are never hit. bool SpikerComponent::SafeToShoot(Vec3 direction) { const missileAttributes_t* ma = BG_Missile(MIS_SPIKER); float missileSize = (float)ma->size; trace_t trace; vec3_t mins, maxs; Vec3 end = Vec3::Load(entity.oldEnt->s.origin) + (SPIKE_RANGE * direction); // Test once with normal and once with inflated missile bounding box. for (float traceSize : {missileSize, missileSize * SAFETY_TRACE_INFLATION}) { mins[0] = mins[1] = mins[2] = -traceSize; maxs[0] = maxs[1] = maxs[2] = traceSize; trap_Trace(&trace, entity.oldEnt->s.origin, mins, maxs, end.Data(), entity.oldEnt->s.number, ma->clipmask, 0); gentity_t* hit = &g_entities[trace.entityNum]; if (hit && G_OnSameTeam(entity.oldEnt, hit)) { return false; } } return true; }
Vec3 TurretComponent::RelativeAnglesToAbsoluteAngles(const Vec3 relativeAngles) const { quat_t torsoRotation; quat_t relativeRotation; quat_t absoluteRotation; vec3_t absoluteAngles; AnglesToQuat(TorsoAngles().Data(), torsoRotation); AnglesToQuat(relativeAngles.Data(), relativeRotation); // Rotate by torso rotation in world space, then by relative orientation in torso space. // This is equivalent to rotating by relative orientation in world space, then by torso rotation // in world space. This then is equivalent to multiplying the torso rotation in world space on // the left hand side and the relative rotation in world space on the right hand side. QuatMultiply(torsoRotation, relativeRotation, absoluteRotation); QuatToAngles(absoluteRotation, absoluteAngles); /*turretLogger.Debug("RelativeAnglesToAbsoluteAngles: %s → %s. Torso angles: %s.", Utility::Print(relativeAngles), Utility::Print(Vec3::Load(absoluteAngles)), TorsoAngles() );*/ return Vec3::Load(absoluteAngles); }
Vec3 TurretComponent::AbsoluteAnglesToRelativeAngles(const Vec3 absoluteAngles) const { quat_t torsoRotation; quat_t absoluteRotation; quat_t relativeRotation; vec3_t relativeAngles; AnglesToQuat(TorsoAngles().Data(), torsoRotation); AnglesToQuat(absoluteAngles.Data(), absoluteRotation); // This is the inverse of RelativeAnglesToAbsoluteAngles. See the comment there for details. quat_t inverseTorsoOrientation; QuatCopy(torsoRotation, inverseTorsoOrientation); QuatInverse(inverseTorsoOrientation); QuatMultiply(inverseTorsoOrientation, absoluteRotation, relativeRotation); QuatToAngles(relativeRotation, relativeAngles); /*turretLogger.Debug("AbsoluteAnglesToRelativeAngles: %s → %s. Torso angles: %s.", Utility::Print(absoluteAngles), Utility::Print(Vec3::Load(relativeAngles)), TorsoAngles() );*/ return Vec3::Load(relativeAngles); }
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() ); }
int BotTacticalSpotsCache::FindNearbyEntities( const Vec3 &origin, float radius, int **entNums ) { if( const auto *nearbyEntitiesCacheEntry = nearbyEntitiesCache.TryGetCachedEntities( origin, radius ) ) { *entNums = (int *)nearbyEntitiesCacheEntry->entNums; return nearbyEntitiesCacheEntry->numEntities; } auto *nearbyEntitiesCacheEntry = nearbyEntitiesCache.Alloc(); if( !nearbyEntitiesCacheEntry ) { return 0; } VectorCopy( origin.Data(), nearbyEntitiesCacheEntry->botOrigin ); nearbyEntitiesCacheEntry->radius = radius; constexpr int maxCachedEntities = NearbyEntitiesCache::MAX_CACHED_NEARBY_ENTITIES; // Find more than maxCachedEntities entities in radius (most entities will usually be filtered out) constexpr int maxRadiusEntities = 2 * maxCachedEntities; int radiusEntNums[maxRadiusEntities]; // Note that this value might be greater than maxRadiusEntities (an actual number of entities is returned) int numRadiusEntities = GClip_FindInRadius( const_cast<float *>( origin.Data() ), radius, radiusEntNums, maxRadiusEntities ); int numEntities = 0; // Copy to locals for faster access (a compiler might be paranoid about aliasing) edict_t *gameEdicts = game.edicts; int *triggerEntNums = nearbyEntitiesCacheEntry->entNums; if( numRadiusEntities <= maxCachedEntities ) { // In this case we can avoid buffer capacity checks on each step for( int i = 0, end = std::min( numRadiusEntities, maxRadiusEntities ); i < end; ++i ) { edict_t *ent = gameEdicts + radiusEntNums[i]; if( !ent->r.inuse ) { continue; } if( !ent->classname ) { continue; } triggerEntNums[numEntities++] = radiusEntNums[i]; } } else { for( int i = 0, end = std::min( numRadiusEntities, maxRadiusEntities ); i < end; ++i ) { edict_t *ent = game.edicts + radiusEntNums[i]; if( !ent->r.inuse ) { continue; } if( !ent->classname ) { continue; } triggerEntNums[numEntities++] = radiusEntNums[i]; if( numEntities == maxCachedEntities ) { break; } } } nearbyEntitiesCacheEntry->numEntities = numEntities; *entNums = (int *)nearbyEntitiesCacheEntry->entNums; return numEntities; }
Vec3 TurretComponent::DirectionToAbsoluteAngles(const Vec3 direction) const { vec3_t absoluteAngles; vectoangles(direction.Data(), absoluteAngles); return Vec3::Load(absoluteAngles); }
Vec3 TurretComponent::AbsoluteAnglesToDirection(const Vec3 absoluteAngles) const { vec3_t direction; AngleVectors(absoluteAngles.Data(), direction, nullptr, nullptr); return Vec3::Load(direction); }
bool TurretComponent::MoveHeadToTarget(int timeDelta) { // Note that a timeDelta of zero may happen on a first thinker execution. // We do not return in that case since we don't know the return value yet. ASSERT_GE(timeDelta, 0); float timeMod = (float)timeDelta / 1000.0f; // Compute maximum angle changes for this execution. Vec3 maxAngleChange; maxAngleChange[PITCH] = timeMod * PITCH_SPEED; maxAngleChange[YAW] = timeMod * YAW_SPEED; maxAngleChange[ROLL] = 0.0f; // Compute angles to target, relative to the turret's base. Vec3 relativeAnglesToTarget = DirectionToRelativeAngles(directionToTarget); // Compute difference between angles to target and current angles. Vec3 deltaAngles; AnglesSubtract(relativeAnglesToTarget.Data(), relativeAimAngles.Data(), deltaAngles.Data()); // Stop if there is nothing to do. if (Math::Length(deltaAngles) < 0.1f) { return true; } bool targetReached = true; Vec3 oldRelativeAimAngles = relativeAimAngles; // Adjust aim angles towards target angles. for (int angle = 0; angle < 3; angle++) { if (angle == ROLL) continue; if (fabs(deltaAngles[angle]) > maxAngleChange[angle]) { relativeAimAngles[angle] += (deltaAngles[angle] < 0.0f) ? -maxAngleChange[angle] : maxAngleChange[angle]; targetReached = false; } else { relativeAimAngles[angle] = relativeAnglesToTarget[angle]; } } // Respect pitch limits. if (relativeAimAngles[PITCH] > PITCH_CAP) { relativeAimAngles[PITCH] = PITCH_CAP; targetReached = false; } if (Math::DistanceSq(oldRelativeAimAngles, relativeAimAngles) > 0.0f) { turretLogger.Debug( "Aiming. Elapsed: %d ms. Delta: %.2f. Max: %.2f. Old: %s. New: %s. Reached: %s.", timeDelta, deltaAngles, maxAngleChange, oldRelativeAimAngles, relativeAimAngles, targetReached ); } // TODO: Move gentity_t.buildableAim to BuildableComponent. Vec3 absoluteAimAngles = RelativeAnglesToAbsoluteAngles(relativeAimAngles); absoluteAimAngles.Store(entity.oldEnt->buildableAim); return targetReached; }
inline bool BotTacticalSpotsCache::BotHasAlmostSameOrigin( const Vec3 &unpackedOrigin ) const { constexpr float squareDistanceError = WorldState::OriginVar::MAX_ROUNDING_SQUARE_DISTANCE_ERROR; return DistanceSquared( self->s.origin, unpackedOrigin.Data() ) < squareDistanceError; }