nav: fall controls are not good. (#656)

nav: fall controls are not good.
nav: increased behavior rate in finding the best goal.
nav: avoiding the door after hitting it.
combat: if the enemy is using a sniper rifle, move.
combat: enemy group functionality has been improved.
combat: if we are doing a camping task, do not switch to a knife at close range.
aim: slight changes to enemy prediction.

---------

Co-authored-by: jeefo <dmitry@jeefo.net>
This commit is contained in:
commandcobra7 2025-01-09 21:53:16 +03:00 committed by GitHub
commit e717710bd1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 245 additions and 211 deletions

View file

@ -128,7 +128,7 @@ public:
bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned); bool kickRandom (bool decQuota = true, Team fromTeam = Team::Unassigned);
bool balancedKickRandom (bool decQuota); bool balancedKickRandom (bool decQuota);
bool hasCustomCSDMSpawnEntities (); bool hasCustomCSDMSpawnEntities ();
bool isLineBlockedBySmoke (const Vector &from, const Vector &to, float grenadeBloat = 1.0f); bool isLineBlockedBySmoke (const Vector &from, const Vector &to);
bool isFrameSkipDisabled (); bool isFrameSkipDisabled ();
public: public:

View file

@ -237,6 +237,7 @@ private:
float m_prevSpeed {}; // speed some frames before float m_prevSpeed {}; // speed some frames before
float m_prevVelocity {}; // velocity some frames before float m_prevVelocity {}; // velocity some frames before
float m_timeDoorOpen {}; // time to next door open check float m_timeDoorOpen {}; // time to next door open check
float m_timeHitDoor {}; // specific time after hitting the door
float m_lastChatTime {}; // time bot last chatted float m_lastChatTime {}; // time bot last chatted
float m_timeLogoSpray {}; // time bot last spray logo float m_timeLogoSpray {}; // time bot last spray logo
float m_knifeAttackTime {}; // time to rush with knife (at the beginning of the round) float m_knifeAttackTime {}; // time to rush with knife (at the beginning of the round)
@ -284,7 +285,6 @@ private:
float m_playServerTime {}; // time bot spent in the game float m_playServerTime {}; // time bot spent in the game
float m_changeViewTime {}; // timestamp to change look at while at freezetime float m_changeViewTime {}; // timestamp to change look at while at freezetime
float m_breakableTime {}; // breakable acquired time float m_breakableTime {}; // breakable acquired time
float m_stuckTimestamp {}; // last time was stuck
float m_timeDebugUpdateTime {}; // time to update last debug timestamp float m_timeDebugUpdateTime {}; // time to update last debug timestamp
float m_lastVictimTime {}; // time when bot killed an enemy float m_lastVictimTime {}; // time when bot killed an enemy
float m_killsInterval {}; // interval between kills float m_killsInterval {}; // interval between kills
@ -437,7 +437,7 @@ private:
bool isEnemyNoTarget (edict_t *enemy); bool isEnemyNoTarget (edict_t *enemy);
bool isEnemyInDarkArea (edict_t *enemy); bool isEnemyInDarkArea (edict_t *enemy);
bool isFriendInLineOfFire (float distance); bool isFriendInLineOfFire (float distance);
bool isGroupOfEnemies (const Vector &location, int numEnemies = 1, float radius = 256.0f); bool isGroupOfEnemies (const Vector &location);
bool isPenetrableObstacle (const Vector &dest); bool isPenetrableObstacle (const Vector &dest);
bool isPenetrableObstacle1 (const Vector &dest, int penetratePower); bool isPenetrableObstacle1 (const Vector &dest, int penetratePower);
bool isPenetrableObstacle2 (const Vector &dest, int penetratePower); bool isPenetrableObstacle2 (const Vector &dest, int penetratePower);
@ -831,11 +831,6 @@ private:
return m_weaponType == WeaponType::Sniper; return m_weaponType == WeaponType::Sniper;
} }
// returns true if bot is using a sniper rifle (awp)
bool usesSniperAWP () const {
return m_currentWeapon == Weapon::AWP;
}
// returns true if bot is using a rifle // returns true if bot is using a rifle
bool usesRifle () const { bool usesRifle () const {
return usesZoomableRifle () || m_weaponType == WeaponType::Rifle; return usesZoomableRifle () || m_weaponType == WeaponType::Rifle;

View file

@ -85,7 +85,7 @@ void Bot::avoidGrenades () {
const auto &activeGrenades = bots.getActiveGrenades (); const auto &activeGrenades = bots.getActiveGrenades ();
// find all grenades on the map // find all grenades on the map
for (auto pent : activeGrenades) { for (const auto &pent : activeGrenades) {
if (pent->v.effects & EF_NODRAW) { if (pent->v.effects & EF_NODRAW) {
continue; continue;
} }
@ -96,10 +96,7 @@ void Bot::avoidGrenades () {
} }
auto model = pent->v.model.str (9); auto model = pent->v.model.str (9);
if (m_preventFlashing < game.time () if (m_preventFlashing < game.time () && model == kFlashbangModelName) {
&& cv_whose_your_daddy
&& model == kFlashbangModelName) {
// don't look at flash bang // don't look at flash bang
if (!(m_states & Sense::SeeingEnemy)) { if (!(m_states & Sense::SeeingEnemy)) {
m_lookAt.y = cr::wrapAngle ((game.getEntityOrigin (pent) - getEyesPos ()).angles ().y + 180.0f); m_lookAt.y = cr::wrapAngle ((game.getEntityOrigin (pent) - getEyesPos ()).angles ().y + 180.0f);
@ -180,9 +177,7 @@ void Bot::checkBreakablesAround () {
|| !game.hasBreakables () || !game.hasBreakables ()
|| m_seeEnemyTime + 4.0f > game.time () || m_seeEnemyTime + 4.0f > game.time ()
|| !game.isNullEntity (m_enemy) || !game.isNullEntity (m_enemy)
|| !hasPrimaryWeapon () || !hasPrimaryWeapon ()) {
|| (m_aimFlags & (AimFlags::Danger | AimFlags::PredictPath | AimFlags::Enemy))
|| getCurrentTaskId () != Task::Normal) {
return; return;
} }
const auto radius = cv_object_destroy_radius.as <float> (); const auto radius = cv_object_destroy_radius.as <float> ();
@ -349,13 +344,13 @@ void Bot::updatePickups () {
bool itemExists = false; bool itemExists = false;
auto pickupItem = m_pickupItem; auto pickupItem = m_pickupItem;
for (auto &ent : interesting) { for (const auto &ent : interesting) {
// in the periods of updating interesting entities we can get fake ones, that already were picked up, so double check if drawn // in the periods of updating interesting entities we can get fake ones, that already were picked up, so double check if drawn
if (ent->v.effects & EF_NODRAW) { if (ent->v.effects & EF_NODRAW) {
continue; continue;
} }
const Vector &origin = game.getEntityOrigin (ent); const auto &origin = game.getEntityOrigin (ent);
// too far from us ? // too far from us ?
if (pev->origin.distanceSq (origin) > radiusSq) { if (pev->origin.distanceSq (origin) > radiusSq) {
@ -595,7 +590,7 @@ void Bot::updatePickups () {
m_defendedBomb = true; m_defendedBomb = true;
const int index = findDefendNode (origin); const int index = findDefendNode (origin);
const Path &path = graph[index]; const auto &path = graph[index];
const float bombTimer = mp_c4timer.as <float> (); const float bombTimer = mp_c4timer.as <float> ();
const float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); const float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
@ -638,7 +633,7 @@ void Bot::updatePickups () {
// don't steal hostage from human teammate (hack) // don't steal hostage from human teammate (hack)
if (allowPickup) { if (allowPickup) {
for (auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) &&
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) { client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) {
allowPickup = false; allowPickup = false;
@ -805,9 +800,9 @@ Vector Bot::getCampDirection (const Vector &dest) {
float nearestDistance = kInfiniteDistance; float nearestDistance = kInfiniteDistance;
int lookAtNode = kInvalidNodeIndex; int lookAtNode = kInvalidNodeIndex;
const Path &path = graph[tempIndex]; const auto &path = graph[tempIndex];
for (auto &link : path.links) { for (const auto &link : path.links) {
if (link.index == kInvalidNodeIndex) { if (link.index == kInvalidNodeIndex) {
continue; continue;
} }
@ -1658,7 +1653,8 @@ void Bot::overrideConditions () {
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
// check if we need to escape from bomb // check if we need to escape from bomb
if (game.mapIs (MapFlags::Demolition) if ((tid == Task::Normal || tid == Task::MoveToPosition)
&& game.mapIs (MapFlags::Demolition)
&& bots.isBombPlanted () && bots.isBombPlanted ()
&& m_isAlive && m_isAlive
&& tid != Task::EscapeFromBomb && tid != Task::EscapeFromBomb
@ -1717,8 +1713,8 @@ void Bot::overrideConditions () {
} }
// special handling for reloading // special handling for reloading
if (!bots.isRoundOver () && if (!bots.isRoundOver ()
tid == Task::Normal && tid == Task::Normal
&& m_reloadState != Reload::None && m_reloadState != Reload::None
&& m_isReloading && m_isReloading
&& !isDucking () && !isDucking ()
@ -1775,7 +1771,7 @@ void Bot::syncUpdatePredictedIndex () {
const float distToBotSq = botOrigin.distanceSq (graph[index].origin); const float distToBotSq = botOrigin.distanceSq (graph[index].origin);
if (vistab.visible (currentNodeIndex, index) && distToBotSq > 128.0f && distToBotSq < cr::sqrf (768.0f)) { if (vistab.visible (currentNodeIndex, index) && distToBotSq < cr::sqrf (2048.0f)) {
bestIndex = index; bestIndex = index;
return false; return false;
} }
@ -1812,7 +1808,8 @@ void Bot::refreshEnemyPredict () {
&& distanceToLastEnemySq < cr::sqrf (256.0f) && distanceToLastEnemySq < cr::sqrf (256.0f)
&& m_shootTime + 1.5f > game.time (); && m_shootTime + 1.5f > game.time ();
if (!(m_aimFlags & (AimFlags::Enemy | AimFlags::PredictPath)) && !denyLastEnemy && seesEntity (m_lastEnemyOrigin, true)) { if (!(m_aimFlags & (AimFlags::Enemy | AimFlags::PredictPath | AimFlags::Danger))
&& !denyLastEnemy && seesEntity (m_lastEnemyOrigin, true)) {
m_aimFlags |= AimFlags::LastEnemy; m_aimFlags |= AimFlags::LastEnemy;
} }
} }
@ -1993,8 +1990,8 @@ void Bot::filterTasks () {
// increase/decrease fear/aggression if bot uses a sniping weapon to be more careful // increase/decrease fear/aggression if bot uses a sniping weapon to be more careful
if (usesSniper ()) { if (usesSniper ()) {
tempFear = tempFear * 1.2f; tempFear = tempFear * 1.5f;
tempAgression = tempAgression * 0.6f; tempAgression = tempAgression * 0.5f;
} }
auto &filter = bots.getFilters (); auto &filter = bots.getFilters ();
@ -2675,7 +2672,7 @@ void Bot::checkRadioQueue () {
switch (getCurrentTaskId ()) { switch (getCurrentTaskId ()) {
case Task::Normal: case Task::Normal:
if (getTask ()->data != kInvalidNodeIndex && rg.chance (70)) { if (getTask ()->data != kInvalidNodeIndex && rg.chance (70)) {
const Path &path = graph[getTask ()->data]; const auto &path = graph[getTask ()->data];
if (path.flags & NodeFlag::Goal) { if (path.flags & NodeFlag::Goal) {
if (m_hasC4) { if (m_hasC4) {
@ -2787,7 +2784,7 @@ void Bot::checkRadioQueue () {
int bombPoint = kInvalidNodeIndex; int bombPoint = kInvalidNodeIndex;
// find nearest bomb node to player // find nearest bomb node to player
for (auto &point : graph.m_goalPoints) { for (const auto &point : graph.m_goalPoints) {
distanceSq = graph[point].origin.distanceSq (m_radioEntity->v.origin); distanceSq = graph[point].origin.distanceSq (m_radioEntity->v.origin);
if (distanceSq < nearestDistanceSq) { if (distanceSq < nearestDistanceSq) {
@ -2932,7 +2929,7 @@ void Bot::frame () {
} }
if (bots.isBombPlanted () && m_team == Team::CT && m_isAlive) { if (bots.isBombPlanted () && m_team == Team::CT && m_isAlive) {
const Vector &bombPosition = graph.getBombOrigin (); const auto &bombPosition = graph.getBombOrigin ();
if (!m_hasProgressBar if (!m_hasProgressBar
&& getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::EscapeFromBomb
@ -3057,9 +3054,9 @@ void Bot::logicDuringFreezetime () {
return; return;
} }
if (rg.chance (15) && m_jumpTime + rg (1.0f, 2.0f) < game.time ()) { if (rg.chance (15) && m_jumpTime < game.time ()) {
pev->button |= IN_JUMP; pev->button |= IN_JUMP;
m_jumpTime = game.time (); m_jumpTime = game.time () + rg (1.0f, 2.0f);
} }
static Array <edict_t *> players {}; static Array <edict_t *> players {};
players.clear (); players.clear ();
@ -3189,7 +3186,7 @@ void Bot::checkSpawnConditions () {
void Bot::logic () { void Bot::logic () {
// this function gets called each frame and is the core of all bot ai. from here all other subroutines are called // this function gets called each frame and is the core of all bot ai. from here all other subroutines are called
float movedDistance = 2.0f; // length of different vector (distance bot moved) float movedDistance = 4.0f; // length of different vector (distance bot moved)
resetMovement (); resetMovement ();
@ -3251,7 +3248,7 @@ void Bot::logic () {
else if (!hasFriendNearby else if (!hasFriendNearby
&& rg.chance (50) && rg.chance (50)
&& game.getTeam (m_enemy) != m_team && game.getTeam (m_enemy) != m_team
&& isGroupOfEnemies (m_enemy->v.origin, 2, 384.0f)) { && isGroupOfEnemies (m_enemy->v.origin)) {
pushChatterMessage (Chatter::ScaredEmotion); pushChatterMessage (Chatter::ScaredEmotion);
} }
@ -3303,8 +3300,8 @@ void Bot::logic () {
setIdealReactionTimers (); setIdealReactionTimers ();
// calculate 2 direction vectors, 1 without the up/down component // calculate 2 direction vectors, 1 without the up/down component
const Vector &dirOld = m_destOrigin - (pev->origin + pev->velocity * m_frameInterval); const auto &dirOld = m_destOrigin - (pev->origin + pev->velocity * m_frameInterval);
const Vector &dirNormal = dirOld.normalize2d_apx (); const auto &dirNormal = dirOld.normalize2d_apx ();
m_moveAngles = dirOld.angles (); m_moveAngles = dirOld.angles ();
m_moveAngles.clampAngles (); m_moveAngles.clampAngles ();
@ -3340,8 +3337,17 @@ void Bot::logic () {
// don't duck to get away faster // don't duck to get away faster
pev->button &= ~IN_DUCK; pev->button &= ~IN_DUCK;
m_moveSpeed = -pev->maxspeed; Vector right {}, forward {};
m_strafeSpeed = pev->maxspeed * static_cast <float> (m_needAvoidGrenade); pev->v_angle.angleVectors (&forward, &right, nullptr);
const auto &front = forward * -pev->maxspeed * 0.2f;
const auto &side = right * pev->maxspeed * static_cast <float> (m_needAvoidGrenade) * 0.2f;
const auto &spot = pev->origin + front + side + pev->velocity * m_frameInterval;
if (!isDeadlyMove (spot)) {
m_moveSpeed = -pev->maxspeed;
m_strafeSpeed = pev->maxspeed * static_cast <float> (m_needAvoidGrenade);
}
} }
// ensure we're not stuck picking something // ensure we're not stuck picking something
@ -3836,7 +3842,7 @@ Vector Bot::isBombAudible () {
if (m_difficulty > Difficulty::Hard) { if (m_difficulty > Difficulty::Hard) {
return graph.getBombOrigin (); return graph.getBombOrigin ();
} }
const Vector &bombOrigin = graph.getBombOrigin (); const auto &bombOrigin = graph.getBombOrigin ();
const float timeElapsed = ((game.time () - bots.getTimeBombPlanted ()) / mp_c4timer.as <float> ()) * 100.0f; const float timeElapsed = ((game.time () - bots.getTimeBombPlanted ()) / mp_c4timer.as <float> ()) * 100.0f;
float desiredRadius = 768.0f; float desiredRadius = 768.0f;
@ -3865,8 +3871,8 @@ Vector Bot::isBombAudible () {
bool Bot::canRunHeavyWeight () { bool Bot::canRunHeavyWeight () {
constexpr auto kInterval = 1.0f / 10.0f; constexpr auto kInterval = 1.0f / 10.0f;
if (m_heavyTimestamp + kInterval < game.time ()) { if (m_heavyTimestamp < game.time ()) {
m_heavyTimestamp = game.time (); m_heavyTimestamp = game.time () + kInterval;
return true; return true;
} }
@ -3981,7 +3987,7 @@ void Bot::updateHearing () {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
// do not hear to other enemies if just tracked old one // do not hear to other enemies if just tracked old one
if (m_timeNextTracking < game.time () && util.isAlive (m_lastEnemy) && m_lastEnemy == m_trackingEdict) { if (m_timeNextTracking < game.time () && m_lastEnemy == m_trackingEdict && util.isAlive (m_lastEnemy)) {
m_hearedEnemy = m_lastEnemy; m_hearedEnemy = m_lastEnemy;
m_lastEnemyOrigin = m_lastEnemy->v.origin; m_lastEnemyOrigin = m_lastEnemy->v.origin;

View file

@ -133,10 +133,11 @@ bool Bot::isEnemyInDarkArea (edict_t *enemy) {
if (!cv_check_darkness && game.isNullEntity (enemy)) { if (!cv_check_darkness && game.isNullEntity (enemy)) {
return false; return false;
} }
const auto &v = enemy->v;
const auto scolor = illum.getSkyColor (); const auto scolor = illum.getSkyColor ();
// check if node near the enemy have a degraded light level // check if node near the enemy have a degraded light level
const auto enemyNodeIndex = graph.getNearest (enemy->v.origin); const auto enemyNodeIndex = graph.getNearest (v.origin);
if (!graph.exists (enemyNodeIndex)) { if (!graph.exists (enemyNodeIndex)) {
return false; return false;
@ -149,9 +150,9 @@ bool Bot::isEnemyInDarkArea (edict_t *enemy) {
} }
bool enemySemiTransparent = false; bool enemySemiTransparent = false;
const bool enemyHasGun = (enemy->v.weapons & kPrimaryWeaponMask) || (enemy->v.weapons & kSecondaryWeaponMask); const bool enemyHasGun = (v.weapons & kPrimaryWeaponMask) || (v.weapons & kSecondaryWeaponMask);
const bool enemyHasFlashlightEnabled = !!(enemy->v.effects & EF_DIMLIGHT); const bool enemyIsAttacking = (v.button & IN_ATTACK) || (v.oldbuttons & IN_ATTACK);
const bool enemyIsAttacking = !!(enemy->v.oldbuttons & IN_ATTACK); const bool enemyHasFlashlightEnabled = !!(v.effects & EF_DIMLIGHT);
if (!m_usesNVG && ((llevel < 3.0f && scolor > 50.0f) || (llevel < 25.0f && scolor <= 50.0f)) if (!m_usesNVG && ((llevel < 3.0f && scolor > 50.0f) || (llevel < 25.0f && scolor <= 50.0f))
&& !enemyHasFlashlightEnabled && (!enemyIsAttacking || !enemyHasGun)) { && !enemyHasFlashlightEnabled && (!enemyIsAttacking || !enemyHasGun)) {
@ -163,7 +164,7 @@ bool Bot::isEnemyInDarkArea (edict_t *enemy) {
enemySemiTransparent = true; enemySemiTransparent = true;
} }
TraceResult result {}; TraceResult result {};
game.testLine (getEyesPos (), enemy->v.origin, m_isCreature ? TraceIgnore::None : TraceIgnore::Everything, ent (), &result); game.testLine (getEyesPos (), v.origin, m_isCreature ? TraceIgnore::None : TraceIgnore::Everything, ent (), &result);
return (result.flFraction <= 1.0f && result.pHit == enemy && (m_usesNVG || !enemySemiTransparent)); return (result.flFraction <= 1.0f && result.pHit == enemy && (m_usesNVG || !enemySemiTransparent));
} }
@ -193,7 +194,7 @@ bool Bot::checkBodyPartsWithOffsets (edict_t *target) {
auto self = pev->pContainingEntity; auto self = pev->pContainingEntity;
// creatures can't hurt behind anything // creatures can't hurt behind anything
const auto ignoreFlags = m_isCreature ? TraceIgnore::None : (cv_aim_trace_consider_glass ?TraceIgnore::Monsters : TraceIgnore::Everything); const auto ignoreFlags = m_isCreature ? TraceIgnore::None : (cv_aim_trace_consider_glass ? TraceIgnore::Monsters : TraceIgnore::Everything);
const auto hitsTarget = [&] () -> bool { const auto hitsTarget = [&] () -> bool {
return result.flFraction >= 1.0f || result.pHit == target; return result.flFraction >= 1.0f || result.pHit == target;
@ -390,7 +391,7 @@ bool Bot::lookupEnemies () {
m_states |= Sense::SuspectEnemy; m_states |= Sense::SuspectEnemy;
const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f const bool denyLastEnemy = pev->velocity.lengthSq2d () > 0.0f
&& pev->origin.distanceSq2d (m_lastEnemyOrigin) < cr::sqrf (256.0f) && m_lastEnemyOrigin.distanceSq (pev->origin) < cr::sqrf (256.0f)
&& m_shootTime + 1.5f > game.time (); && m_shootTime + 1.5f > game.time ();
if (!(m_aimFlags & (AimFlags::Enemy | AimFlags::PredictPath | AimFlags::Danger)) if (!(m_aimFlags & (AimFlags::Enemy | AimFlags::PredictPath | AimFlags::Danger))
@ -1244,10 +1245,9 @@ void Bot::fireWeapons () {
if (cv_stab_close_enemies && m_difficulty >= Difficulty::Normal if (cv_stab_close_enemies && m_difficulty >= Difficulty::Normal
&& m_healthValue > 80.0f && m_healthValue > 80.0f
&& !game.isNullEntity (m_enemy) && !game.isNullEntity (m_enemy)
&& m_healthValue >= m_enemy->v.health
&& distance < 100.0f && distance < 100.0f
&& !isOnLadder () && !isGroupOfEnemies (pev->origin)
&& !isGroupOfEnemies (pev->origin)) { && getCurrentTaskId () != Task::Camp) {
selectWeapons (distance, selectIndex, selectId, choosenWeapon); selectWeapons (distance, selectIndex, selectId, choosenWeapon);
return; return;
@ -1428,13 +1428,14 @@ void Bot::attackMovement () {
approach = 49; approach = 49;
} }
} }
const bool isEnemyCone = isInViewCone (m_enemy->v.origin);
// only take cover when bomb is not planted and enemy can see the bot or the bot is VIP // only take cover when bomb is not planted and enemy can see the bot or the bot is VIP
if (!game.is (GameFlags::CSDM) && !isKnifeMode ()) { if (!game.is (GameFlags::CSDM) && !isKnifeMode ()) {
if ((m_states & Sense::SeeingEnemy) if ((m_states & Sense::SeeingEnemy)
&& approach < 30 && approach < 30
&& !bots.isBombPlanted () && !bots.isBombPlanted ()
&& (isInViewCone (m_enemy->v.origin) || m_isVIP || m_isReloading)) { && (isEnemyCone || m_isVIP || m_isReloading)) {
if (m_retreatTime < game.time ()) { if (m_retreatTime < game.time ()) {
startTask (Task::SeekCover, TaskPri::SeekCover, kInvalidNodeIndex, 0.0f, true); startTask (Task::SeekCover, TaskPri::SeekCover, kInvalidNodeIndex, 0.0f, true);
@ -1467,10 +1468,7 @@ void Bot::attackMovement () {
m_fightStyle = Fight::Strafe; m_fightStyle = Fight::Strafe;
} }
else if (distanceSq < cr::sqrf (1024.0f)) { else if (distanceSq < cr::sqrf (1024.0f)) {
if (isGroupOfEnemies (m_enemy->v.origin)) { if (rand < (usesSubmachine () ? 50 : 30)) {
m_fightStyle = Fight::Strafe;
}
else if (rand < (usesSubmachine () ? 50 : 30)) {
m_fightStyle = Fight::Strafe; m_fightStyle = Fight::Strafe;
} }
else { else {
@ -1478,10 +1476,7 @@ void Bot::attackMovement () {
} }
} }
else { else {
if (isGroupOfEnemies (m_enemy->v.origin)) { if (rand < (usesSubmachine () ? 80 : 90)) {
m_fightStyle = Fight::Strafe;
}
else if (rand < (usesSubmachine () ? 80 : 90)) {
m_fightStyle = Fight::Stay; m_fightStyle = Fight::Stay;
} }
else { else {
@ -1505,7 +1500,12 @@ void Bot::attackMovement () {
// fire hurts friend value here is from previous frame, but acceptable, and saves us alot of cpu cycles // fire hurts friend value here is from previous frame, but acceptable, and saves us alot of cpu cycles
if (approach < 30 || m_fireHurtsFriend || ((usesPistol () || usesShotgun ()) if (approach < 30 || m_fireHurtsFriend || ((usesPistol () || usesShotgun ())
&& distanceSq < cr::sqrf (pistolStrafeDistance) && distanceSq < cr::sqrf (pistolStrafeDistance)
&& isInViewCone (m_enemyOrigin))) { && isEnemyCone)) {
m_fightStyle = Fight::Strafe;
}
const auto enemyWeaponIsSniper = (m_enemy->v.weapons & kSniperWeaponMask);
if (enemyWeaponIsSniper && isEnemyCone) {
m_fightStyle = Fight::Strafe; m_fightStyle = Fight::Strafe;
} }
m_lastFightStyleCheck = game.time () + 3.0f; m_lastFightStyleCheck = game.time () + 3.0f;
@ -1515,7 +1515,7 @@ void Bot::attackMovement () {
m_moveSpeed = -pev->maxspeed; m_moveSpeed = -pev->maxspeed;
} }
if (usesKnife () && isInViewCone (m_enemy->v.origin)) { if (usesKnife () && isEnemyCone) {
m_fightStyle = Fight::Strafe; m_fightStyle = Fight::Strafe;
} }
@ -1624,7 +1624,7 @@ void Bot::attackMovement () {
if (m_difficulty >= Difficulty::Normal && isOnFloor () && m_duckTime < game.time ()) { if (m_difficulty >= Difficulty::Normal && isOnFloor () && m_duckTime < game.time ()) {
if (distanceSq < cr::sqrf (kSprayDistanceX2)) { if (distanceSq < cr::sqrf (kSprayDistanceX2)) {
if (rg (0, 1000) < rg (5, 10) && pev->velocity.length2d () > 150.0f && isInViewCone (m_enemy->v.origin)) { if (rg (0, 1000) < rg (5, 10) && pev->velocity.length2d () > 150.0f && isEnemyCone) {
pev->button |= IN_JUMP; pev->button |= IN_JUMP;
} }
} }
@ -2003,28 +2003,29 @@ void Bot::updateTeamCommands () {
m_timeTeamOrder = game.time () + rg (15.0f, 30.0f); m_timeTeamOrder = game.time () + rg (15.0f, 30.0f);
} }
bool Bot::isGroupOfEnemies (const Vector &location, int numEnemies, float radius) { bool Bot::isGroupOfEnemies (const Vector &location) {
int numPlayers = 0; int numPlayers = 0;
// needs a square radius // needs a square radius
const float radiusSq = cr::sqrf (radius); const float radiusSq = cr::sqrf (768.0f);
// search the world for enemy players... // search the world for enemy players...
for (const auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent ()) { if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team == m_team || client.ent == ent ()) {
continue; continue;
} }
if (client.ent->v.origin.distanceSq (location) < radiusSq) { if (client.ent->v.origin.distanceSq (location) < radiusSq) {
if (client.team == m_team) { if (!seesEntity (client.origin)) {
return false; // don't target our teammates... continue;
}
if (numPlayers++ > numEnemies) {
return true;
} }
++numPlayers;
} }
} }
if (numPlayers < 2) {
return false;
}
return false; return false;
} }
@ -2541,7 +2542,7 @@ bool Bot::isEnemyNoticeable (float range) {
if (isCrouching) { if (isCrouching) {
// crouching and motionless - very tough to notice // crouching and motionless - very tough to notice
closeChance = 80.0f; closeChance = 80.0f;
farChance = 5.0f; // takes about three seconds to notice (50% chance) farChance = 5.0f; // takes about three seconds to notice (50% chance)
} }
// standing // standing
else { else {

View file

@ -1509,6 +1509,7 @@ void Bot::newRound () {
m_lastBreakable = nullptr; m_lastBreakable = nullptr;
m_timeDoorOpen = 0.0f; m_timeDoorOpen = 0.0f;
m_timeHitDoor = 0.0f;
for (auto &fall : m_checkFallPoint) { for (auto &fall : m_checkFallPoint) {
fall.clear (); fall.clear ();
@ -1624,7 +1625,6 @@ void Bot::newRound () {
m_dodgeStrafeDir = Dodge::None; m_dodgeStrafeDir = Dodge::None;
m_fightStyle = Fight::None; m_fightStyle = Fight::None;
m_lastFightStyleCheck = 0.0f; m_lastFightStyleCheck = 0.0f;
m_stuckTimestamp = 0.0f;
m_checkWeaponSwitch = true; m_checkWeaponSwitch = true;
m_checkKnifeSwitch = true; m_checkKnifeSwitch = true;
@ -2209,11 +2209,11 @@ void BotThreadWorker::startup (int workers) {
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) { bool BotManager::isLineBlockedBySmoke (const Vector &from, const Vector &to) {
if (m_activeGrenades.empty ()) { if (m_activeGrenades.empty ()) {
return false; return false;
} }
constexpr auto kSmokeGrenadeRadius = 115; constexpr auto kSmokeGrenadeRadius = 115.0f;
// distance along line of sight covered by smoke // distance along line of sight covered by smoke
float totalSmokedLength = 0.0f; float totalSmokedLength = 0.0f;
@ -2241,7 +2241,7 @@ bool BotManager::isLineBlockedBySmoke (const Vector &from, const Vector &to, flo
continue; continue;
} }
const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius) * cr::sqrf (grenadeBloat); const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius);
const Vector &smokeOrigin = game.getEntityOrigin (pent); const Vector &smokeOrigin = game.getEntityOrigin (pent);
Vector toGrenade = smokeOrigin - from; Vector toGrenade = smokeOrigin - from;

View file

@ -99,6 +99,7 @@ int Bot::findBestGoal () {
} }
return findGoalPost (hasMoreHostagesAround ? GoalTactic::Goal : GoalTactic::RescueHostage, defensiveNodes, offensiveNodes); return findGoalPost (hasMoreHostagesAround ? GoalTactic::Goal : GoalTactic::RescueHostage, defensiveNodes, offensiveNodes);
} }
constexpr float kBehaviorBase = 30.0f;
const auto difficulty = static_cast <float> (m_difficulty); const auto difficulty = static_cast <float> (m_difficulty);
offensive = m_agressionLevel * 100.0f; offensive = m_agressionLevel * 100.0f;
@ -107,27 +108,27 @@ int Bot::findBestGoal () {
if (game.mapIs (MapFlags::Assassination | MapFlags::HostageRescue)) { if (game.mapIs (MapFlags::Assassination | MapFlags::HostageRescue)) {
if (m_team == Team::Terrorist) { if (m_team == Team::Terrorist) {
if (m_personality == Personality::Rusher) { if (m_personality == Personality::Rusher) {
defensive -= 25.0f - difficulty * 0.5f; defensive -= kBehaviorBase - difficulty * 0.5f;
offensive += 25.0f + difficulty * 5.0f; offensive += kBehaviorBase + difficulty * 5.0f;
} }
else if (m_personality == Personality::Normal && rg.chance (40)) { else if (m_personality == Personality::Normal && rg.chance (40)) {
defensive -= 25.0f; defensive -= kBehaviorBase;
offensive += 25.0f; offensive += kBehaviorBase;
} }
else { else {
defensive += 25.0f; defensive += kBehaviorBase;
offensive -= 25.0f; offensive -= kBehaviorBase;
} }
} }
else if (m_team == Team::CT) { else if (m_team == Team::CT) {
// on hostage maps force more bots to save hostages // on hostage maps force more bots to save hostages
if (game.mapIs (MapFlags::HostageRescue)) { if (game.mapIs (MapFlags::HostageRescue)) {
defensive -= 25.0f - difficulty * 0.5f; defensive -= kBehaviorBase - difficulty * 0.5f;
offensive += 25.0f + difficulty * 5.0f; offensive += kBehaviorBase + difficulty * 5.0f;
} }
else { else {
defensive -= 25.0f; defensive -= kBehaviorBase;
offensive += 25.0f; offensive += kBehaviorBase;
} }
} }
} }
@ -140,8 +141,8 @@ int Bot::findBestGoal () {
} }
return m_chosenGoalIndex = findBombNode (); return m_chosenGoalIndex = findBombNode ();
} }
defensive += 25.0f + difficulty * 4.0f; defensive += kBehaviorBase + difficulty * 5.0f;
offensive -= 25.0f - difficulty * 0.5f; offensive -= kBehaviorBase - difficulty * 0.5f;
if (m_personality != Personality::Rusher) { if (m_personality != Personality::Rusher) {
defensive += 10.0f; defensive += 10.0f;
@ -155,12 +156,12 @@ int Bot::findBestGoal () {
} }
else if (game.mapIs (MapFlags::Escape)) { else if (game.mapIs (MapFlags::Escape)) {
if (m_team == Team::Terrorist) { if (m_team == Team::Terrorist) {
offensive += 25.0f + difficulty * 4.0f; offensive += kBehaviorBase + difficulty * 5.0f;
defensive -= 25.0f - difficulty * 0.5f; defensive -= kBehaviorBase - difficulty * 0.5f;
} }
else if (m_team == Team::CT) { else if (m_team == Team::CT) {
offensive -= 25.0f - difficulty * 4.5f; offensive -= kBehaviorBase - difficulty * 5.0f;
defensive += 25.0f + difficulty * 0.5f; defensive += kBehaviorBase + difficulty * 0.5f;
} }
} }
@ -188,6 +189,7 @@ int Bot::findBestGoal () {
} }
if (goalDesire > tacticChoice) { if (goalDesire > tacticChoice) {
tacticChoice = goalDesire;
tactic = GoalTactic::Goal; tactic = GoalTactic::Goal;
} }
return findGoalPost (tactic, defensiveNodes, offensiveNodes); return findGoalPost (tactic, defensiveNodes, offensiveNodes);
@ -230,7 +232,7 @@ int Bot::findBestGoalWhenBombAction () {
m_defendedBomb = true; m_defendedBomb = true;
result = findDefendNode (bombOrigin); result = findDefendNode (bombOrigin);
const Path &path = graph[result]; const auto &path = graph[result];
const float bombTimer = mp_c4timer.as <float> (); const float bombTimer = mp_c4timer.as <float> ();
const float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin); const float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
@ -258,7 +260,11 @@ int Bot::findBestGoalWhenBombAction () {
} }
int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offensive) { int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offensive) {
int goalChoices[4] = { kInvalidNodeIndex, kInvalidNodeIndex, kInvalidNodeIndex, kInvalidNodeIndex }; int goalChoices[4] {};
for (int i = 0; i < 4; ++i) {
goalChoices[i] = kInvalidNodeIndex;
}
if (tactic == GoalTactic::Defensive && !(*defensive).empty ()) { // careful goal if (tactic == GoalTactic::Defensive && !(*defensive).empty ()) { // careful goal
postProcessGoals (*defensive, goalChoices); postProcessGoals (*defensive, goalChoices);
@ -282,7 +288,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offensive) {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
int count = 0; int count = 0;
for (auto &point : graph.m_goalPoints) { for (const auto &point : graph.m_goalPoints) {
const float distanceSq = graph[point].origin.distanceSq (pev->origin); const float distanceSq = graph[point].origin.distanceSq (pev->origin);
if (distanceSq > cr::sqrf (1024.0f)) { if (distanceSq > cr::sqrf (1024.0f)) {
@ -313,7 +319,7 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offensive) {
float nearestDistanceSq = kInfiniteDistance; float nearestDistanceSq = kInfiniteDistance;
int count = 0; int count = 0;
for (auto &point : graph.m_rescuePoints) { for (const auto &point : graph.m_rescuePoints) {
const float distanceSq = graph[point].origin.distanceSq (pev->origin); const float distanceSq = graph[point].origin.distanceSq (pev->origin);
if (distanceSq < nearestDistanceSq) { if (distanceSq < nearestDistanceSq) {
@ -334,10 +340,6 @@ int Bot::findGoalPost (int tactic, IntArray *defensive, IntArray *offensive) {
} }
ensureCurrentNodeIndex (); ensureCurrentNodeIndex ();
if (goalChoices[0] == kInvalidNodeIndex) {
return m_chosenGoalIndex = graph.random ();
}
// rusher bots does not care any danger (idea from pbmm) // rusher bots does not care any danger (idea from pbmm)
if (m_personality == Personality::Rusher) { if (m_personality == Personality::Rusher) {
const auto randomGoal = goalChoices[rg (0, 3)]; const auto randomGoal = goalChoices[rg (0, 3)];
@ -468,39 +470,23 @@ void Bot::ignoreCollision () {
} }
void Bot::doPlayerAvoidance (const Vector &normal) { void Bot::doPlayerAvoidance (const Vector &normal) {
if (cv_has_team_semiclip || game.is (GameFlags::FreeForAll)) { if (isOnLadder () || cv_has_team_semiclip || game.is (GameFlags::FreeForAll)) {
return; // no player avoiding when with semiclip plugin return; // no player avoiding when with semiclip plugin
} }
m_hindrance = nullptr; m_hindrance = nullptr;
float distanceSq = cr::sqrf (512.0f); float distanceSq = cr::sqrf (512.0f);
if (isOnLadder ()) {
return;
}
const auto ownPrio = bots.getPlayerPriority (ent ()); const auto ownPrio = bots.getPlayerPriority (ent ());
// find nearest player to bot // find nearest player to bot
for (const auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team != m_team || client.ent == ent ()) {
// need only good meat
if (!(client.flags & ClientFlags::Used)) {
continue;
}
// and still alive meet
if (!(client.flags & ClientFlags::Alive)) {
continue;
}
// our team, alive and not myself?
if (client.team != m_team || client.ent == ent ()) {
continue; continue;
} }
const auto otherPrio = bots.getPlayerPriority (client.ent); const auto otherPrio = bots.getPlayerPriority (client.ent);
// give some priorities to bot avoidance // give some priorities to bot avoidance
if (ownPrio > otherPrio) { if (ownPrio < otherPrio) {
continue; continue;
} }
@ -509,7 +495,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
const auto avoidPrio = bots.getPlayerPriority (m_hindrance); const auto avoidPrio = bots.getPlayerPriority (m_hindrance);
// ignore because we're already avoiding someone better // ignore because we're already avoiding someone better
if (avoidPrio > otherPrio) { if (avoidPrio < otherPrio) {
continue; continue;
} }
} }
@ -525,7 +511,7 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
if (game.isNullEntity (m_hindrance)) { if (game.isNullEntity (m_hindrance)) {
return; return;
} }
const float interval = m_frameInterval * (!isDucking () && pev->velocity.lengthSq2d () > 0.0f ? 7.5f : 2.0f); const float interval = m_frameInterval * (!isDucking () && pev->velocity.lengthSq2d () > 0.0f ? 7.5f : 2.0f);
// use our movement angles, try to predict where we should be next frame // use our movement angles, try to predict where we should be next frame
Vector right {}, forward {}; Vector right {}, forward {};
@ -551,10 +537,13 @@ void Bot::doPlayerAvoidance (const Vector &normal) {
setStrafeSpeed (normal, -pev->maxspeed); setStrafeSpeed (normal, -pev->maxspeed);
} }
if (distanceSq < cr::sqrf (80.0f)) { if (distanceSq < cr::sqrf (96.0f)) {
if ((dir | forward.normalize2d_apx ()) < 0.0f) { if ((dir | forward.normalize2d_apx ()) < 0.0f) {
m_moveSpeed = -pev->maxspeed; m_moveSpeed = -pev->maxspeed;
} }
else {
m_moveSpeed = pev->maxspeed;
}
} }
} }
} }
@ -568,7 +557,7 @@ void Bot::checkTerrain (float movedDistance, const Vector &dirNormal) {
// standing still, no need to check? // standing still, no need to check?
if (m_lastCollTime < game.time () && getCurrentTaskId () != Task::Attack) { if (m_lastCollTime < game.time () && getCurrentTaskId () != Task::Attack) {
// didn't we move enough previously? // didn't we move enough previously?
if (movedDistance < 2.0f && (m_prevSpeed > 20.0f || m_prevVelocity < m_moveSpeed / 2)) { if (movedDistance < 4.0f && (m_prevSpeed > 20.0f || m_prevVelocity < m_moveSpeed / 2)) {
m_prevTime = game.time (); // then consider being stuck m_prevTime = game.time (); // then consider being stuck
m_isStuck = true; m_isStuck = true;
@ -862,7 +851,7 @@ void Bot::checkFall () {
} }
} }
else if (!isOnLadder () && !isInWater ()) { else if (!isOnLadder () && !isInWater ()) {
if (!m_checkFallPoint[0].empty () && !m_checkFallPoint[1].empty ()) { if (!m_checkFallPoint[0].empty () || !m_checkFallPoint[1].empty ()) {
m_checkFall = true; m_checkFall = true;
} }
} }
@ -882,12 +871,12 @@ void Bot::checkFall () {
&& baseDistanceSq >= cr::sqrf (80.0f) && nowDistanceSq >= cr::sqrf (100.0f)) { && baseDistanceSq >= cr::sqrf (80.0f) && nowDistanceSq >= cr::sqrf (100.0f)) {
fixFall = true; fixFall = true;
} }
else if (pev->origin.z + 128.0f < m_checkFallPoint[1].z && pev->origin.z + 128.0f < m_checkFallPoint[0].z) { else if (m_checkFallPoint[1].z > pev->origin.z + 128.0f || m_checkFallPoint[0].z > pev->origin.z + 128.0f) {
fixFall = true; fixFall = true;
} }
else if (m_currentNodeIndex != kInvalidNodeIndex else if (m_currentNodeIndex != kInvalidNodeIndex
&& nowDistanceSq <= cr::sqrf (32.0f) && nowDistanceSq > cr::sqrf (16.0f)
&& pev->origin.z + 64.0f < m_checkFallPoint[1].z) { && m_checkFallPoint[1].z > pev->origin.z + 62.0f) {
fixFall = true; fixFall = true;
} }
@ -925,6 +914,26 @@ void Bot::moveToGoal () {
pev->button |= IN_DUCK; pev->button |= IN_DUCK;
} }
} }
// press jump button if we need to leave the ladder
if (!(m_pathFlags & NodeFlag::Ladder)
&& isPreviousLadder ()
&& isOnFloor ()
&& isOnLadder ()
&& m_moveSpeed > 50.0f
&& pev->velocity.lengthSq () < 50.0f) {
pev->button |= IN_JUMP;
m_jumpTime = game.time () + 1.0f;
}
const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval);
if (distanceSq2d < cr::sqrf (m_moveSpeed) * m_frameInterval && getTask ()->data != kInvalidNodeIndex) {
m_moveSpeed = distanceSq2d;
}
if (m_moveSpeed > pev->maxspeed) {
m_moveSpeed = pev->maxspeed;
}
m_lastUsedNodesTime = game.time (); m_lastUsedNodesTime = game.time ();
// special movement for swimming here // special movement for swimming here
@ -1083,8 +1092,10 @@ bool Bot::updateNavigation () {
m_approachingLadderTimer.start (m_frameInterval * 4.0f); m_approachingLadderTimer.start (m_frameInterval * 4.0f);
} }
if (m_pathOrigin.z < pev->origin.z + 16.0f && !isOnLadder () && isOnFloor () && !isDucking ()) { if (!isOnLadder () && isOnFloor () && !isDucking ()) {
m_moveSpeed = ladderDistance; if (!isPreviousLadder ()) {
m_moveSpeed = ladderDistance;
}
if (m_moveSpeed < 150.0f) { if (m_moveSpeed < 150.0f) {
m_moveSpeed = 150.0f; m_moveSpeed = 150.0f;
@ -1107,17 +1118,14 @@ bool Bot::updateNavigation () {
bool foundGround = false; bool foundGround = false;
if (cr::abs (pev->origin.z - client.ent->v.origin.z) > 15.0f) { if (!isPreviousLadder ()
if (isPreviousLadder ()) { && client.ent->v.origin.distanceSq (pev->origin) < cr::sqrf (128.0f)
if (client.ent->v.origin.distanceSq (pev->origin) < cr::sqrf (64.0f)) { && cr::abs (client.ent->v.origin.z - pev->origin.z) > 17.0f) {
foundGround = true; foundGround = true;
}
}
} }
if (foundGround) { if (foundGround) {
clearSearchNodes (); changeNodeIndex (prevNodeIndex);
findNextBestNode ();
} }
} }
} }
@ -1140,8 +1148,11 @@ bool Bot::updateNavigation () {
game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr); game.testLine (pev->origin, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr);
if (!game.isNullEntity (tr.pHit) && game.isNullEntity (m_liftEntity) && util.isDoorEntity (tr.pHit)) { if (!game.isNullEntity (tr.pHit) && game.isNullEntity (m_liftEntity) && util.isDoorEntity (tr.pHit)) {
const auto &origin = game.getEntityOrigin (tr.pHit);
const float distanceSq = pev->origin.distanceSq (origin);
// if the door is near enough... // if the door is near enough...
if (pev->origin.distanceSq (game.getEntityOrigin (tr.pHit)) < cr::sqrf (50.0f)) { if (distanceSq < cr::sqrf (56.0f)) {
ignoreCollision (); // don't consider being stuck ignoreCollision (); // don't consider being stuck
// also 'use' the door randomly // also 'use' the door randomly
@ -1175,8 +1186,8 @@ bool Bot::updateNavigation () {
} }
// if bot hits the door, then it opens, so wait a bit to let it open safely // if bot hits the door, then it opens, so wait a bit to let it open safely
if (pev->velocity.lengthSq2d () < cr::sqrf (4.0f) && m_timeDoorOpen < game.time ()) { if (pev->velocity.lengthSq2d () < cr::sqrf (10.0f) && m_timeDoorOpen < game.time ()) {
if (getCurrentTaskId () != Task::MoveToPosition) { if (m_timeHitDoor >= game.time ()) {
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.5f, false); startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.5f, false);
} }
m_timeDoorOpen = game.time () + 1.0f; // retry in 1 sec until door is open m_timeDoorOpen = game.time () + 1.0f; // retry in 1 sec until door is open
@ -1205,10 +1216,25 @@ bool Bot::updateNavigation () {
else if (m_tryOpenDoor > 4) { else if (m_tryOpenDoor > 4) {
m_tryOpenDoor = 0; m_tryOpenDoor = 0;
// go back to prev node, if blocked for long time if (rg.chance (50)) {
if (graph.exists (m_previousNodes[0])) { const auto prevNode = m_previousNodes[0];
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, m_previousNodes[0], game.time () + 3.0f, true);
// go back to prev node, if blocked for long time
if (graph.exists (prevNode)) {
changeNodeIndex (prevNode);
}
} }
else {
const auto &dirToPoint = (pev->origin - origin).normalize2d_apx ();
const auto &forwardMove = m_moveAngles.forward ().normalize2d_apx ();
if (distanceSq < cr::sqrf (80.0f)) {
if ((dirToPoint | forwardMove) < 0.0f && !checkWallOnBehind ()) {
m_moveSpeed = -pev->maxspeed;
}
}
}
m_timeHitDoor = game.time () + 3.0f;
} }
} }
} }
@ -1307,7 +1333,7 @@ bool Bot::updateNavigation () {
&& getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::EscapeFromBomb
&& taskTarget != kInvalidNodeIndex) { && taskTarget != kInvalidNodeIndex) {
const Vector &bombOrigin = isBombAudible (); const auto &bombOrigin = isBombAudible ();
// bot within 'hearable' bomb tick noises? // bot within 'hearable' bomb tick noises?
if (!bombOrigin.empty ()) { if (!bombOrigin.empty ()) {
@ -1404,7 +1430,7 @@ bool Bot::updateLiftHandling () {
} }
else if (!m_pathWalk.empty ()) { // no lift found at node else if (!m_pathWalk.empty ()) { // no lift found at node
if ((m_liftState == LiftState::None || m_liftState == LiftState::WaitingFor) && m_pathWalk.hasNext ()) { if ((m_liftState == LiftState::None || m_liftState == LiftState::WaitingFor) && m_pathWalk.hasNext ()) {
int nextNode = m_pathWalk.next (); const auto nextNode = m_pathWalk.next ();
if (graph.exists (nextNode) && (graph[nextNode].flags & NodeFlag::Lift)) { if (graph.exists (nextNode) && (graph[nextNode].flags & NodeFlag::Lift)) {
game.testLine (m_path->origin, graph[nextNode].origin, TraceIgnore::Everything, ent (), &tr); game.testLine (m_path->origin, graph[nextNode].origin, TraceIgnore::Everything, ent (), &tr);
@ -1727,8 +1753,13 @@ bool Bot::findNextBestNode () {
int busyIndex = kInvalidNodeIndex; int busyIndex = kInvalidNodeIndex;
float lessDist[3] = { kInfiniteDistance, kInfiniteDistance, kInfiniteDistance }; float lessDist[3] {};
int lessIndex[3] = { kInvalidNodeIndex, kInvalidNodeIndex, kInvalidNodeIndex }; int lessIndex[3] {};
for (int i = 0; i < 3; ++i) {
lessDist[i] = kInfiniteDistance;
lessIndex[i] = kInvalidNodeIndex;
}
const auto &origin = pev->origin + pev->velocity * m_frameInterval; const auto &origin = pev->origin + pev->velocity * m_frameInterval;
const auto &bucket = graph.getNodesInBucket (origin); const auto &bucket = graph.getNodesInBucket (origin);
@ -1785,19 +1816,10 @@ bool Bot::findNextBestNode () {
const float distanceSq = pev->origin.distanceSq (path.origin); const float distanceSq = pev->origin.distanceSq (path.origin);
if (distanceSq < lessDist[0]) { if (distanceSq < lessDist[0]) {
lessDist[2] = lessDist[1];
lessIndex[2] = lessIndex[1];
lessDist[1] = lessDist[0];
lessIndex[1] = lessIndex[0];
lessDist[0] = distanceSq; lessDist[0] = distanceSq;
lessIndex[0] = path.number; lessIndex[0] = path.number;
} }
else if (distanceSq < lessDist[1]) { else if (distanceSq < lessDist[1]) {
lessDist[2] = lessDist[1];
lessIndex[2] = lessIndex[1];
lessDist[1] = distanceSq; lessDist[1] = distanceSq;
lessIndex[1] = path.number; lessIndex[1] = path.number;
} }
@ -1888,7 +1910,7 @@ void Bot::findValidNode () {
damageValue = cr::clamp (damageValue + 100, 0, kMaxDamageValue); damageValue = cr::clamp (damageValue + 100, 0, kMaxDamageValue);
// affect nearby connected with victim nodes // affect nearby connected with victim nodes
for (auto &neighbour : m_path->links) { for (const auto &neighbour : m_path->links) {
if (graph.exists (neighbour.index)) { if (graph.exists (neighbour.index)) {
int neighbourValue = practice.getDamage (team, neighbour.index, neighbour.index); int neighbourValue = practice.getDamage (team, neighbour.index, neighbour.index);
neighbourValue = cr::clamp (neighbourValue + 100, 0, kMaxDamageValue); neighbourValue = cr::clamp (neighbourValue + 100, 0, kMaxDamageValue);
@ -2019,7 +2041,7 @@ int Bot::findBombNode () {
float lastDistanceSq = kInfiniteDistance; float lastDistanceSq = kInfiniteDistance;
// find nearest goal node either to bomb (if "heard" or player) // find nearest goal node either to bomb (if "heard" or player)
for (auto &point : goals) { for (const auto &point : goals) {
const float distanceSq = bomb.distanceSq (graph[point].origin); const float distanceSq = bomb.distanceSq (graph[point].origin);
// check if we got more close distance // check if we got more close distance
@ -2211,7 +2233,7 @@ int Bot::findCoverNode (float maxDistance) {
} }
// now get enemies neigbouring points // now get enemies neigbouring points
for (auto &link : graph[enemyIndex].links) { for (const auto &link : graph[enemyIndex].links) {
if (link.index != kInvalidNodeIndex) { if (link.index != kInvalidNodeIndex) {
enemies.push (link.index); enemies.push (link.index);
} }
@ -2228,7 +2250,7 @@ int Bot::findCoverNode (float maxDistance) {
} }
bool neighbourVisible = false; // now check neighbour nodes for visibility bool neighbourVisible = false; // now check neighbour nodes for visibility
for (auto &enemy : enemies) { for (const auto &enemy : enemies) {
if (vistab.visible (enemy, path.number)) { if (vistab.visible (enemy, path.number)) {
neighbourVisible = true; neighbourVisible = true;
break; break;
@ -2422,7 +2444,7 @@ bool Bot::advanceMovement () {
m_campButtons = 0; m_campButtons = 0;
const int nextIndex = m_pathWalk.next (); const auto nextIndex = m_pathWalk.next ();
auto kills = static_cast <float> (practice.getDamage (m_team, nextIndex, nextIndex)); auto kills = static_cast <float> (practice.getDamage (m_team, nextIndex, nextIndex));
// if damage done higher than one // if damage done higher than one
@ -2452,7 +2474,7 @@ bool Bot::advanceMovement () {
} }
// force terrorist bot to plant bomb // force terrorist bot to plant bomb
if (m_inBombZone && !m_hasProgressBar && m_hasC4 && getCurrentTaskId () != Task::PlantBomb) { if (m_inBombZone && !m_hasProgressBar && m_hasC4 && tid != Task::PlantBomb) {
changeNextGoal (); changeNextGoal ();
return false; return false;
} }
@ -2462,7 +2484,7 @@ bool Bot::advanceMovement () {
if (!m_pathWalk.empty ()) { if (!m_pathWalk.empty ()) {
m_jumpSequence = false; m_jumpSequence = false;
const int destIndex = m_pathWalk.first (); const auto destIndex = m_pathWalk.first ();
bool isCurrentJump = false; bool isCurrentJump = false;
// find out about connection flags // find out about connection flags
@ -3158,13 +3180,13 @@ int Bot::getRandomCampDir () {
} }
void Bot::setStrafeSpeed (const Vector &moveDir, float strafeSpeed) { void Bot::setStrafeSpeed (const Vector &moveDir, float strafeSpeed) {
const Vector &los = (moveDir - pev->origin).normalize2d_apx (); const auto &los = (moveDir - pev->origin).normalize2d_apx ();
const float dot = los | pev->angles.forward ().get2d (); const float dot = los | pev->angles.forward ().get2d ();
if (dot > 0.0f && !checkWallOnRight ()) { if (dot > 0.0f && !checkWallOnRight ()) {
m_strafeSpeed = strafeSpeed; m_strafeSpeed = strafeSpeed;
} }
else if (!checkWallOnLeft ()) { else if (dot < 0.0f && !checkWallOnLeft ()) {
m_strafeSpeed = -strafeSpeed; m_strafeSpeed = -strafeSpeed;
} }
} }
@ -3371,15 +3393,10 @@ void Bot::syncFindPath (int srcIndex, int destIndex, FindPath pathType) {
fprintf (stderr, "%s source path is same as dest (%d).\n", __func__, destIndex); fprintf (stderr, "%s source path is same as dest (%d).\n", __func__, destIndex);
return; return;
} }
clearSearchNodes ();
m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f;
// always use shortest-path algorithm when failed sanity checks within load // always use shortest-path algorithm when failed sanity checks within load
if (planner.isPathsCheckFailed ()) { if (planner.isPathsCheckFailed ()) {
findShortestPath (srcIndex, destIndex); findShortestPath (srcIndex, destIndex);
return; return;
} }
@ -3414,6 +3431,7 @@ void Bot::syncFindPath (int srcIndex, int destIndex, FindPath pathType) {
m_planner->setG (PlannerHeuristic::gfunctionPathDist); m_planner->setG (PlannerHeuristic::gfunctionPathDist);
} }
} }
clearSearchNodes ();
m_chosenGoalIndex = srcIndex; m_chosenGoalIndex = srcIndex;
m_goalValue = 0.0f; m_goalValue = 0.0f;

View file

@ -222,7 +222,12 @@ bool BotSupport::isDoorEntity (edict_t *ent) {
if (game.isNullEntity (ent)) { if (game.isNullEntity (ent)) {
return false; return false;
} }
return ent->v.classname.str ().startsWith ("func_door"); const auto classHash = ent->v.classname.str ().hash ();
constexpr auto kFuncDoor = StringRef::fnv1a32 ("func_door");
constexpr auto kFuncDoorRotating = StringRef::fnv1a32 ("func_door_rotating");
return classHash == kFuncDoor || classHash == kFuncDoorRotating;
} }
bool BotSupport::isHostageEntity (edict_t *ent) { bool BotSupport::isHostageEntity (edict_t *ent) {
@ -241,6 +246,14 @@ bool BotSupport::isShootableBreakable (edict_t *ent) {
if (game.isNullEntity (ent) || ent == game.getStartEntity ()) { if (game.isNullEntity (ent) || ent == game.getStartEntity ()) {
return false; return false;
} }
// todo: move the breakables list into own array, and refresh them every round, since next thing is very expensive
#if 0
StringRef material = engfuncs.pfnInfoKeyValue (engfuncs.pfnGetInfoKeyBuffer (ent), "material");
if (material == "7") {
return false;
}
#endif
const auto limit = cv_breakable_health_limit.as <float> (); const auto limit = cv_breakable_health_limit.as <float> ();
// not shootable // not shootable

View file

@ -148,6 +148,12 @@ void Bot::normal_ () {
campingAllowed = false; campingAllowed = false;
} }
// if the bot is about to come to the camp spot, but there is already someone else camping
if (!campingAllowed && getTask ()->data == m_currentNodeIndex && getTask ()->data != kInvalidNodeIndex) {
clearSearchNodes ();
getTask ()->data = kInvalidNodeIndex;
}
if (campingAllowed) { if (campingAllowed) {
// crouched camping here? // crouched camping here?
if (m_pathFlags & NodeFlag::Crouch) { if (m_pathFlags & NodeFlag::Crouch) {
@ -427,7 +433,7 @@ void Bot::seekCover_ () {
startTask (Task::Hide, TaskPri::Hide, kInvalidNodeIndex, game.time () + rg (3.0f, 12.0f), false); startTask (Task::Hide, TaskPri::Hide, kInvalidNodeIndex, game.time () + rg (3.0f, 12.0f), false);
// get a valid look direction // get a valid look direction
const Vector &dest = getCampDirection (m_lastEnemyOrigin); const auto &dest = getCampDirection (m_lastEnemyOrigin);
m_aimFlags |= AimFlags::Camp; m_aimFlags |= AimFlags::Camp;
m_lookAtSafe = dest; m_lookAtSafe = dest;
@ -641,18 +647,18 @@ void Bot::camp_ () {
// random camp dir, or prediction // random camp dir, or prediction
auto useRandomCampDirOrPredictEnemy = [&] () { auto useRandomCampDirOrPredictEnemy = [&] () {
if (!m_lastEnemyOrigin.empty ()) { if (!m_lastEnemyOrigin.empty () && util.isAlive (m_lastEnemy)) {
auto pathLength = m_lastPredictLength; auto pathLength = m_lastPredictLength;
auto predictNode = m_lastPredictIndex; auto predictNode = m_lastPredictIndex;
if (pathLength > 1 && isNodeValidForPredict (predictNode)) { if (isNodeValidForPredict (predictNode) && pathLength > 1) {
m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; m_lookAtSafe = graph[predictNode].origin + pev->view_ofs;
} }
else { else {
pathLength = 0; pathLength = 0;
predictNode = findAimingNode (m_lastEnemyOrigin, pathLength); predictNode = findAimingNode (m_lastEnemyOrigin, pathLength);
if (pathLength > 1 && isNodeValidForPredict (predictNode)) { if (isNodeValidForPredict (predictNode) && pathLength > 1) {
m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; m_lookAtSafe = graph[predictNode].origin + pev->view_ofs;
} }
} }
@ -681,7 +687,7 @@ void Bot::camp_ () {
TraceResult tr {}; TraceResult tr {};
// and use real angles to check it // and use real angles to check it
auto to = m_pathOrigin + dest.forward () * 500.0f; const auto &to = m_pathOrigin + dest.forward () * 500.0f;
// let's check the destination // let's check the destination
game.testLine (getEyesPos (), to, TraceIgnore::Monsters, ent (), &tr); game.testLine (getEyesPos (), to, TraceIgnore::Monsters, ent (), &tr);
@ -886,7 +892,7 @@ void Bot::defuseBomb_ () {
defuseRemainingTime = fullDefuseTime - game.time (); defuseRemainingTime = fullDefuseTime - game.time ();
} }
const Vector &bombPos = graph.getBombOrigin (); const auto &bombPos = graph.getBombOrigin ();
bool defuseError = false; bool defuseError = false;
// exception: bomb has been defused // exception: bomb has been defused
@ -1001,11 +1007,11 @@ void Bot::defuseBomb_ () {
botStandOrigin = pev->origin; botStandOrigin = pev->origin;
} }
const float duckLengthSq = m_entity.distanceSq (botDuckOrigin); const float duckDistanceSq = m_entity.distanceSq (botDuckOrigin);
const float standLengthSq = m_entity.distanceSq (botStandOrigin); const float standDistanceSq = m_entity.distanceSq (botStandOrigin);
if (duckLengthSq > cr::sqrf (75.0f) || standLengthSq > cr::sqrf (75.0f)) { if (duckDistanceSq > cr::sqrf (75.0f) || standDistanceSq > cr::sqrf (75.0f)) {
if (standLengthSq < duckLengthSq) { if (standDistanceSq < duckDistanceSq) {
m_duckDefuse = false; // stand m_duckDefuse = false; // stand
} }
else { else {
@ -1126,7 +1132,7 @@ void Bot::followUser_ () {
int destIndex = graph.getNearest (m_targetEntity->v.origin); int destIndex = graph.getNearest (m_targetEntity->v.origin);
auto points = graph.getNearestInRadius (200.0f, m_targetEntity->v.origin); auto points = graph.getNearestInRadius (200.0f, m_targetEntity->v.origin);
for (auto &newIndex : points) { for (const auto &newIndex : points) {
// if node not yet used, assign it as dest // if node not yet used, assign it as dest
if (newIndex != m_currentNodeIndex && !isOccupiedNode (newIndex)) { if (newIndex != m_currentNodeIndex && !isOccupiedNode (newIndex)) {
destIndex = newIndex; destIndex = newIndex;
@ -1398,10 +1404,7 @@ void Bot::escapeFromBomb_ () {
pev->button |= IN_ATTACK2; pev->button |= IN_ATTACK2;
} }
if (!usesKnife () if (!usesKnife () && game.isNullEntity (m_enemy) && !util.isAlive (m_lastEnemy)) {
&& m_lastEnemyOrigin.empty ()
&& !(m_states & Sense::SeeingEnemy)
&& !util.isAlive (m_lastEnemy)) {
selectWeaponById (Weapon::Knife); selectWeaponById (Weapon::Knife);
} }
@ -1457,7 +1460,6 @@ void Bot::escapeFromBomb_ () {
} }
void Bot::shootBreakable_ () { void Bot::shootBreakable_ () {
// breakable destroyed? // breakable destroyed?
if (!util.isShootableBreakable (m_breakableEntity)) { if (!util.isShootableBreakable (m_breakableEntity)) {
completeTask (); completeTask ();
@ -1471,12 +1473,12 @@ void Bot::shootBreakable_ () {
m_lookAtSafe = m_breakableOrigin; m_lookAtSafe = m_breakableOrigin;
// is bot facing the breakable? // is bot facing the breakable?
if (util.getConeDeviation (ent (), m_breakableOrigin) >= 0.95f && util.isVisible (m_breakableOrigin, ent ())) { if (util.getConeDeviation (ent (), m_breakableOrigin) >= 0.90f) {
m_aimFlags |= AimFlags::Override;
m_moveSpeed = 0.0f; m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f; m_strafeSpeed = 0.0f;
m_aimFlags |= AimFlags::Override;
if (usesKnife ()) { if (usesKnife ()) {
selectBestWeapon (); selectBestWeapon ();
} }
@ -1501,7 +1503,7 @@ void Bot::pickupItem_ () {
return; return;
} }
const Vector &dest = game.getEntityOrigin (m_pickupItem); const auto &dest = game.getEntityOrigin (m_pickupItem);
m_destOrigin = dest; m_destOrigin = dest;
m_entity = dest; m_entity = dest;
@ -1669,7 +1671,7 @@ void Bot::pickupItem_ () {
} }
// check if hostage is with a human teammate (hack) // check if hostage is with a human teammate (hack)
for (auto &client : util.getClients ()) { for (const auto &client : util.getClients ()) {
if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) &&
client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) { client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) {
return EntitySearchResult::Continue; return EntitySearchResult::Continue;

View file

@ -361,9 +361,9 @@ bool Frustum::isObjectInsidePlane (const Plane &plane, const Vector &center, flo
return plane.result + (plane.normal | point) >= 0.0f; return plane.result + (plane.normal | point) >= 0.0f;
}; };
const Vector &test = plane.normal.get2d (); const auto &test = plane.normal.get2d ();
const Vector &top = center + Vector (0.0f, 0.0f, height * 0.5f) + test * radius; const auto &top = center + Vector (0.0f, 0.0f, height * 0.5f) + test * radius;
const Vector &bottom = center - Vector (0.0f, 0.0f, height * 0.5f) + test * radius; const auto &bottom = center - Vector (0.0f, 0.0f, height * 0.5f) + test * radius;
return isPointInsidePlane (top) || isPointInsidePlane (bottom); return isPointInsidePlane (top) || isPointInsidePlane (bottom);
} }
@ -403,7 +403,7 @@ void Frustum::calculate (Planes &planes, const Vector &viewAngle, const Vector &
bool Frustum::check (const Planes &planes, edict_t *ent) const { bool Frustum::check (const Planes &planes, edict_t *ent) const {
constexpr auto kOffset = Vector (0.0f, 0.0f, 5.0f); constexpr auto kOffset = Vector (0.0f, 0.0f, 5.0f);
const Vector &origin = ent->v.origin - kOffset; const auto &origin = ent->v.origin - kOffset;
for (const auto &plane : planes) { for (const auto &plane : planes) {
if (!isObjectInsidePlane (plane, origin, 60.0f, 16.0f)) { if (!isObjectInsidePlane (plane, origin, 60.0f, 16.0f)) {
@ -507,26 +507,25 @@ void Bot::setAimDirection () {
auto predictNode = m_lastPredictIndex; auto predictNode = m_lastPredictIndex;
auto isPredictedIndexApplicable = [&] () -> bool { auto isPredictedIndexApplicable = [&] () -> bool {
if (!isNodeValidForPredict (predictNode) TraceResult result {};
|| pathLength >= cv_max_nodes_for_predict.as <int> ()) { game.testLine (getEyesPos (), graph[predictNode].origin + pev->view_ofs, TraceIgnore::None, ent (), &result);
if (result.flFraction < 0.5f) {
return false;
}
const float distToPredictNodeSq = graph[predictNode].origin.distanceSq (pev->origin);
if (distToPredictNodeSq >= cr::sqrf (2048.0f)) {
return false; return false;
} }
if (!vistab.visible (m_currentNodeIndex, predictNode) if (!vistab.visible (m_currentNodeIndex, predictNode) || !vistab.visible (m_previousNodes[0], predictNode)) {
|| !vistab.visible (m_previousNodes[0], predictNode)
|| !vistab.visible (predictNode, m_currentNodeIndex)) {
predictNode = kInvalidNodeIndex; predictNode = kInvalidNodeIndex;
pathLength = kInfiniteDistanceLong; pathLength = kInfiniteDistanceLong;
return false; return false;
} }
return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as <int> ();
TraceResult result {};
game.testLine (getEyesPos (), graph[predictNode].origin + pev->view_ofs, TraceIgnore::None, ent (), &result);
return result.flFraction >= 0.8f && graph[predictNode].origin.distanceSq (pev->origin) > cr::sqrf (256.0f);
}; };
if (changePredictedEnemy) { if (changePredictedEnemy) {