fix: low-skilled bots aiming overflows on ladders (ref #543)

nav: ignore first collision if we are in ladder node (by @commandcobra7).
bot: backported csbot function to check is enemy behind smoke.
bot: added ``yb_smoke_grenade_checks`` to control which method to use (2-csbot, 1-podbot, 0-disabled).
This commit is contained in:
jeefo 2024-03-31 23:23:01 +03:00
commit f9bae83466
No known key found for this signature in database
GPG key ID: 927BCA0779BEA8ED
8 changed files with 180 additions and 11 deletions

@ -1 +1 @@
Subproject commit 2d7a426b69102e3c6c07f2503240ed21f69c1143 Subproject commit 3596ac42ccc21d93d7fe2a937870d76e944f7f07

View file

@ -126,6 +126,7 @@ public:
bool isTeamStacked (int team); bool isTeamStacked (int team);
bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned); bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned);
bool hasCustomCSDMSpawnEntities (); bool hasCustomCSDMSpawnEntities ();
bool isLineBlockedBySmoke (const Vector &from, const Vector &to, float grenadeBloat = 1.0f);
public: public:
const Array <edict_t *> &getActiveGrenades () { const Array <edict_t *> &getActiveGrenades () {

View file

@ -427,6 +427,7 @@ private:
bool isEnemyInSight (Vector &endPos); bool isEnemyInSight (Vector &endPos);
bool isEnemyNoticeable (float range); bool isEnemyNoticeable (float range);
bool isCreature (); bool isCreature ();
bool isOnLadderPath ();
void doPlayerAvoidance (const Vector &normal); void doPlayerAvoidance (const Vector &normal);
void selectCampButtons (int index); void selectCampButtons (int index);
@ -877,6 +878,7 @@ extern ConVar cv_grenadier_mode;
extern ConVar cv_ignore_enemies_after_spawn_time; extern ConVar cv_ignore_enemies_after_spawn_time;
extern ConVar cv_camping_time_min; extern ConVar cv_camping_time_min;
extern ConVar cv_camping_time_max; extern ConVar cv_camping_time_max;
extern ConVar cv_smoke_grenade_checks;
extern ConVar mp_freezetime; extern ConVar mp_freezetime;
extern ConVar mp_roundtime; extern ConVar mp_roundtime;

View file

@ -38,6 +38,7 @@ ConVar cv_pickup_custom_items ("pickup_custom_items", "0", "Allows or disallows
ConVar cv_pickup_ammo_and_kits ("pickup_ammo_and_kits", "0", "Allows bots pickup mod items like ammo, health kits and suits."); ConVar cv_pickup_ammo_and_kits ("pickup_ammo_and_kits", "0", "Allows bots pickup mod items like ammo, health kits and suits.");
ConVar cv_pickup_best ("pickup_best", "1", "Allows or disallows bots to pickup best weapons."); ConVar cv_pickup_best ("pickup_best", "1", "Allows or disallows bots to pickup best weapons.");
ConVar cv_ignore_objectives ("ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages."); ConVar cv_ignore_objectives ("ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages.");
ConVar cv_smoke_grenade_checks ("smoke_grenade_checks", "2", "Affect bot's vision by smoke clouds.", true, 0.0f, 2.0f);
// game console variables // game console variables
ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef);
@ -131,7 +132,7 @@ void Bot::avoidGrenades () {
} }
} }
} }
else if ((pent->v.flags & FL_ONGROUND) && model == kSmokeModelName) { else if (cv_smoke_grenade_checks.int_ () == 1 && (pent->v.flags & FL_ONGROUND) && model == kSmokeModelName) {
if (isInFOV (pent->v.origin - getEyesPos ()) < pev->fov / 3.0f) { if (isInFOV (pent->v.origin - getEyesPos ()) < pev->fov / 3.0f) {
const auto &entOrigin = game.getEntityOrigin (pent); const auto &entOrigin = game.getEntityOrigin (pent);
const auto &betweenUs = (entOrigin - pev->origin).normalize_apx (); const auto &betweenUs = (entOrigin - pev->origin).normalize_apx ();

View file

@ -215,6 +215,13 @@ bool Bot::checkBodyParts (edict_t *target) {
} }
bool Bot::seesEnemy (edict_t *player) { bool Bot::seesEnemy (edict_t *player) {
auto isBehindSmokeClouds = [&] (const Vector &pos) {
if (cv_smoke_grenade_checks.int_ () == 2) {
return bots.isLineBlockedBySmoke (getEyesPos (), pos);
}
return false;
};
if (game.isNullEntity (player)) { if (game.isNullEntity (player)) {
return false; return false;
} }
@ -224,7 +231,11 @@ bool Bot::seesEnemy (edict_t *player) {
ignoreFieldOfView = true; ignoreFieldOfView = true;
} }
if ((ignoreFieldOfView || isInViewCone (player->v.origin)) && frustum.check (m_viewFrustum, player) && checkBodyParts (player)) { if ((ignoreFieldOfView || isInViewCone (player->v.origin))
&& frustum.check (m_viewFrustum, player)
&& !isBehindSmokeClouds (player->v.origin)
&& checkBodyParts (player)) {
m_seeEnemyTime = game.time (); m_seeEnemyTime = game.time ();
m_lastEnemy = player; m_lastEnemy = player;
m_lastEnemyOrigin = m_enemyOrigin; m_lastEnemyOrigin = m_enemyOrigin;
@ -1379,7 +1390,7 @@ void Bot::attackMovement () {
const bool alreadyDucking = m_duckTime > game.time () || isDucking (); const bool alreadyDucking = m_duckTime > game.time () || isDucking ();
if (alreadyDucking) { if (alreadyDucking) {
m_duckTime = game.time () + m_frameInterval * 2.0f; m_duckTime = game.time () + m_frameInterval * 3.0f;
} }
else if ((distance > 768.0f && hasPrimaryWeapon ()) else if ((distance > 768.0f && hasPrimaryWeapon ())
&& (m_enemyParts & (Visibility::Head | Visibility::Body)) && (m_enemyParts & (Visibility::Head | Visibility::Body))
@ -1390,7 +1401,7 @@ void Bot::attackMovement () {
if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch)
&& vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) { && vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) {
m_duckTime = game.time () + m_frameInterval * 2.0f; m_duckTime = game.time () + m_frameInterval * 3.0f;
} }
} }
m_moveSpeed = 0.0f; m_moveSpeed = 0.0f;

View file

@ -2149,3 +2149,118 @@ void BotThreadWorker::startup (int workers) {
// start up the worker // start up the worker
m_botWorker.startup (static_cast <size_t> (requestedThreads)); m_botWorker.startup (static_cast <size_t> (requestedThreads));
} }
bool BotManager::isLineBlockedBySmoke (const Vector &from, const Vector &to, float grenadeBloat) {
if (m_activeGrenades.empty ()) {
return false;
}
constexpr auto kSmokeGrenadeRadius = 115;
// distance along line of sight covered by smoke
float totalSmokedLength = 0.0f;
Vector sightDir = to - from;
const float sightLength = sightDir.normalizeInPlace ();
for (auto pent : m_activeGrenades) {
if (game.isNullEntity (pent)) {
continue;
}
// need drawn models
if (pent->v.effects & EF_NODRAW) {
continue;
}
// smoke must be on a ground
if (!(pent->v.flags & FL_ONGROUND)) {
continue;
}
// must be a smoke grenade
if (!util.isModel (pent, kSmokeModelName)) {
continue;
}
const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius) * cr::sqrf (grenadeBloat);
const Vector &smokeOrigin = game.getEntityOrigin (pent);
Vector toGrenade = smokeOrigin - from;
float alongDist = toGrenade | sightDir;
// compute closest point to grenade along line of sight ray
Vector close;
// constrain closest point to line segment
if (alongDist < 0.0f) {
close = from;
}
else if (alongDist >= sightLength) {
close = to;
}
else {
close = from + sightDir * alongDist;
}
// if closest point is within smoke radius, the line overlaps the smoke cloud
Vector toClose = close - smokeOrigin;
float lengthSq = toClose.lengthSq ();
if (lengthSq < smokeRadiusSq) {
// some portion of the ray intersects the cloud
const float fromSq = toGrenade.lengthSq ();
const float toSq = (smokeOrigin - to).lengthSq ();
if (fromSq < smokeRadiusSq) {
if (toSq < smokeRadiusSq) {
// both 'from' and 'to' lie within the cloud
// entire length is smoked
totalSmokedLength += (to - from).length ();
}
else {
// 'from' is inside the cloud, 'to' is outside
// compute half of total smoked length as if ray crosses entire cloud chord
float halfSmokedLength = cr::sqrtf (smokeRadiusSq - lengthSq);
if (alongDist > 0.0f) {
// ray goes thru 'close'
totalSmokedLength += halfSmokedLength + (close - from).length ();
}
else {
// ray starts after 'close'
totalSmokedLength += halfSmokedLength - (close - from).length ();
}
}
}
else if (toSq < smokeRadiusSq) {
// 'from' is outside the cloud, 'to' is inside
// compute half of total smoked length as if ray crosses entire cloud chord
const float halfSmokedLength = cr::sqrtf (smokeRadiusSq - lengthSq);
Vector v = to - smokeOrigin;
if ((v | sightDir) > 0.0f) {
// ray goes thru 'close'
totalSmokedLength += halfSmokedLength + (close - to).length ();
}
else {
// ray ends before 'close'
totalSmokedLength += halfSmokedLength - (close - to).length ();
}
}
else {
// 'from' and 'to' lie outside of the cloud - the line of sight completely crosses it
// determine the length of the chord that crosses the cloud
const float smokedLength = 2.0f * cr::sqrtf (smokeRadiusSq - lengthSq);
totalSmokedLength += smokedLength;
}
}
}
// define how much smoke a bot can see thru
const float maxSmokedLength = 0.7f * kSmokeGrenadeRadius;
// return true if the total length of smoke-covered line-of-sight is too much
return (totalSmokedLength > maxSmokedLength);
}

View file

@ -555,7 +555,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
// not stuck yet // not stuck yet
else { else {
// test if there's something ahead blocking the way // test if there's something ahead blocking the way
if (!isOnLadder () && isBlockedForward (dirNormal, &tr)) { if (!isOnLadderPath () && !isOnLadder () && isBlockedForward (dirNormal, &tr)) {
if (cr::fzero (m_firstCollideTime)) { if (cr::fzero (m_firstCollideTime)) {
m_firstCollideTime = game.time () + 0.2f; m_firstCollideTime = game.time () + 0.2f;
} }
@ -3151,6 +3151,15 @@ bool Bot::isReachableNode (int index) {
return false; return false;
} }
bool Bot::isOnLadderPath () {
const auto prevNodeIndex = m_previousNodes[0];
// bot entered ladder path
return (m_pathFlags & NodeFlag::Ladder)
&& graph.exists (prevNodeIndex)
&& (graph[prevNodeIndex].flags & NodeFlag::Ladder);
}
void Bot::findShortestPath (int srcIndex, int destIndex) { void Bot::findShortestPath (int srcIndex, int destIndex) {
// this function finds the shortest path from source index to destination index // this function finds the shortest path from source index to destination index

View file

@ -226,9 +226,12 @@ void Bot::setAimDirection () {
if (onLadder && m_pathWalk.hasNext ()) { if (onLadder && m_pathWalk.hasNext ()) {
const auto &nextPath = graph[m_pathWalk.next ()]; const auto &nextPath = graph[m_pathWalk.next ()];
if ((nextPath.flags & NodeFlag::Ladder) && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (64.0f) && nextPath.origin.z > m_pathOrigin.z + 30.0f) { if ((nextPath.flags & NodeFlag::Ladder) && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (128.0f) && nextPath.origin.z > m_pathOrigin.z + 26.0f) {
m_lookAt = nextPath.origin; m_lookAt = nextPath.origin;
} }
else {
m_lookAt = m_destOrigin;
}
} }
// try to look at last victim for a little, maybe there's some one else // try to look at last victim for a little, maybe there's some one else
@ -399,13 +402,19 @@ void Bot::updateLookAngles () {
updateBodyAngles (); updateBodyAngles ();
return; return;
} }
const float aimSkill = cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 25.0f; float aimSkill = cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 25.0f;
// do not slowdown while on ladder
if (isOnLadderPath () || isOnLadder ()) {
aimSkill = 100.0f;
}
const bool importantAimFlags = (m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade));
float accelerate = aimSkill * 30.0f; float accelerate = aimSkill * 30.0f;
float stiffness = aimSkill * 2.0f; float stiffness = aimSkill * 2.0f;
float damping = aimSkill * 0.25f; float damping = aimSkill * 0.25f;
if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_wantsToFire) && m_difficulty > Difficulty::Normal) { if ((importantAimFlags || m_wantsToFire) && m_difficulty > Difficulty::Normal) {
if (m_difficulty == Difficulty::Expert) { if (m_difficulty == Difficulty::Expert) {
accelerate += 600.0f; accelerate += 600.0f;
} }
@ -414,8 +423,29 @@ void Bot::updateLookAngles () {
} }
m_idealAngles = pev->v_angle; m_idealAngles = pev->v_angle;
const float angleDiffPitch = cr::anglesDifference (direction.x, m_idealAngles.x); float angleDiffPitch = cr::anglesDifference (direction.x, m_idealAngles.x);
const float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y);
// prevent reverse facing angles when navigating normally
if (m_difficulty < Difficulty::Easy && m_moveToGoal && !importantAimFlags && !m_pathOrigin.empty ()) {
const float forward = (m_pathOrigin - pev->origin).yaw ();
if (!cr::fzero (forward)) {
const float current = cr::wrapAngle (pev->v_angle.y - forward);
const float target = cr::wrapAngle (direction.y - forward);
if (current * target < 0.0f) {
if (cr::abs (current - target) >= 180.0f) {
if (angleDiffYaw > 0.0f) {
angleDiffYaw -= 360.0f;
}
else {
angleDiffYaw += 360.0f;
}
}
}
}
}
if (cr::abs (angleDiffYaw) < 1.0f) { if (cr::abs (angleDiffYaw) < 1.0f) {
m_lookYawVel = 0.0f; m_lookYawVel = 0.0f;