graph: reworked buckets so they can handle very large number of nodes

graph: reworked buckets so they can handle very large number of nodes
aim: bots should more respect headshot allow option (needs testing)
aim: incorporated never-finished changes from pr #204
nav: increased reachability timers a bit
nav: ensure buckets has enough nodes before use they
conf: introduced max recoil in difficulty config file
bot: overall fixes to jason mode, treat knife in hands and no weapons as jason mode too
bot: changed default difficulty level for bots to level 3
fix: knife attacks not working since last commit (fixes #429)
fix: hostage rescue not working since last commit (fixes #427)
refactor: use range loops for graph outside graph class when possible
This commit is contained in:
jeefo 2023-04-11 22:32:28 +03:00
commit 1a650c57ce
No known key found for this signature in database
GPG key ID: 927BCA0779BEA8ED
14 changed files with 426 additions and 347 deletions

View file

@ -19,11 +19,12 @@
; headshotProbability - Probability bot should aim at head instead of body if body and head both visible. ; headshotProbability - Probability bot should aim at head instead of body if body and head both visible.
; seenThruWallChance - Chance the bot will attack enemy if he believes he's there and just seen him. ; seenThruWallChance - Chance the bot will attack enemy if he believes he's there and just seen him.
; heardThruWallChance - Chance the bot will attack enemy if he believes he's there and just heard him. ; heardThruWallChance - Chance the bot will attack enemy if he believes he's there and just heard him.
; maxWeaponRecoil - Maximum weapon recoil to compensate by pausing fire.
; aimError - (x, y, z) offsets to add aim error to bot aiming
; ;
Noob = 0.8, 1.0, 5, 0, 0 Noob = 0.8, 1.0, 5, 0, 0, 38, 30.0, 30.0, 40.0
Easy = 0.6, 0.8, 30, 10, 10 Easy = 0.6, 0.8, 10, 0, 10, 32, 15.0, 15.0, 24.0
Normal = 0.4, 0.6, 50, 30, 40 Normal = 0.4, 0.6, 50, 30, 40, 26, 5.0, 5.0, 10.0
Hard = 0.2, 0.4, 75, 60, 70 Hard = 0.2, 0.4, 75, 60, 70, 23, 0.0, 0.0, 0.0
Expert = 0.1, 0.2, 100, 90, 90 Expert = 0.1, 0.2, 100, 90, 90, 21, 0.0, 0.0, 0.0

View file

@ -60,6 +60,13 @@ yb_radio_mode "2"
// //
yb_economics_rounds "1" yb_economics_rounds "1"
//
// Allows bots to ignore economics and buy weapons with disrespect of economics.
// ---
// Default: "25", Min: "0", Max: "100"
//
yb_economics_disrespect_percent "25"
// //
// Specifies whether bots able to use 'shift' if they thinks that enemy is near. // Specifies whether bots able to use 'shift' if they thinks that enemy is near.
// --- // ---
@ -81,6 +88,13 @@ yb_camping_allowed "1"
// //
yb_avoid_grenades "1" yb_avoid_grenades "1"
//
// Allows or disallows bot to check environment for darkness, thus allows or not to use flashlights or NVG.
// ---
// Default: "1", Min: "0", Max: "1"
//
yb_check_darkness "1"
// //
// Lower bound of time from which time for camping is calculated // Lower bound of time from which time for camping is calculated
// --- // ---
@ -193,6 +207,13 @@ yb_ignore_objectives "0"
// //
yb_random_knife_attacks "1" yb_random_knife_attacks "1"
//
// Maximum number for path length, to predict the enemy.
// ---
// Default: "30", Min: "15", Max: "256"
//
yb_max_nodes_for_predict "30"
// //
// Enables or disables bots chat functionality. // Enables or disables bots chat functionality.
// --- // ---
@ -210,7 +231,7 @@ yb_chat_percent "30"
// //
// Specifies whether bots able to fire at enemies behind the wall, if they hearing or suspecting them. // Specifies whether bots able to fire at enemies behind the wall, if they hearing or suspecting them.
// --- // ---
// Default: "2", Min: "0", Max: "2" // Default: "2", Min: "0", Max: "3"
// //
yb_shoots_thru_walls "2" yb_shoots_thru_walls "2"
@ -407,9 +428,9 @@ yb_name_prefix ""
// //
// All bots difficulty level. Changing at runtime will affect already created bots. // All bots difficulty level. Changing at runtime will affect already created bots.
// --- // ---
// Default: "4", Min: "0", Max: "4" // Default: "3", Min: "0", Max: "4"
// //
yb_difficulty "4" yb_difficulty "3"
// //
// Lower bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used. // Lower bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.

View file

@ -35,6 +35,8 @@ public:
int32_t headshotPct {}; int32_t headshotPct {};
int32_t seenThruPct {}; int32_t seenThruPct {};
int32_t hearThruPct {}; int32_t hearThruPct {};
int32_t maxRecoil {};
Vector aimError {};
}; };
private: private:

View file

@ -151,6 +151,9 @@ public:
// test line // test line
void testLine (const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult *ptr); void testLine (const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult *ptr);
// test model
void testModel (const Vector &start, const Vector &end, int hullNumber, edict_t *entToHit, TraceResult *ptr);
// trace line with channel, but allows us to store last traceline bot has fired, saving us some cpu cycles // trace line with channel, but allows us to store last traceline bot has fired, saving us some cpu cycles
bool testLineChannel (TraceChannel channel, const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult &result); bool testLineChannel (TraceChannel channel, const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult &result);

View file

@ -259,10 +259,6 @@ public:
friend class Bot; friend class Bot;
private: private:
struct Bucket {
int x, y, z;
};
int m_editFlags {}; int m_editFlags {};
int m_loadAttempts {}; int m_loadAttempts {};
int m_cacheNodeIndex {}; int m_cacheNodeIndex {};
@ -297,12 +293,13 @@ private:
IntArray m_rescuePoints {}; IntArray m_rescuePoints {};
IntArray m_visitedGoals {}; IntArray m_visitedGoals {};
SmallArray <int32_t> m_buckets[kMaxBucketsInsidePos][kMaxBucketsInsidePos][kMaxBucketsInsidePos];
SmallArray <Matrix> m_matrix {}; SmallArray <Matrix> m_matrix {};
SmallArray <Practice> m_practice {}; SmallArray <Practice> m_practice {};
SmallArray <Path> m_paths {}; SmallArray <Path> m_paths {};
SmallArray <uint8_t> m_vistable {}; SmallArray <uint8_t> m_vistable {};
HashMap <int32_t, Array <int32_t>, EmptyHash <int32_t>> m_hashTable;
String m_graphAuthor {}; String m_graphAuthor {};
String m_graphModified {}; String m_graphModified {};
@ -327,6 +324,7 @@ public:
int getPathDist (int srcIndex, int destIndex); int getPathDist (int srcIndex, int destIndex);
int clearConnections (int index); int clearConnections (int index);
int getBspSize (); int getBspSize ();
int locateBucket (const Vector &pos);
float calculateTravelTime (float maxSpeed, const Vector &src, const Vector &origin); float calculateTravelTime (float maxSpeed, const Vector &src, const Vector &origin);
@ -394,9 +392,8 @@ public:
const char *getDataDirectory (bool isMemoryFile = false); const char *getDataDirectory (bool isMemoryFile = false);
const char *getOldFormatGraphName (bool isMemoryFile = false); const char *getOldFormatGraphName (bool isMemoryFile = false);
Bucket locateBucket (const Vector &pos); IntArray getNarestInRadius (float radius, const Vector &origin, int maxCount = -1);
IntArray searchRadius (float radius, const Vector &origin, int maxCount = -1); const IntArray &getNodesInBucket (const Vector &pos);
const SmallArray <int32_t> &getNodesInBucket (const Vector &pos);
public: public:
size_t getMaxRouteLength () const { size_t getMaxRouteLength () const {
@ -468,6 +465,23 @@ public:
edict_t *getEditor () { edict_t *getEditor () {
return m_editor; return m_editor;
} }
public:
Path *begin () {
return m_paths.begin ();
}
Path *begin () const {
return m_paths.begin ();
}
Path *end () {
return m_paths.end ();
}
Path *end () const {
return m_paths.end ();
}
}; };
// we're need `bots` // we're need `bots`

View file

@ -467,10 +467,6 @@ constexpr int kGameMaxPlayers = 32;
constexpr int kGameTeamNum = 2; constexpr int kGameTeamNum = 2;
constexpr int kInvalidNodeIndex = -1; constexpr int kInvalidNodeIndex = -1;
constexpr int kMaxBucketSize = static_cast <int> (kMaxNodes * 0.65);
constexpr int kMaxBucketsInsidePos = kMaxNodes * 8 / kMaxBucketSize + 1;
constexpr int kMaxNodesInsideBucket = kMaxBucketSize / kMaxBucketsInsidePos;
// weapon masks // weapon masks
constexpr auto kPrimaryWeaponMask = (cr::bit (Weapon::XM1014) | cr::bit (Weapon::M3) | cr::bit (Weapon::MAC10) | cr::bit (Weapon::UMP45) | cr::bit (Weapon::MP5) | cr::bit (Weapon::TMP) | cr::bit (Weapon::P90) | cr::bit (Weapon::AUG) | cr::bit (Weapon::M4A1) | cr::bit (Weapon::SG552) | cr::bit (Weapon::AK47) | cr::bit (Weapon::Scout) | cr::bit (Weapon::SG550) | cr::bit (Weapon::AWP) | cr::bit (Weapon::G3SG1) | cr::bit (Weapon::M249) | cr::bit (Weapon::Famas) | cr::bit (Weapon::Galil)); constexpr auto kPrimaryWeaponMask = (cr::bit (Weapon::XM1014) | cr::bit (Weapon::M3) | cr::bit (Weapon::MAC10) | cr::bit (Weapon::UMP45) | cr::bit (Weapon::MP5) | cr::bit (Weapon::TMP) | cr::bit (Weapon::P90) | cr::bit (Weapon::AUG) | cr::bit (Weapon::M4A1) | cr::bit (Weapon::SG552) | cr::bit (Weapon::AK47) | cr::bit (Weapon::Scout) | cr::bit (Weapon::SG550) | cr::bit (Weapon::AWP) | cr::bit (Weapon::G3SG1) | cr::bit (Weapon::M249) | cr::bit (Weapon::Famas) | cr::bit (Weapon::Galil));
constexpr auto kSecondaryWeaponMask = (cr::bit (Weapon::P228) | cr::bit (Weapon::Elite) | cr::bit (Weapon::USP) | cr::bit (Weapon::Glock18) | cr::bit (Weapon::Deagle) | cr::bit (Weapon::FiveSeven)); constexpr auto kSecondaryWeaponMask = (cr::bit (Weapon::P228) | cr::bit (Weapon::Elite) | cr::bit (Weapon::USP) | cr::bit (Weapon::Glock18) | cr::bit (Weapon::Deagle) | cr::bit (Weapon::FiveSeven));
@ -690,6 +686,7 @@ private:
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 {}; // breakeble acquired time float m_breakableTime {}; // breakeble acquired time
float m_jumpDistance {}; // last jump distance float m_jumpDistance {}; // last jump distance
float m_lastBadWeaponSwitchTime {}; // last time we're switched weapon as it's bad
bool m_moveToGoal {}; // bot currently moving to goal?? bool m_moveToGoal {}; // bot currently moving to goal??
bool m_isStuck {}; // bot is stuck bool m_isStuck {}; // bot is stuck
@ -709,6 +706,7 @@ private:
bool m_moveToC4 {}; // ct is moving to bomb bool m_moveToC4 {}; // ct is moving to bomb
bool m_grenadeRequested {}; // bot requested change to grenade bool m_grenadeRequested {}; // bot requested change to grenade
bool m_needToSendWelcomeChat {}; // bot needs to greet people on server? bool m_needToSendWelcomeChat {}; // bot needs to greet people on server?
bool m_switchedToKnifeDuringJump {}; // bot needs to revert weapon after jump?
Pickup m_pickupType {}; // type of entity which needs to be used/picked up Pickup m_pickupType {}; // type of entity which needs to be used/picked up
PathWalk m_pathWalk {}; // pointer to current node from path PathWalk m_pathWalk {}; // pointer to current node from path
@ -778,7 +776,6 @@ private:
float getEstimatedNodeReachTime (); float getEstimatedNodeReachTime ();
float isInFOV (const Vector &dest); float isInFOV (const Vector &dest);
float getShiftSpeed (); float getShiftSpeed ();
float getEnemyBodyOffsetCorrection (float distance);
float calculateScaleFactor (edict_t *ent); float calculateScaleFactor (edict_t *ent);
bool canReplaceWeapon (); bool canReplaceWeapon ();
@ -809,6 +806,7 @@ private:
bool reactOnEnemy (); bool reactOnEnemy ();
bool selectBestNextNode (); bool selectBestNextNode ();
bool hasAnyWeapons (); bool hasAnyWeapons ();
bool isKnifeMode ();
bool isDeadlyMove (const Vector &to); bool isDeadlyMove (const Vector &to);
bool isOutOfBombTimer (); bool isOutOfBombTimer ();
bool isWeaponBadAtDistance (int weaponIndex, float distance); bool isWeaponBadAtDistance (int weaponIndex, float distance);
@ -830,6 +828,7 @@ private:
bool updateLiftHandling (); bool updateLiftHandling ();
bool updateLiftStates (); bool updateLiftStates ();
bool canRunHeavyWeight (); bool canRunHeavyWeight ();
bool isEnemyInSight (Vector &endPos);
void doPlayerAvoidance (const Vector &normal); void doPlayerAvoidance (const Vector &normal);
void selectCampButtons (int index); void selectCampButtons (int index);
@ -920,12 +919,13 @@ private:
edict_t *lookupBreakable (); edict_t *lookupBreakable ();
edict_t *setCorrectGrenadeVelocity (const char *model); edict_t *setCorrectGrenadeVelocity (const char *model);
const Vector &getEnemyBodyOffset (); Vector getEnemyBodyOffset ();
Vector calcThrow (const Vector &start, const Vector &stop); Vector calcThrow (const Vector &start, const Vector &stop);
Vector calcToss (const Vector &start, const Vector &stop); Vector calcToss (const Vector &start, const Vector &stop);
Vector isBombAudible (); Vector isBombAudible ();
Vector getBodyOffsetError (float distance); Vector getBodyOffsetError (float distance);
Vector getCampDirection (const Vector &dest); Vector getCampDirection (const Vector &dest);
Vector getCustomHeight (float distance);
uint8_t computeMsec (); uint8_t computeMsec ();

View file

@ -44,6 +44,8 @@ ConVar cv_pickup_best ("yb_pickup_best", "1", "Allows or disallows bots to picku
ConVar cv_ignore_objectives ("yb_ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages."); ConVar cv_ignore_objectives ("yb_ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages.");
ConVar cv_random_knife_attacks ("yb_random_knife_attacks", "1", "Allows or disallows the ability for random knife attacks when bot is rushing and no enemy is nearby."); ConVar cv_random_knife_attacks ("yb_random_knife_attacks", "1", "Allows or disallows the ability for random knife attacks when bot is rushing and no enemy is nearby.");
ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "30", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f);
// game console variables // game console variables
ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef);
ConVar mp_flashlight ("mp_flashlight", nullptr, Var::GameRef); ConVar mp_flashlight ("mp_flashlight", nullptr, Var::GameRef);
@ -327,7 +329,7 @@ void Bot::updatePickups () {
// this function finds Items to collect or use in the near of a bot // this function finds Items to collect or use in the near of a bot
// don't try to pickup anything while on ladder or trying to escape from bomb... // don't try to pickup anything while on ladder or trying to escape from bomb...
if (isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || m_seeEnemyTime + 3.0f > game.time () || !game.isNullEntity (m_enemy) || !bots.hasIntrestingEntities ()) { if (isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || !cv_pickup_best.bool_ () || cv_jasonmode.bool_ () || m_seeEnemyTime + 3.0f < game.time () || !game.isNullEntity (m_enemy) || !bots.hasIntrestingEntities ()) {
m_pickupItem = nullptr; m_pickupItem = nullptr;
m_pickupType = Pickup::None; m_pickupType = Pickup::None;
@ -758,9 +760,14 @@ Vector Bot::getCampDirection (const Vector &dest) {
} }
if (graph.exists (lookAtWaypoint)) { if (graph.exists (lookAtWaypoint)) {
return graph[lookAtWaypoint].origin + pev->view_ofs; return graph[lookAtWaypoint].origin;
} }
} }
auto dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
if (graph.exists (dangerIndex)) {
return graph[dangerIndex].origin;
}
return nullptr; return nullptr;
} }
@ -1548,15 +1555,6 @@ void Bot::updateEmotions () {
} }
void Bot::overrideConditions () { void Bot::overrideConditions () {
if (!usesKnife () && m_difficulty >= Difficulty::Normal && ((m_aimFlags & AimFlags::Enemy) || (m_states & Sense::SeeingEnemy)) && !cv_jasonmode.bool_ () && getCurrentTaskId () != Task::Camp && getCurrentTaskId () != Task::SeekCover && !isOnLadder ()) {
m_moveToGoal = false; // don't move to goal
m_navTimeset = game.time ();
if (util.isPlayer (m_enemy) || (cv_attack_monsters.bool_ () && util.isMonster (m_enemy))) {
attackMovement ();
}
}
// check if we need to escape from bomb // check if we need to escape from bomb
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_notKilled && getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::Camp && isOutOfBombTimer ()) { if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_notKilled && getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::Camp && isOutOfBombTimer ()) {
completeTask (); // complete current task completeTask (); // complete current task
@ -1566,7 +1564,7 @@ void Bot::overrideConditions () {
} }
// special handling, if we have a knife in our hands // special handling, if we have a knife in our hands
if ((bots.getRoundStartTime () + 6.0f > game.time () || !hasAnyWeapons ()) && usesKnife () && (util.isPlayer (m_enemy) || (cv_attack_monsters.bool_ () && util.isMonster (m_enemy)))) { if ((usesKnife () || !hasAnyWeapons ()) && (util.isPlayer (m_enemy) || (cv_attack_monsters.bool_ () && util.isMonster (m_enemy)))) {
float length = pev->origin.distance2d (m_enemy->v.origin); float length = pev->origin.distance2d (m_enemy->v.origin);
// do waypoint movement if enemy is not reachable with a knife // do waypoint movement if enemy is not reachable with a knife
@ -1574,15 +1572,20 @@ void Bot::overrideConditions () {
int nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin); int nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin);
if (nearestToEnemyPoint != kInvalidNodeIndex && nearestToEnemyPoint != m_currentNodeIndex && cr::abs (graph[nearestToEnemyPoint].origin.z - m_enemy->v.origin.z) < 16.0f) { if (nearestToEnemyPoint != kInvalidNodeIndex && nearestToEnemyPoint != m_currentNodeIndex && cr::abs (graph[nearestToEnemyPoint].origin.z - m_enemy->v.origin.z) < 16.0f) {
float taskTime = game.time () + length / pev->maxspeed * 0.5f;
if (getCurrentTaskId () != Task::MoveToPosition && !cr::fequal (getTask ()->desire, TaskPri::Hide)) { if (getCurrentTaskId () != Task::MoveToPosition && !cr::fequal (getTask ()->desire, TaskPri::Hide)) {
startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, taskTime, true); startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, game.time () + length / (m_moveSpeed * 2.0f), true);
} }
m_isEnemyReachable = false; else {
m_enemy = nullptr; if (getCurrentTaskId () == Task::MoveToPosition && getTask ()->data != nearestToEnemyPoint) {
clearTask (Task::MoveToPosition);
m_enemyIgnoreTimer = taskTime; startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, game.time () + length / (m_moveSpeed * 2.0f), true);
}
}
}
}
else {
if (length <= 100.0f && (m_states & Sense::SeeingEnemy) && getCurrentTaskId () == Task::MoveToPosition) {
clearTask (Task::MoveToPosition); // remove any move tasks
} }
} }
} }
@ -1596,7 +1599,7 @@ void Bot::overrideConditions () {
} }
// special handling for reloading // special handling for reloading
if (m_reloadState != Reload::None && m_isReloading && ((pev->button | m_oldButtons) & IN_RELOAD)) { if (m_reloadState != Reload::None && m_isReloading) {
if (m_seeEnemyTime + 4.0f < game.time () && (m_states & (Sense::SuspectEnemy | Sense::HearingEnemy))) { if (m_seeEnemyTime + 4.0f < game.time () && (m_states & (Sense::SuspectEnemy | Sense::HearingEnemy))) {
m_moveSpeed = 0.0f; m_moveSpeed = 0.0f;
m_strafeSpeed = 0.0f; m_strafeSpeed = 0.0f;
@ -1714,7 +1717,7 @@ void Bot::setConditions () {
auto pathLength = 0; auto pathLength = 0;
auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength); auto nodeIndex = findAimingNode (m_lastEnemyOrigin, pathLength);
if (graph.exists (nodeIndex) && pathLength < kMaxBucketsInsidePos * 2 && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::square (384.0f)) { if (graph.exists (nodeIndex) && pathLength < cv_max_nodes_for_predict.int_ () && pev->origin.distanceSq (graph[nodeIndex].origin) > cr::square (384.0f)) {
m_aimFlags |= AimFlags::PredictPath; m_aimFlags |= AimFlags::PredictPath;
} }
} }
@ -2655,7 +2658,7 @@ void Bot::updateAimDir () {
auto pathLength = 0; auto pathLength = 0;
auto aimNode = findAimingNode (m_lastEnemyOrigin, pathLength); auto aimNode = findAimingNode (m_lastEnemyOrigin, pathLength);
if (graph.exists (aimNode) && pathLength < kMaxBucketsInsidePos * 2) { if (graph.exists (aimNode) && pathLength < cv_max_nodes_for_predict.int_ ()) {
m_lookAt = graph[aimNode].origin; m_lookAt = graph[aimNode].origin;
m_lookAtSafe = m_lookAt; m_lookAtSafe = m_lookAt;
@ -2688,7 +2691,7 @@ void Bot::updateAimDir () {
m_lookAt = m_destOrigin; m_lookAt = m_destOrigin;
} }
} }
else if (m_seeEnemyTime + 3.0f > game.time () && !m_lastEnemyOrigin.empty ()){ else if (m_seeEnemyTime + 3.0f > game.time () && !m_lastEnemyOrigin.empty ()) {
m_lookAt = m_lastEnemyOrigin;; m_lookAt = m_lastEnemyOrigin;;
} }
else { else {
@ -2821,6 +2824,12 @@ void Bot::frame () {
kick (); kick ();
return; return;
} }
// clear enemy far away
if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && pev->origin.distanceSq (m_lastEnemyOrigin) >= cr::square (2048.0)) {
m_lastEnemy = nullptr;
m_lastEnemyOrigin = nullptr;
}
m_slowFrameTimestamp = game.time () + 0.5f; m_slowFrameTimestamp = game.time () + 0.5f;
} }
@ -2839,7 +2848,7 @@ void Bot::update () {
if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) { if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) {
m_hasC4 = !!(pev->weapons & cr::bit (Weapon::C4)); m_hasC4 = !!(pev->weapons & cr::bit (Weapon::C4));
if (m_hasC4 && cv_ignore_objectives.bool_ ()) { if (m_hasC4 && (cv_ignore_objectives.bool_ () || cv_jasonmode.bool_ ())) {
m_hasC4 = false; m_hasC4 = false;
} }
} }
@ -2937,7 +2946,7 @@ void Bot::normal_ () {
} }
// bots rushing with knife, when have no enemy (thanks for idea to nicebot project) // bots rushing with knife, when have no enemy (thanks for idea to nicebot project)
if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) { if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !hasHostage () && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) {
if (rg.chance (40)) { if (rg.chance (40)) {
pev->button |= IN_ATTACK; pev->button |= IN_ATTACK;
} }
@ -2976,7 +2985,7 @@ void Bot::normal_ () {
} }
// reached waypoint is a camp waypoint // reached waypoint is a camp waypoint
if ((m_pathFlags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && cv_camping_allowed.bool_ ()) { if ((m_pathFlags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && cv_camping_allowed.bool_ () && !isKnifeMode ()) {
// check if bot has got a primary weapon and hasn't camped before // check if bot has got a primary weapon and hasn't camped before
if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !hasHostage ()) { if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !hasHostage ()) {
@ -3129,13 +3138,13 @@ void Bot::normal_ () {
} }
} }
else { else {
if (!isDucking () && !cr::fequal (m_minSpeed, pev->maxspeed) && m_minSpeed > 1.0f) { if (!isDucking () && !usesKnife () && !cr::fequal (m_minSpeed, pev->maxspeed) && m_minSpeed > 1.0f) {
m_moveSpeed = m_minSpeed; m_moveSpeed = m_minSpeed;
} }
} }
float shiftSpeed = getShiftSpeed (); float shiftSpeed = getShiftSpeed ();
if ((!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !cv_jasonmode.bool_ () && !bots.isBombPlanted ()) { if ((!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) {
m_moveSpeed = shiftSpeed; m_moveSpeed = shiftSpeed;
} }
@ -3248,7 +3257,7 @@ void Bot::huntEnemy_ () {
} }
// bots skill higher than 60? // bots skill higher than 60?
if (cv_walking_allowed.bool_ () && mp_footsteps.bool_ () && m_difficulty >= Difficulty::Normal && !cv_jasonmode.bool_ ()) { if (cv_walking_allowed.bool_ () && mp_footsteps.bool_ () && m_difficulty >= Difficulty::Normal && !isKnifeMode ()) {
// then make him move slow if near enemy // then make him move slow if near enemy
if (!(m_currentTravelFlags & PathFlag::Jump)) { if (!(m_currentTravelFlags & PathFlag::Jump)) {
if (m_currentNodeIndex != kInvalidNodeIndex) { if (m_currentNodeIndex != kInvalidNodeIndex) {
@ -3359,8 +3368,8 @@ void Bot::attackEnemy_ () {
ignoreCollision (); ignoreCollision ();
attackMovement (); attackMovement ();
if (usesKnife () && !m_lastEnemyOrigin.empty ()) { if (usesKnife () && !m_enemyOrigin.empty ()) {
m_destOrigin = m_lastEnemyOrigin; m_destOrigin = m_enemyOrigin;
} }
} }
else { else {
@ -3939,7 +3948,7 @@ void Bot::followUser_ () {
} }
m_aimFlags |= AimFlags::Nav; m_aimFlags |= AimFlags::Nav;
if (cv_walking_allowed.bool_ () && m_targetEntity->v.maxspeed < m_moveSpeed && !cv_jasonmode.bool_ ()) { if (cv_walking_allowed.bool_ () && m_targetEntity->v.maxspeed < m_moveSpeed && !isKnifeMode ()) {
m_moveSpeed = getShiftSpeed (); m_moveSpeed = getShiftSpeed ();
} }
@ -3957,7 +3966,7 @@ void Bot::followUser_ () {
clearSearchNodes (); clearSearchNodes ();
int destIndex = graph.getNearest (m_targetEntity->v.origin); int destIndex = graph.getNearest (m_targetEntity->v.origin);
auto points = graph.searchRadius (200.0f, m_targetEntity->v.origin); auto points = graph.getNarestInRadius (200.0f, m_targetEntity->v.origin);
for (auto &newIndex : points) { for (auto &newIndex : points) {
// if waypoint not yet used, assign it as dest // if waypoint not yet used, assign it as dest
@ -4268,15 +4277,15 @@ void Bot::escapeFromBomb_ () {
int lastSelectedGoal = kInvalidNodeIndex, minPathDistance = kInfiniteDistanceLong; int lastSelectedGoal = kInvalidNodeIndex, minPathDistance = kInfiniteDistanceLong;
float safeRadius = rg.get (1513.0f, 2048.0f); float safeRadius = rg.get (1513.0f, 2048.0f);
for (int i = 0; i < graph.length (); ++i) { for (const auto &path : graph) {
if (graph[i].origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (i)) { if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) {
continue; continue;
} }
int pathDistance = graph.getPathDist (m_currentNodeIndex, i); int pathDistance = graph.getPathDist (m_currentNodeIndex, path.number);
if (minPathDistance > pathDistance) { if (minPathDistance > pathDistance) {
minPathDistance = pathDistance; minPathDistance = pathDistance;
lastSelectedGoal = i; lastSelectedGoal = path.number;
} }
} }
@ -4606,8 +4615,7 @@ void Bot::checkSpawnConditions () {
} }
if (m_difficulty >= Difficulty::Normal && rg.chance (m_personality == Personality::Rusher ? 99 : 50) && !m_isReloading && game.mapIs (MapFlags::HostageRescue | MapFlags::Demolition | MapFlags::Escape | MapFlags::Assassination)) { if (m_difficulty >= Difficulty::Normal && rg.chance (m_personality == Personality::Rusher ? 99 : 50) && !m_isReloading && game.mapIs (MapFlags::HostageRescue | MapFlags::Demolition | MapFlags::Escape | MapFlags::Assassination)) {
if (cv_jasonmode.bool_ ()) { if (isKnifeMode ()) {
selectSecondary ();
dropCurrentWeapon (); dropCurrentWeapon ();
} }
else { else {
@ -4745,7 +4753,7 @@ void Bot::logic () {
updateLookAngles (); // and turn to chosen aim direction updateLookAngles (); // and turn to chosen aim direction
// the bots wants to fire at something? // the bots wants to fire at something?
if (m_shootAtDeadTime > game.time () || (m_wantsToFire && !m_isUsingGrenade && (usesKnife () || m_shootTime <= game.time ()))) { if (m_shootAtDeadTime > game.time () || (m_wantsToFire && !m_isUsingGrenade && (m_shootTime <= game.time ()))) {
fireWeapons (); // if bot didn't fire a bullet try again next frame fireWeapons (); // if bot didn't fire a bullet try again next frame
} }
@ -4794,7 +4802,7 @@ void Bot::logic () {
} }
// ensure we're not stuck destroying/picking something // ensure we're not stuck destroying/picking something
if (m_navTimeset + getEstimatedNodeReachTime () - m_frameInterval * 2.0f < game.time () && !(m_states & Sense::SeeingEnemy)) { if (m_navTimeset + getEstimatedNodeReachTime () + 2.0f < game.time () && !(m_states & Sense::SeeingEnemy)) {
ensureEntitiesClear (); ensureEntitiesClear ();
} }
translateInput (); translateInput ();
@ -4848,6 +4856,10 @@ void Bot::showDebugOverlay () {
static HashMap <int32_t, String> personalities; static HashMap <int32_t, String> personalities;
static HashMap <int32_t, String> flags; static HashMap <int32_t, String> flags;
auto boolValue = [] (const bool test) {
return test ? "Yes" : "No";
};
if (tasks.empty ()) { if (tasks.empty ()) {
tasks[Task::Normal] = "Normal"; tasks[Task::Normal] = "Normal";
tasks[Task::Pause] = "Pause"; tasks[Task::Pause] = "Pause";
@ -4919,7 +4931,7 @@ void Bot::showDebugOverlay () {
auto weapon = util.weaponIdToAlias (m_currentWeapon); auto weapon = util.weaponIdToAlias (m_currentWeapon);
String debugData; String debugData;
debugData.assignf ("\n\n\n\n\n%s (H:%.1f/A:%.1f)- Task: %d=%s Desire:%.02f\nItem: %s Clip: %d Ammo: %d%s Money: %d AimFlags: %s\nSP=%.02f SSP=%.02f I=%d PG=%d G=%d T: %.02f MT: %d\nEnemy=%s Pickup=%s Type=%s\n", pev->netname.chars (), m_healthValue, pev->armorvalue, taskID, tasks[taskID], getTask ()->desire, weapon, getAmmoInClip (), getAmmo (), m_isReloading ? " (R)" : "", m_moneyAmount, aimFlags.trim (), m_moveSpeed, m_strafeSpeed, index, m_prevGoalIndex, goal, m_navTimeset - game.time (), pev->movetype, enemy, pickup, personalities[m_personality]); debugData.assignf ("\n\n\n\n\n%s (H:%.1f/A:%.1f)- Task: %d=%s Desire:%.02f\nItem: %s Clip: %d Ammo: %d%s Money: %d AimFlags: %s\nSP=%.02f SSP=%.02f I=%d PG=%d G=%d T: %.02f MT: %d\nEnemy=%s Pickup=%s Type=%s Terrain=%s Stuck=%s\n", pev->netname.chars (), m_healthValue, pev->armorvalue, taskID, tasks[taskID], getTask ()->desire, weapon, getAmmoInClip (), getAmmo (), m_isReloading ? " (R)" : "", m_moneyAmount, aimFlags.trim (), m_moveSpeed, m_strafeSpeed, index, m_prevGoalIndex, goal, m_navTimeset - game.time (), pev->movetype, enemy, pickup, personalities[m_personality], boolValue (m_checkTerrain), boolValue (m_isStuck));
MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, overlayEntity) MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, overlayEntity)
.writeByte (TE_TEXTMESSAGE) .writeByte (TE_TEXTMESSAGE)
@ -4957,7 +4969,7 @@ void Bot::showDebugOverlay () {
} }
bool Bot::hasHostage () { bool Bot::hasHostage () {
if (cv_ignore_objectives.bool_ () || !game.mapIs (MapFlags::Demolition)) { if (cv_ignore_objectives.bool_ () || game.mapIs (MapFlags::Demolition)) {
return false; return false;
} }
@ -4983,7 +4995,7 @@ int Bot::getAmmo (int id) {
const auto &prop = conf.getWeaponProp (id); const auto &prop = conf.getWeaponProp (id);
if (prop.ammo1 == -1 || prop.ammo1 > kMaxWeapons - 1) { if (prop.ammo1 == -1 || prop.ammo1 > kMaxWeapons - 1) {
return 0; return -1;
} }
return m_ammo[prop.ammo1]; return m_ammo[prop.ammo1];
} }
@ -5500,7 +5512,7 @@ void Bot::updateHearing () {
// did the bot hear someone ? // did the bot hear someone ?
if (player != nullptr && util.isPlayer (player)) { if (player != nullptr && util.isPlayer (player)) {
// change to best weapon if heard something // change to best weapon if heard something
if (m_shootTime < game.time () - 5.0f && isOnFloor () && m_currentWeapon != Weapon::C4 && m_currentWeapon != Weapon::Explosive && m_currentWeapon != Weapon::Smoke && m_currentWeapon != Weapon::Flashbang && !cv_jasonmode.bool_ ()) { if (m_shootTime < game.time () - 5.0f && isOnFloor () && m_currentWeapon != Weapon::C4 && m_currentWeapon != Weapon::Explosive && m_currentWeapon != Weapon::Smoke && m_currentWeapon != Weapon::Flashbang && !isKnifeMode ()) {
selectBestWeapon (); selectBestWeapon ();
} }

View file

@ -245,6 +245,7 @@ bool Bot::lookupEnemies () {
newEnemy = player; newEnemy = player;
} }
} }
auto distanceScale = usesKnife () ? 1.0f : 0.75f;
// the old enemy is no longer visible or // the old enemy is no longer visible or
if (game.isNullEntity (newEnemy)) { if (game.isNullEntity (newEnemy)) {
@ -271,7 +272,7 @@ bool Bot::lookupEnemies () {
float scaleFactor = (1.0f / calculateScaleFactor (intresting)); float scaleFactor = (1.0f / calculateScaleFactor (intresting));
float distance = intresting->v.origin.distanceSq (pev->origin) * scaleFactor; float distance = intresting->v.origin.distanceSq (pev->origin) * scaleFactor;
if (distance * 0.65f < nearestDistance) { if (distance * distanceScale < nearestDistance) {
nearestDistance = distance; nearestDistance = distance;
newEnemy = intresting; newEnemy = intresting;
} }
@ -304,7 +305,7 @@ bool Bot::lookupEnemies () {
} }
float distance = player->v.origin.distanceSq (pev->origin); float distance = player->v.origin.distanceSq (pev->origin);
if (distance * 0.65f < nearestDistance) { if (distance * distanceScale < nearestDistance) {
nearestDistance = distance; nearestDistance = distance;
newEnemy = player; newEnemy = player;
@ -315,7 +316,7 @@ bool Bot::lookupEnemies () {
} }
} }
} }
m_enemyUpdateTime = cr::clamp (game.time () + m_frameInterval * 25.0f, 0.5f, 0.75f); m_enemyUpdateTime = game.time () + cr::clamp (m_frameInterval * 3.0f, 0.2f, 0.64f);
if (game.isNullEntity (newEnemy) && !game.isNullEntity (shieldEnemy)) { if (game.isNullEntity (newEnemy) && !game.isNullEntity (shieldEnemy)) {
newEnemy = shieldEnemy; newEnemy = shieldEnemy;
@ -445,16 +446,20 @@ Vector Bot::getBodyOffsetError (float distance) {
} }
if (m_aimErrorTime < game.time ()) { if (m_aimErrorTime < game.time ()) {
const float error = distance / (cr::clamp (static_cast <float> (m_difficulty), 1.0f, 4.0f) * 1000.0f); const float hitError = distance / (cr::clamp (m_difficulty, 1, 4) * 1000.0f);
auto &maxs = m_enemy->v.maxs, &mins = m_enemy->v.mins; auto &maxs = m_enemy->v.maxs, &mins = m_enemy->v.mins;
m_aimLastError = Vector (rg.get (mins.x * error, maxs.x * error), rg.get (mins.y * error, maxs.y * error), rg.get (mins.z * error, maxs.z * error)); m_aimLastError = Vector (rg.get (mins.x * hitError, maxs.x * hitError), rg.get (mins.y * hitError, maxs.y * hitError), rg.get (mins.z * hitError, maxs.z * hitError));
m_aimErrorTime = game.time () + rg.get (1.0f, 1.2f);
auto &aimError = conf.getDifficultyTweaks (m_difficulty) ->aimError;
m_aimLastError += Vector (rg.get (-aimError.x, aimError.x), rg.get (-aimError.y, aimError.y), rg.get (-aimError.z, aimError.z));
m_aimErrorTime = game.time () + rg.get (1.5f, 2.0f);
} }
return m_aimLastError; return m_aimLastError;
} }
const Vector &Bot::getEnemyBodyOffset () { Vector Bot::getEnemyBodyOffset () {
// the purpose of this function, is to make bot aiming not so ideal. it's mutate m_enemyOrigin enemy vector // the purpose of this function, is to make bot aiming not so ideal. it's mutate m_enemyOrigin enemy vector
// returned from visibility check function. // returned from visibility check function.
@ -474,11 +479,15 @@ const Vector &Bot::getEnemyBodyOffset () {
m_enemyParts &= ~Visibility::Head; m_enemyParts &= ~Visibility::Head;
} }
const auto headOffset = [] (edict_t *e) {
return e->v.absmin.z + e->v.size.z * 0.81f;
};
Vector spot = m_enemy->v.origin; Vector spot = m_enemy->v.origin;
Vector compensation = 1.0f * m_frameInterval * m_enemy->v.velocity - 1.0f * m_frameInterval * pev->velocity; Vector compensation = nullptr;
if (!usesSniper () && distance > kDoubleSprayDistance) { if (!usesSniper () && distance > kDoubleSprayDistance) {
compensation *= m_frameInterval; compensation = (m_enemy->v.velocity - pev->velocity) * m_frameInterval * 2.0f;
compensation.z = 0.0f; compensation.z = 0.0f;
} }
else { else {
@ -490,71 +499,67 @@ const Vector &Bot::getEnemyBodyOffset () {
spot += getBodyOffsetError (distance); spot += getBodyOffsetError (distance);
} }
else if (util.isPlayer (m_enemy)) { else if (util.isPlayer (m_enemy)) {
const float highOffset = m_kpdRatio < 1.0f ? 1.5f : 0.0f;
// now take in account different parts of enemy body // now take in account different parts of enemy body
if (m_enemyParts & (Visibility::Head | Visibility::Body)) { if (m_enemyParts & (Visibility::Head | Visibility::Body)) {
auto headshotPct = conf.getDifficultyTweaks (m_difficulty)->headshotPct; auto headshotPct = conf.getDifficultyTweaks (m_difficulty)->headshotPct;
auto onLoosingStreak = (m_fearLevel > m_agressionLevel || m_kpdRatio < 1.25f);
// reduce headshot percent in case we're play too good
if (!onLoosingStreak) {
headshotPct = cr::abs (headshotPct - headshotPct / 4);
}
// now check is our skill match to aim at head, else aim at enemy body // now check is our skill match to aim at head, else aim at enemy body
if (rg.chance (headshotPct)) { if (rg.chance (headshotPct)) {
if (onLoosingStreak) { spot = m_enemyOrigin + getCustomHeight (distance);
spot = m_enemyOrigin + Vector (0.0f, 0.0f, getEnemyBodyOffsetCorrection (distance));
}
else {
spot = m_enemyOrigin;
}
} }
else { else {
spot.z += highOffset; spot = m_enemy->v.origin;
spot.z -= 3.0f;
} }
} }
else if (m_enemyParts & Visibility::Body) { else if (m_enemyParts & Visibility::Body) {
spot = m_enemyOrigin + Vector (0.0f, 0.0f, getEnemyBodyOffsetCorrection (distance)); spot = m_enemy->v.origin;
} }
else if (m_enemyParts & Visibility::Other) { else if (m_enemyParts & Visibility::Other) {
spot = m_enemyOrigin; spot = m_enemyOrigin;
} }
else if (m_enemyParts & Visibility::Head) { else if (m_enemyParts & Visibility::Head) {
spot = m_enemyOrigin + Vector (0.0f, 0.0f, getEnemyBodyOffsetCorrection (distance)); spot = m_enemyOrigin + getCustomHeight (distance);
} }
} }
Vector newSpot = spot;
m_enemyOrigin = spot + compensation; if (m_difficulty < Difficulty::Expert && isEnemyInSight (newSpot)) {
m_lastEnemyOrigin = spot + compensation; spot = newSpot + ((spot - newSpot) * 0.01f); // gradually adjust the aiming direction
}
spot += compensation;
if (usesKnife () && m_difficulty >= Difficulty::Normal) {
spot.z = headOffset (m_enemy);
}
m_lastEnemyOrigin = spot;
// add some error to unskilled bots // add some error to unskilled bots
if (m_difficulty < Difficulty::Hard) { if (m_difficulty < Difficulty::Hard) {
m_enemyOrigin += getBodyOffsetError (distance); spot += getBodyOffsetError (distance);
} }
return m_enemyOrigin; return spot;
} }
float Bot::getEnemyBodyOffsetCorrection (float distance) { Vector Bot::getCustomHeight (float distance) {
enum DistanceIndex { enum DistanceIndex {
Long, Middle, Short Long, Middle, Short
}; };
static float offsetRanges[9][3] = { static constexpr float offsetRanges[9][3] = {
{ 0.0f, 0.0f, 0.0f }, // none { 0.0f, 0.0f, 0.0f }, // none
{ 0.0f, 0.0f, 4.0f }, // melee { 0.0f, 0.0f, 0.0f }, // melee
{ 3.5f, 2.0f, 1.5f }, // pistol { 1.5f, -3.0f, -4.5f }, // pistol
{ 9.5f, 5.0f, -8.0f }, // shotgun { 6.5f, 6.0f, -2.0f }, // shotgun
{ 4.5f -3.5f, -5.0f }, // zoomrifle { 2.5f -7.5f, -9.5f }, // zoomrifle
{ 5.5f, -3.0f, -5.5f }, // rifle { 2.5f, -7.5f, -9.5f }, // rifle
{ 5.5f, -2.5f, -5.5f }, // smg { 2.5f, -7.5f, -9.5f }, // smg
{ 3.5f, 1.5f, -6.0f }, // sniper { 0.0f, -2.5f, -6.0f }, // sniper
{ 2.5f, -4.0f, -9.0f } // heavy { 1.5f, -4.0f, -9.0f } // heavy
}; };
// only highskilled bots do that // only highskilled bots do that
if (m_difficulty != Difficulty::Expert) { if (m_difficulty != Difficulty::Expert || (m_enemy->v.flags & FL_DUCKING)) {
return 0.0f; return 0.0f;
} }
@ -568,7 +573,7 @@ float Bot::getEnemyBodyOffsetCorrection (float distance) {
else if (distance > kSprayDistance && distance <= kDoubleSprayDistance) { else if (distance > kSprayDistance && distance <= kDoubleSprayDistance) {
distanceIndex = DistanceIndex::Middle; distanceIndex = DistanceIndex::Middle;
} }
return offsetRanges[m_weaponType][distanceIndex]; return { 0.0f, 0.0f, offsetRanges[m_weaponType][distanceIndex] };
} }
bool Bot::isFriendInLineOfFire (float distance) { bool Bot::isFriendInLineOfFire (float distance) {
@ -742,7 +747,7 @@ bool Bot::isPenetrableObstacle3 (const Vector &dest) {
bool Bot::needToPauseFiring (float distance) { bool Bot::needToPauseFiring (float distance) {
// returns true if bot needs to pause between firing to compensate for punchangle & weapon spread // returns true if bot needs to pause between firing to compensate for punchangle & weapon spread
if (usesSniper ()) { if (usesSniper () || m_isUsingGrenade) {
return false; return false;
} }
@ -761,22 +766,21 @@ bool Bot::needToPauseFiring (float distance) {
return false; return false;
} }
else if (distance < kDoubleSprayDistance) { else if (distance < kDoubleSprayDistance) {
offset = 8.0f; offset = 2.75f;
} }
const float xPunch = cr::deg2rad (pev->punchangle.x); const float xPunch = cr::square (cr::deg2rad (pev->punchangle.x));
const float yPunch = cr::deg2rad (pev->punchangle.y); const float yPunch = cr::square (cr::deg2rad (pev->punchangle.y));
const float interval = m_frameInterval; const float interval = m_frameInterval;
const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f; const float tolerance = (100.0f - static_cast <float> (m_difficulty) * 25.0f) / 99.0f;
const float baseTime = distance > kDoubleSprayDistance ? 0.65f : 0.48f;
const float maxRecoil = static_cast <float> (conf.getDifficultyTweaks (m_difficulty)->maxRecoil);
// check if we need to compensate recoil // check if we need to compensate recoil
if (cr::tanf (cr::sqrtf (cr::abs (xPunch * xPunch) + cr::abs (yPunch * yPunch))) * distance > offset + 30.0f + tolerance) { if (cr::tanf (cr::sqrtf (cr::abs (xPunch) + cr::abs (yPunch))) * distance > offset + maxRecoil + tolerance) {
if (m_firePause < game.time ()) { if (m_firePause < game.time ()) {
m_firePause = rg.get (0.65f, 0.65f + 0.3f * tolerance); m_firePause = game.time () + rg.get (baseTime, baseTime + maxRecoil * 0.01f * tolerance) - interval;
} }
m_firePause -= interval;
m_firePause += game.time ();
return true; return true;
} }
return false; return false;
@ -874,10 +878,10 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
} }
// need to care for burst fire? // need to care for burst fire?
if (distance < kSprayDistance || m_blindTime > game.time ()) { if (distance < kSprayDistance || m_blindTime > game.time () || usesKnife ()) {
if (id == Weapon::Knife) { if (id == Weapon::Knife) {
if (distance < 64.0f) { if (distance < 72.0f) {
if (rg.chance (30) || hasShield ()) { if (rg.chance (40) || hasShield ()) {
pev->button |= IN_ATTACK; // use primary attack pev->button |= IN_ATTACK; // use primary attack
} }
else { else {
@ -901,16 +905,16 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
m_shootTime = game.time (); m_shootTime = game.time ();
} }
else { else {
if (needToPauseFiring (distance)) {
return;
}
// don't attack with knife over long distance // don't attack with knife over long distance
if (id == Weapon::Knife) { if (id == Weapon::Knife) {
m_shootTime = game.time (); m_shootTime = game.time ();
return; return;
} }
if (needToPauseFiring (distance)) {
return;
}
if (tab[choosen].primaryFireHold) { if (tab[choosen].primaryFireHold) {
m_shootTime = game.time (); m_shootTime = game.time ();
m_zoomCheckTime = game.time (); m_zoomCheckTime = game.time ();
@ -922,8 +926,8 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
pev->button |= IN_ATTACK; pev->button |= IN_ATTACK;
} }
const float minDelay[] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.6f }; constexpr float minDelay[] = { 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.6f };
const float maxDelay[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.7f }; constexpr float maxDelay[] = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.7f };
const int offset = cr::abs <int> (m_difficulty * 25 / 20 - 5); const int offset = cr::abs <int> (m_difficulty * 25 / 20 - 5);
@ -935,26 +939,29 @@ void Bot::selectWeapons (float distance, int index, int id, int choosen) {
void Bot::fireWeapons () { void Bot::fireWeapons () {
// this function will return true if weapon was fired, false otherwise // this function will return true if weapon was fired, false otherwise
//
// do not handle this if with grenade, as it's done it throw grendate task
if (m_isUsingGrenade) {
return;
}
float distance = m_lookAt.distance (getEyesPos ()); // how far away is the enemy? float distance = m_lookAt.distance (getEyesPos ()); // how far away is the enemy?
// or if friend in line of fire, stop this too but do not update shoot time // or if friend in line of fire, stop this too but do not update shoot time
if (!game.isNullEntity (m_enemy)) { if (isFriendInLineOfFire (distance)) {
if (isFriendInLineOfFire (distance)) { m_fightStyle = Fight::Strafe;
m_fightStyle = Fight::Strafe; m_lastFightStyleCheck = game.time ();
m_lastFightStyleCheck = game.time ();
return; return;
}
} }
auto tab = conf.getRawWeapons (); auto tab = conf.getRawWeapons ();
edict_t *enemy = m_enemy; auto enemy = m_enemy;
int selectId = Weapon::Knife, selectIndex = 0, choosenWeapon = 0; int selectId = Weapon::Knife, selectIndex = 0, choosenWeapon = 0;
int weapons = pev->weapons; int weapons = pev->weapons;
// if jason mode use knife only // if jason mode use knife only
if (cv_jasonmode.bool_ ()) { if (isKnifeMode ()) {
selectWeapons (distance, selectIndex, selectId, choosenWeapon); selectWeapons (distance, selectIndex, selectId, choosenWeapon);
return; return;
} }
@ -997,7 +1004,7 @@ void Bot::fireWeapons () {
m_reloadState = Reload::Primary; m_reloadState = Reload::Primary;
m_reloadCheckTime = game.time (); m_reloadCheckTime = game.time ();
if (rg.chance (cr::abs (m_difficulty * 25 - 100)) && rg.chance (15)) { if (rg.chance (cr::abs (m_difficulty * 25 - 100)) && rg.chance (20)) {
pushRadioMessage (Radio::NeedBackup); pushRadioMessage (Radio::NeedBackup);
} }
} }
@ -1015,30 +1022,35 @@ bool Bot::isWeaponBadAtDistance (int weaponIndex, float distance) {
// this function checks, is it better to use pistol instead of current primary weapon // this function checks, is it better to use pistol instead of current primary weapon
// to attack our enemy, since current weapon is not very good in this situation. // to attack our enemy, since current weapon is not very good in this situation.
auto &info = conf.getWeapons (); // do not switch weapons when crossing the distance line
if (m_lastBadWeaponSwitchTime + 3.0f < game.time ()) {
auto &info = conf.getWeapons ();
if (m_difficulty < Difficulty::Normal || !hasSecondaryWeapon ()) { if (m_difficulty < Difficulty::Normal || !hasSecondaryWeapon ()) {
return false; return false;
} }
auto weaponType = info[weaponIndex].type; auto weaponType = info[weaponIndex].type;
if (weaponType == WeaponType::Melee) { if (weaponType == WeaponType::Melee) {
return false; return false;
} }
// check is ammo available for secondary weapon // check is ammo available for secondary weapon
if (m_ammoInClip[info[bestSecondaryCarried ()].id] >= 3) { if (m_ammoInClip[info[bestSecondaryCarried ()].id] >= 3) {
return false; return false;
} }
// better use pistol in short range distances, when using sniper weapons // better use pistol in short range distances, when using sniper weapons
if (weaponType == WeaponType::Sniper && distance < 450.0f) { if (weaponType == WeaponType::Sniper && distance < 400.0f) {
return true; m_lastBadWeaponSwitchTime = game.time ();
} return true;
}
// shotguns is too inaccurate at long distances, so weapon is bad // shotguns is too inaccurate at long distances, so weapon is bad
if (weaponType == WeaponType::Shotgun && distance > 750.0f) { if (weaponType == WeaponType::Shotgun && distance > 750.0f) {
return true; m_lastBadWeaponSwitchTime = game.time ();
return true;
}
} }
return false; return false;
} }
@ -1051,7 +1063,7 @@ void Bot::focusEnemy () {
// aim for the head and/or body // aim for the head and/or body
m_lookAt = getEnemyBodyOffset (); m_lookAt = getEnemyBodyOffset ();
if (m_enemySurpriseTime > game.time () && !usesKnife ()) { if (m_enemySurpriseTime > game.time ()) {
return; return;
} }
float distance = m_lookAt.distance2d (getEyesPos ()); // how far away is the enemy scum? float distance = m_lookAt.distance2d (getEyesPos ()); // how far away is the enemy scum?
@ -1061,9 +1073,6 @@ void Bot::focusEnemy () {
if (distance < 80.0f) { if (distance < 80.0f) {
m_wantsToFire = true; m_wantsToFire = true;
} }
else if (distance > 120.0f) {
m_wantsToFire = false;
}
} }
else { else {
m_wantsToFire = true; m_wantsToFire = true;
@ -1100,6 +1109,11 @@ void Bot::attackMovement () {
return; return;
} }
// use enemy as dest origin if with knife
if (usesKnife ()) {
m_destOrigin = m_enemy->v.origin;
}
if (m_lastUsedNodesTime - m_frameInterval > game.time ()) { if (m_lastUsedNodesTime - m_frameInterval > game.time ()) {
return; return;
} }
@ -1136,10 +1150,6 @@ void Bot::attackMovement () {
m_moveSpeed = pev->maxspeed; m_moveSpeed = pev->maxspeed;
} }
if (distance < 96.0f && !usesKnife ()) {
m_moveSpeed = -pev->maxspeed;
}
if (m_lastFightStyleCheck + 3.0f < game.time ()) { if (m_lastFightStyleCheck + 3.0f < game.time ()) {
if (usesSniper ()) { if (usesSniper ()) {
m_fightStyle = Fight::Stay; m_fightStyle = Fight::Stay;
@ -1167,7 +1177,7 @@ void Bot::attackMovement () {
} }
} }
} }
else if (rg.get (0, 100) < (isInNarrowPlace () ? 25 : 75)) { else if (rg.get (0, 100) < (isInNarrowPlace () ? 25 : 75) || usesKnife ()) {
m_fightStyle = Fight::Strafe; m_fightStyle = Fight::Strafe;
} }
else { else {
@ -1181,6 +1191,13 @@ void Bot::attackMovement () {
m_lastFightStyleCheck = game.time (); m_lastFightStyleCheck = game.time ();
} }
if (distance < 96.0f && !usesKnife ()) {
m_moveSpeed = -pev->maxspeed;
}
else if (usesKnife ()) {
m_fightStyle = Fight::None;
}
if (m_fightStyle == Fight::Strafe) { if (m_fightStyle == Fight::Strafe) {
auto swapStrafeCombatDir = [&] () { auto swapStrafeCombatDir = [&] () {
m_combatStrafeDir = (m_combatStrafeDir == Dodge::Left ? Dodge::Right : Dodge::Left); m_combatStrafeDir = (m_combatStrafeDir == Dodge::Left ? Dodge::Right : Dodge::Left);
@ -1243,8 +1260,13 @@ void Bot::attackMovement () {
pev->button |= IN_JUMP; pev->button |= IN_JUMP;
} }
} }
else { else if (m_fightStyle == Fight::Stay) {
if ((m_enemyParts & (Visibility::Head | Visibility::Body)) && getCurrentTaskId () != Task::SeekCover && getCurrentTaskId () != Task::Hunt) { const bool alreadyDucking = m_duckTime > game.time () || isDucking ();
if (alreadyDucking) {
m_duckTime = game.time () + m_frameInterval * 2.0f;
}
else if ((distance > kDoubleSprayDistance && hasPrimaryWeapon ()) && (m_enemyParts & (Visibility::Head | Visibility::Body)) && getCurrentTaskId () != Task::SeekCover && getCurrentTaskId () != Task::Hunt) {
int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); int enemyNearestIndex = graph.getNearest (m_enemy->v.origin);
if (graph.isVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (enemyNearestIndex, m_currentNodeIndex)) { if (graph.isVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (m_currentNodeIndex, enemyNearestIndex) && graph.isDuckVisible (enemyNearestIndex, m_currentNodeIndex)) {
@ -1255,10 +1277,6 @@ void Bot::attackMovement () {
m_strafeSpeed = 0.0f; m_strafeSpeed = 0.0f;
} }
m_navTimeset = game.time ();
m_moveToGoal = false;
m_checkTerrain = false;
if (m_difficulty >= Difficulty::Normal && isOnFloor () && m_duckTime < game.time ()) { if (m_difficulty >= Difficulty::Normal && isOnFloor () && m_duckTime < game.time ()) {
if (distance < 768.0f) { if (distance < 768.0f) {
if (rg.get (0, 1000) < rg.get (7, 12) && pev->velocity.length2d () > 150.0f && isInViewCone (m_enemy->v.origin)) { if (rg.get (0, 1000) < rg.get (7, 12) && pev->velocity.length2d () > 150.0f && isInViewCone (m_enemy->v.origin)) {
@ -1465,7 +1483,11 @@ bool Bot::rateGroundWeapon (edict_t *ent) {
} }
bool Bot::hasAnyWeapons () { bool Bot::hasAnyWeapons () {
return (pev->weapons & (kPrimaryWeaponMask | kSecondaryWeaponMask)); return !!(pev->weapons & (kPrimaryWeaponMask | kSecondaryWeaponMask));
}
bool Bot::isKnifeMode () {
return cv_jasonmode.bool_ () || (usesKnife () && !hasAnyWeapons ());
} }
void Bot::selectBestWeapon () { void Bot::selectBestWeapon () {
@ -1473,7 +1495,7 @@ void Bot::selectBestWeapon () {
// current weapon to best one. // current weapon to best one.
// if knife mode activated, force bot to use knife // if knife mode activated, force bot to use knife
if (cv_jasonmode.bool_ ()) { if (isKnifeMode ()) {
selectWeaponById (Weapon::Knife); selectWeaponById (Weapon::Knife);
return; return;
} }
@ -1649,7 +1671,7 @@ void Bot::checkReload () {
bool uninterruptibleTask = (task == Task::PlantBomb || task == Task::DefuseBomb || task == Task::PickupItem || task == Task::ThrowExplosive || task == Task::ThrowFlashbang || task == Task::ThrowSmoke); bool uninterruptibleTask = (task == Task::PlantBomb || task == Task::DefuseBomb || task == Task::PickupItem || task == Task::ThrowExplosive || task == Task::ThrowFlashbang || task == Task::ThrowSmoke);
// do not check for reload // do not check for reload
if (uninterruptibleTask || m_isUsingGrenade) { if (uninterruptibleTask || m_isUsingGrenade || usesKnife ()) {
m_reloadState = Reload::None; m_reloadState = Reload::None;
return; return;
} }
@ -1849,7 +1871,7 @@ void Bot::checkGrenadesThrow () {
}; };
// check if throwing a grenade is a good thing to do... // check if throwing a grenade is a good thing to do...
if (preventibleTasks || isInNarrowPlace () || cv_ignore_enemies.bool_ () || m_isUsingGrenade || m_grenadeRequested || m_isReloading || cv_jasonmode.bool_ () || (m_grenadeRequested || m_grenadeCheckTime >= game.time ())) { if (preventibleTasks || isInNarrowPlace () || cv_ignore_enemies.bool_ () || m_isUsingGrenade || m_grenadeRequested || m_isReloading || (isKnifeMode () && !bots.isBombPlanted ()) || (m_grenadeRequested || m_grenadeCheckTime >= game.time ())) {
clearThrowStates (m_states); clearThrowStates (m_states);
return; return;
} }
@ -1915,7 +1937,7 @@ void Bot::checkGrenadesThrow () {
if (radius < 164.0f) { if (radius < 164.0f) {
radius = 164.0f; radius = 164.0f;
} }
auto predicted = graph.searchRadius (radius, pos, 12); auto predicted = graph.getNarestInRadius (radius, pos, 12);
if (predicted.empty ()) { if (predicted.empty ()) {
m_states &= ~Sense::ThrowExplosive; m_states &= ~Sense::ThrowExplosive;
@ -2029,3 +2051,14 @@ void Bot::checkGrenadesThrow () {
clearThrowStates (m_states); clearThrowStates (m_states);
} }
} }
bool Bot::isEnemyInSight (Vector &endPos) {
TraceResult aimHitTr {};
game.testModel (getEyesPos (), getEyesPos () + pev->v_angle.forward () * kInfiniteDistance, 0, m_enemy, &aimHitTr);
if (aimHitTr.pHit != m_enemy) {
return false;
}
endPos = aimHitTr.vecEndPos;
return true;
}

View file

@ -20,6 +20,7 @@ BotConfig::BotConfig () {
void BotConfig::loadConfigs () { void BotConfig::loadConfigs () {
setupMemoryFiles (); setupMemoryFiles ();
loadCustomConfig ();
loadNamesConfig (); loadNamesConfig ();
loadChatConfig (); loadChatConfig ();
loadChatterConfig (); loadChatterConfig ();
@ -28,7 +29,6 @@ void BotConfig::loadConfigs () {
loadLogosConfig (); loadLogosConfig ();
loadAvatarsConfig (); loadAvatarsConfig ();
loadDifficultyConfig (); loadDifficultyConfig ();
loadCustomConfig ();
} }
void BotConfig::loadMainConfig (bool isFirstLoad) { void BotConfig::loadMainConfig (bool isFirstLoad) {
@ -533,27 +533,27 @@ void BotConfig::loadDifficultyConfig () {
// initialize defaults // initialize defaults
m_difficulty[Difficulty::Noob] = { m_difficulty[Difficulty::Noob] = {
{ 0.8f, 1.0f }, 5, 0, 0 { 0.8f, 1.0f }, 5, 0, 0, 38, { 30.0f, 30.0f, 40.0f }
}; };
m_difficulty[Difficulty::Easy] = { m_difficulty[Difficulty::Easy] = {
{ 0.6f, 0.8f }, 30, 10, 10 { 0.6f, 0.8f }, 30, 10, 10, 32, { 15.0f, 15.0f, 24.0f }
}; };
m_difficulty[Difficulty::Normal] = { m_difficulty[Difficulty::Normal] = {
{ 0.4f, 0.6f }, 50, 30, 40 { 0.4f, 0.6f }, 50, 30, 40, 26, { 5.0f, 5.0f, 10.0f }
}; };
m_difficulty[Difficulty::Hard] = { m_difficulty[Difficulty::Hard] = {
{ 0.2f, 0.4f }, 75, 60, 70 { 0.2f, 0.4f }, 75, 60, 70, 23, { 0.0f, 0.0f, 0.0f }
}; };
m_difficulty[Difficulty::Expert] = { m_difficulty[Difficulty::Expert] = {
{ 0.1f, 0.2f }, 100, 90, 90 { 0.1f, 0.2f }, 100, 90, 90, 21, { 0.0f, 0.0f, 0.0f }
}; };
// currently, mindelay, maxdelay, headprob, seenthruprob, heardthruprob // currently, mindelay, maxdelay, headprob, seenthruprob, heardthruprob, recoil, aim_error {x,y,z}
constexpr uint32_t kMaxDifficultyValues = 5; constexpr uint32_t kMaxDifficultyValues = 9;
// helper for parsing each level // helper for parsing each level
auto parseLevel = [&] (int32_t level, StringRef data) { auto parseLevel = [&] (int32_t level, StringRef data) {
@ -570,6 +570,10 @@ void BotConfig::loadDifficultyConfig () {
diff->headshotPct = values[2].int_ (); diff->headshotPct = values[2].int_ ();
diff->seenThruPct = values[3].int_ (); diff->seenThruPct = values[3].int_ ();
diff->hearThruPct = values[4].int_ (); diff->hearThruPct = values[4].int_ ();
diff->maxRecoil = values[5].int_ ();
diff->aimError.x = values[6].float_ ();
diff->aimError.y = values[7].float_ ();
diff->aimError.z = values[8].float_ ();
}; };
// avatars inititalization // avatars inititalization

View file

@ -589,7 +589,7 @@ int BotControl::cmdNodeClean () {
if (strValue (option) == "all") { if (strValue (option) == "all") {
int removed = 0; int removed = 0;
for (int i = 0; i < graph.length (); ++i) { for (auto i = 0; i < graph.length (); ++i) {
removed += graph.clearConnections (i); removed += graph.clearConnections (i);
} }
msg ("Done. Processed %d nodes. %d useless paths was cleared.", graph.length (), removed); msg ("Done. Processed %d nodes. %d useless paths was cleared.", graph.length (), removed);
@ -842,9 +842,9 @@ int BotControl::cmdNodeIterateCamp () {
} }
} }
else if (op == "begin") { else if (op == "begin") {
for (int i = 0; i < graph.length (); ++i) { for (const auto &path : graph) {
if (graph[i].flags & NodeFlag::Camp) { if (path.flags & NodeFlag::Camp) {
m_campIterator.push (i); m_campIterator.push (path.number);
} }
} }
if (!m_campIterator.empty ()) { if (!m_campIterator.empty ()) {
@ -877,8 +877,8 @@ int BotControl::cmdAdjustHeight () {
auto heightOffset = floatValue (offset); auto heightOffset = floatValue (offset);
// adjust the height for all the nodes (negative values possible) // adjust the height for all the nodes (negative values possible)
for (int i = 0; i < graph.length (); ++i) { for (auto &path : graph) {
graph[i].origin.z += heightOffset; path.origin.z += heightOffset;
} }
return BotCommandResult::Handled; return BotCommandResult::Handled;
} }

View file

@ -195,6 +195,10 @@ void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int w
.writeByte (speed); // speed .writeByte (speed); // speed
} }
void Game::testModel (const Vector &start, const Vector &end, int hullNumber, edict_t *entToHit, TraceResult *ptr) {
engfuncs.pfnTraceModel (start, end, hullNumber, entToHit, ptr);
}
void Game::testLine (const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult *ptr) { void Game::testLine (const Vector &start, const Vector &end, int ignoreFlags, edict_t *ignoreEntity, TraceResult *ptr) {
// this function traces a line dot by dot, starting from vecStart in the direction of vecEnd, // this function traces a line dot by dot, starting from vecStart in the direction of vecEnd,
// ignoring or not monsters (depending on the value of IGNORE_MONSTERS, true or false), and stops // ignoring or not monsters (depending on the value of IGNORE_MONSTERS, true or false), and stops

View file

@ -451,7 +451,7 @@ int BotGraph::getFarest (const Vector &origin, float maxDistance) {
maxDistance = cr::square (maxDistance); maxDistance = cr::square (maxDistance);
for (const auto &path : m_paths) { for (const auto &path : m_paths) {
float distance = path.origin.distanceSq (origin); const float distance = path.origin.distanceSq (origin);
if (distance > maxDistance) { if (distance > maxDistance) {
index = path.number; index = path.number;
@ -472,7 +472,7 @@ int BotGraph::getNearestNoBuckets (const Vector &origin, float minDistance, int
if (flags != -1 && !(path.flags & flags)) { if (flags != -1 && !(path.flags & flags)) {
continue; // if flag not -1 and node has no this flag, skip node continue; // if flag not -1 and node has no this flag, skip node
} }
float distance = path.origin.distanceSq (origin); const float distance = path.origin.distanceSq (origin);
if (distance < minDistance) { if (distance < minDistance) {
index = path.number; index = path.number;
@ -492,9 +492,12 @@ int BotGraph::getEditorNearest () {
int BotGraph::getNearest (const Vector &origin, float minDistance, int flags) { int BotGraph::getNearest (const Vector &origin, float minDistance, int flags) {
// find the nearest node to that origin and return the index // find the nearest node to that origin and return the index
auto &bucket = getNodesInBucket (origin); if (minDistance > 256.0f && !cr::fequal (minDistance, kInfiniteDistance)) {
return getNearestNoBuckets (origin, minDistance, flags);
}
const auto &bucket = getNodesInBucket (origin);
if (bucket.empty ()) { if (bucket.length () < kMaxNodeLinks) {
return getNearestNoBuckets (origin, minDistance, flags); return getNearestNoBuckets (origin, minDistance, flags);
} }
@ -520,17 +523,26 @@ int BotGraph::getNearest (const Vector &origin, float minDistance, int flags) {
return index; return index;
} }
IntArray BotGraph::searchRadius (float radius, const Vector &origin, int maxCount) { IntArray BotGraph::getNarestInRadius (float radius, const Vector &origin, int maxCount) {
// returns all nodes within radius from position // returns all nodes within radius from position
radius = cr::square (radius);
IntArray result; IntArray result;
const auto &bucket = getNodesInBucket (origin); const auto &bucket = getNodesInBucket (origin);
if (bucket.empty ()) { if (bucket.length () < kMaxNodeLinks || radius > cr::square (256.0f)) {
result.push (getNearestNoBuckets (origin, radius)); for (const auto &path : m_paths) {
if (maxCount != -1 && static_cast <int> (result.length ()) > maxCount) {
break;
}
if (origin.distanceSq (path.origin) < radius) {
result.push (path.number);
}
}
return result; return result;
} }
radius = cr::square (radius);
for (const auto &at : bucket) { for (const auto &at : bucket) {
if (maxCount != -1 && static_cast <int> (result.length ()) > maxCount) { if (maxCount != -1 && static_cast <int> (result.length ()) > maxCount) {
@ -3010,24 +3022,19 @@ BotGraph::BotGraph () {
} }
void BotGraph::initBuckets () { void BotGraph::initBuckets () {
for (int x = 0; x < kMaxBucketsInsidePos; ++x) { m_hashTable.clear ();
for (int y = 0; y < kMaxBucketsInsidePos; ++y) {
for (int z = 0; z < kMaxBucketsInsidePos; ++z) {
m_buckets[x][y][z].reserve (kMaxNodesInsideBucket);
m_buckets[x][y][z].clear ();
}
}
}
} }
void BotGraph::addToBucket (const Vector &pos, int index) { void BotGraph::addToBucket (const Vector &pos, int index) {
const auto &bucket = locateBucket (pos); m_hashTable[locateBucket (pos)].emplace (index);
m_buckets[bucket.x][bucket.y][bucket.z].push (index); }
const Array <int32_t> &BotGraph::getNodesInBucket (const Vector &pos) {
return m_hashTable[locateBucket (pos)];
} }
void BotGraph::eraseFromBucket (const Vector &pos, int index) { void BotGraph::eraseFromBucket (const Vector &pos, int index) {
const auto &bucket = locateBucket (pos); auto &data = m_hashTable[locateBucket (pos)];
auto &data = m_buckets[bucket.x][bucket.y][bucket.z];
for (size_t i = 0; i < data.length (); ++i) { for (size_t i = 0; i < data.length (); ++i) {
if (data[i] == index) { if (data[i] == index) {
@ -3037,19 +3044,13 @@ void BotGraph::eraseFromBucket (const Vector &pos, int index) {
} }
} }
BotGraph::Bucket BotGraph::locateBucket (const Vector &pos) { int BotGraph::locateBucket (const Vector &pos) {
constexpr auto size = static_cast <float> (kMaxNodes * 2); constexpr auto width = cr::square (kMaxNodes);
return { auto hash = [&] (float axis, int32_t shift) {
cr::abs (static_cast <int> ((pos.x + size) / kMaxBucketSize)), return ((static_cast <int> (axis) + width) & 0x007f80) >> shift;
cr::abs (static_cast <int> ((pos.y + size) / kMaxBucketSize)),
cr::abs (static_cast <int> ((pos.z + size) / kMaxBucketSize))
}; };
} return hash (pos.x, 15) + hash (pos.y, 7);
const SmallArray <int32_t> &BotGraph::getNodesInBucket (const Vector &pos) {
const auto &bucket = locateBucket (pos);
return m_buckets[bucket.x][bucket.y][bucket.z];
} }
void BotGraph::updateGlobalPractice () { void BotGraph::updateGlobalPractice () {

View file

@ -21,7 +21,7 @@ ConVar cv_join_team ("yb_join_team", "any", "Forces all bots to join team specif
ConVar cv_join_delay ("yb_join_delay", "5.0", "Specifies after how many seconds bots should start to join the game after the changelevel.", true, 0.0f, 30.0f); ConVar cv_join_delay ("yb_join_delay", "5.0", "Specifies after how many seconds bots should start to join the game after the changelevel.", true, 0.0f, 30.0f);
ConVar cv_name_prefix ("yb_name_prefix", "", "All the bot names will be prefixed with string specified with this cvar.", false); ConVar cv_name_prefix ("yb_name_prefix", "", "All the bot names will be prefixed with string specified with this cvar.", false);
ConVar cv_difficulty ("yb_difficulty", "4", "All bots difficulty level. Changing at runtime will affect already created bots.", true, 0.0f, 4.0f); ConVar cv_difficulty ("yb_difficulty", "3", "All bots difficulty level. Changing at runtime will affect already created bots.", true, 0.0f, 4.0f);
ConVar cv_difficulty_min ("yb_difficulty_min", "-1", "Lower bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.", true, -1.0f, 4.0f); ConVar cv_difficulty_min ("yb_difficulty_min", "-1", "Lower bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.", true, -1.0f, 4.0f);
ConVar cv_difficulty_max ("yb_difficulty_max", "-1", "Upper bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.", true, -1.0f, 4.0f); ConVar cv_difficulty_max ("yb_difficulty_max", "-1", "Upper bound of random difficulty on bot creation. Only affects newly created bots. -1 means yb_difficulty only used.", true, -1.0f, 4.0f);
@ -1285,6 +1285,7 @@ void Bot::newRound () {
m_isLeader = false; m_isLeader = false;
m_hasProgressBar = false; m_hasProgressBar = false;
m_canChooseAimDirection = true; m_canChooseAimDirection = true;
m_switchedToKnifeDuringJump = false;
m_preventFlashing = 0.0f; m_preventFlashing = 0.0f;
m_timeTeamOrder = 0.0f; m_timeTeamOrder = 0.0f;
@ -1410,6 +1411,7 @@ void Bot::newRound () {
m_combatStrafeDir = Dodge::None; m_combatStrafeDir = Dodge::None;
m_fightStyle = Fight::None; m_fightStyle = Fight::None;
m_lastFightStyleCheck = 0.0f; m_lastFightStyleCheck = 0.0f;
m_lastBadWeaponSwitchTime = 0.0f;
m_checkWeaponSwitch = true; m_checkWeaponSwitch = true;
m_checkKnifeSwitch = true; m_checkKnifeSwitch = true;

View file

@ -103,7 +103,7 @@ int Bot::findBestGoal () {
else if (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && bots.getRoundStartTime () + 10.0f < game.time ()) { else if (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && bots.getRoundStartTime () + 10.0f < game.time ()) {
// send some terrorists to guard planted bomb // send some terrorists to guard planted bomb
if (!m_defendedBomb && bots.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && getBombTimeleft () >= 15.0f) { if (!m_defendedBomb && bots.isBombPlanted () && getCurrentTaskId () != Task::EscapeFromBomb && getBombTimeleft () >= 15.0f) {
return pushToHistroy (m_chosenGoalIndex = graph.getNearest (graph.getBombOrigin ())); return pushToHistroy (m_chosenGoalIndex = findDefendNode (graph.getBombOrigin ()));
} }
} }
else if (game.mapIs (MapFlags::Escape)) { else if (game.mapIs (MapFlags::Escape)) {
@ -844,7 +844,7 @@ bool Bot::updateNavigation () {
m_desiredVelocity = nullptr; m_desiredVelocity = nullptr;
} }
} }
else if (!cv_jasonmode.bool_ () && usesKnife () && isOnFloor ()) { else if (!isKnifeMode () && m_switchedToKnifeDuringJump) {
selectBestWeapon (); selectBestWeapon ();
// if jump distance was big enough, cooldown a little // if jump distance was big enough, cooldown a little
@ -852,6 +852,7 @@ bool Bot::updateNavigation () {
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.45f, false); startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.45f, false);
} }
m_jumpDistance = 0.0f; m_jumpDistance = 0.0f;
m_switchedToKnifeDuringJump = false;
} }
} }
@ -1041,7 +1042,7 @@ bool Bot::updateNavigation () {
desiredDistance = 0.0f; desiredDistance = 0.0f;
} }
else if (isOccupiedNode (m_path->number)) { else if (isOccupiedNode (m_path->number)) {
desiredDistance = 72.0f; desiredDistance = 96.0f;
} }
else { else {
desiredDistance = m_path->radius; desiredDistance = m_path->radius;
@ -1601,16 +1602,16 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath:
}; };
// square distance heuristic with hostages // square distance heuristic with hostages
auto hfunctionPathDistWithHostage = [&hfunctionPathDist] (int index, int startIndex, int goalIndex) -> float { auto hfunctionPathDistWithHostage = [&hfunctionPathDist] (int index, int, int goalIndex) -> float {
if (graph[startIndex].flags & NodeFlag::NoHostage) { if (graph[index].flags & NodeFlag::NoHostage) {
return 65355.0f; return 65355.0f;
} }
return hfunctionPathDist (index, startIndex, goalIndex); return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex);
}; };
// none heuristic // none heuristic
auto hfunctionNone = [&hfunctionPathDist] (int index, int startIndex, int goalIndex) -> float { auto hfunctionNone = [&hfunctionPathDist] (int index, int, int goalIndex) -> float {
return hfunctionPathDist (index, startIndex, goalIndex) / (128.0f * 10.0f); return hfunctionPathDist (index, kInvalidNodeIndex, goalIndex) / (128.0f * 10.0f);
}; };
if (!graph.exists (srcIndex)) { if (!graph.exists (srcIndex)) {
@ -1673,7 +1674,7 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath:
// put start node into open list // put start node into open list
srcRoute->g = gcalc (m_team, srcIndex, kInvalidNodeIndex); srcRoute->g = gcalc (m_team, srcIndex, kInvalidNodeIndex);
srcRoute->f = srcRoute->g + hcalc (srcIndex, srcIndex, destIndex); srcRoute->f = srcRoute->g + hcalc (srcIndex, kInvalidNodeIndex, destIndex);
srcRoute->state = RouteState::Open; srcRoute->state = RouteState::Open;
m_routeQue.clear (); m_routeQue.clear ();
@ -1724,7 +1725,7 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath:
// calculate the F value as F = G + H // calculate the F value as F = G + H
const float g = curRoute->g + gcalc (m_team, child.index, currentIndex); const float g = curRoute->g + gcalc (m_team, child.index, currentIndex);
const float h = hcalc (child.index, srcIndex, destIndex); const float h = hcalc (child.index, kInvalidNodeIndex, destIndex);
const float f = g + h; const float f = g + h;
if (childRoute->state == RouteState::New || childRoute->f > f) { if (childRoute->state == RouteState::New || childRoute->f > f) {
@ -1747,14 +1748,13 @@ void Bot::findPath (int srcIndex, int destIndex, FindPath pathType /*= FindPath:
void Bot::clearSearchNodes () { void Bot::clearSearchNodes () {
m_pathWalk.clear (); m_pathWalk.clear ();
m_chosenGoalIndex = kInvalidNodeIndex;
} }
void Bot::clearRoute () { void Bot::clearRoute () {
m_routes.resize (static_cast <size_t> (graph.length ())); m_routes.resize (static_cast <size_t> (graph.length ()));
for (int i = 0; i < graph.length (); ++i) { for (const auto &path : graph) {
auto route = &m_routes[i]; auto route = &m_routes[path.number];
route->g = route->f = 0.0f; route->g = route->f = 0.0f;
route->parent = kInvalidNodeIndex; route->parent = kInvalidNodeIndex;
@ -1801,28 +1801,25 @@ bool Bot::findNextBestNode () {
// this function find a node in the near of the bot if bot had lost his path of pathfinder needs // this function find a node in the near of the bot if bot had lost his path of pathfinder needs
// to be restarted over again. // to be restarted over again.
int busy = kInvalidNodeIndex; int busyIndex = kInvalidNodeIndex;
float lessDist[3] = { kInfiniteDistance, kInfiniteDistance, kInfiniteDistance }; float lessDist[3] = { kInfiniteDistance, kInfiniteDistance, kInfiniteDistance };
int lessIndex[3] = { kInvalidNodeIndex, kInvalidNodeIndex , kInvalidNodeIndex }; int lessIndex[3] = { kInvalidNodeIndex, kInvalidNodeIndex , kInvalidNodeIndex };
auto &bucket = graph.getNodesInBucket (pev->origin); const auto &origin = pev->origin + pev->velocity * m_frameInterval;
int numToSkip = cr::clamp (rg.get (0, 2), 0, static_cast <int> (bucket.length () / 2)); const auto &bucket = graph.getNodesInBucket (origin);
for (const int at : bucket) { for (const auto &i : bucket) {
bool skip = !!(at == m_currentNodeIndex); const auto &path = graph[i];
// skip the current node, if any if (!graph.exists (path.number)) {
if (skip && numToSkip > 0) {
continue; continue;
} }
bool skip = !!(path.number == m_currentNodeIndex);
// skip current and recent previous nodes // skip current or recent previous node
for (int j = 0; j < numToSkip; ++j) { if (path.number == m_previousNodes[0]) {
if (at == m_previousNodes[j]) { skip = true;
skip = true;
break;
}
} }
// skip node from recent list // skip node from recent list
@ -1831,28 +1828,28 @@ bool Bot::findNextBestNode () {
} }
// cts with hostages should not pick nodes with no hostage flag // cts with hostages should not pick nodes with no hostage flag
if (game.mapIs (MapFlags::HostageRescue) && m_team == Team::CT && (graph[at].flags & NodeFlag::NoHostage) && hasHostage ()) { if (game.mapIs (MapFlags::HostageRescue) && m_team == Team::CT && (graph[path.number].flags & NodeFlag::NoHostage) && hasHostage ()) {
continue; continue;
} }
// check we're have link to it // check we're have link to it
if (m_currentNodeIndex != kInvalidNodeIndex && !graph.isConnected (m_currentNodeIndex, at)) { if (m_currentNodeIndex != kInvalidNodeIndex && !graph.isConnected (m_currentNodeIndex, path.number)) {
continue; continue;
} }
// ignore non-reacheable nodes... // ignore non-reacheable nodes...
if (!isReachableNode (at)) { if (!isReachableNode (path.number)) {
continue; continue;
} }
// check if node is already used by another bot... // check if node is already used by another bot...
if (bots.getRoundStartTime () + 5.0f < game.time () && isOccupiedNode (at)) { if (bots.getRoundStartTime () + 5.0f < game.time () && isOccupiedNode (path.number)) {
busy = at; busyIndex = path.number;
continue; continue;
} }
// if we're still here, find some close nodes // if we're still here, find some close nodes
float distance = pev->origin.distanceSq (graph[at].origin); float distance = pev->origin.distanceSq (path.origin);
if (distance < lessDist[0]) { if (distance < lessDist[0]) {
lessDist[2] = lessDist[1]; lessDist[2] = lessDist[1];
@ -1862,18 +1859,18 @@ bool Bot::findNextBestNode () {
lessIndex[1] = lessIndex[0]; lessIndex[1] = lessIndex[0];
lessDist[0] = distance; lessDist[0] = distance;
lessIndex[0] = at; lessIndex[0] = path.number;
} }
else if (distance < lessDist[1]) { else if (distance < lessDist[1]) {
lessDist[2] = lessDist[1]; lessDist[2] = lessDist[1];
lessIndex[2] = lessIndex[1]; lessIndex[2] = lessIndex[1];
lessDist[1] = distance; lessDist[1] = distance;
lessIndex[1] = at; lessIndex[1] = path.number;
} }
else if (distance < lessDist[2]) { else if (distance < lessDist[2]) {
lessDist[2] = distance; lessDist[2] = distance;
lessIndex[2] = at; lessIndex[2] = path.number;
} }
} }
int selected = kInvalidNodeIndex; int selected = kInvalidNodeIndex;
@ -1894,8 +1891,8 @@ bool Bot::findNextBestNode () {
selected = lessIndex[index]; selected = lessIndex[index];
// if we're still have no node and have busy one (by other bot) pick it up // if we're still have no node and have busy one (by other bot) pick it up
if (selected == kInvalidNodeIndex && busy != kInvalidNodeIndex) { if (selected == kInvalidNodeIndex && busyIndex != kInvalidNodeIndex) {
selected = busy; selected = busyIndex;
} }
// worst case... find atleast something // worst case... find atleast something
@ -1909,26 +1906,27 @@ bool Bot::findNextBestNode () {
} }
float Bot::getEstimatedNodeReachTime () { float Bot::getEstimatedNodeReachTime () {
float estimatedTime = 2.8f; float estimatedTime = 6.0f;
// if just fired at enemy, increase reachability
if (m_shootTime + 0.15f < game.time ()) {
return estimatedTime;
}
// calculate 'real' time that we need to get from one node to another // calculate 'real' time that we need to get from one node to another
if (graph.exists (m_currentNodeIndex) && graph.exists (m_previousNodes[0])) { if (graph.exists (m_currentNodeIndex) && graph.exists (m_previousNodes[0])) {
float distance = graph[m_previousNodes[0]].origin.distance (graph[m_currentNodeIndex].origin); float distance = graph[m_previousNodes[0]].origin.distanceSq (graph[m_currentNodeIndex].origin);
// caclulate estimated time // caclulate estimated time
if (pev->maxspeed <= 0.0f) { estimatedTime = 5.0f * (distance / cr::square (m_moveSpeed + 1.0f));
estimatedTime = 3.0f * distance / 240.0f;
}
else {
estimatedTime = 3.0f * distance / pev->maxspeed;
}
bool longTermReachability = (m_pathFlags & NodeFlag::Crouch) || (m_pathFlags & NodeFlag::Ladder) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK); bool longTermReachability = (m_pathFlags & NodeFlag::Crouch) || (m_pathFlags & NodeFlag::Ladder) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK);
// check for special nodes, that can slowdown our movement // check for special nodes, that can slowdown our movement
if (longTermReachability) { if (longTermReachability) {
estimatedTime *= 2.0f; estimatedTime *= 2.0f;
} }
estimatedTime = cr::clamp (estimatedTime, 2.0f, longTermReachability ? 8.0f : 5.0f); estimatedTime = cr::clamp (estimatedTime, 3.0f, longTermReachability ? 8.0f : 6.0f);
} }
return estimatedTime + m_frameInterval; return estimatedTime + m_frameInterval;
} }
@ -1941,8 +1939,7 @@ void Bot::findValidNode () {
clearSearchNodes (); clearSearchNodes ();
findNextBestNode (); findNextBestNode ();
} }
else if (m_navTimeset + getEstimatedNodeReachTime () < game.time () && game.isNullEntity (m_enemy)) { else if (m_navTimeset + getEstimatedNodeReachTime () < game.time ()) {
// increase danager for both teams // increase danager for both teams
for (int team = Team::Terrorist; team < kGameTeamNum; ++team) { for (int team = Team::Terrorist; team < kGameTeamNum; ++team) {
int damageValue = graph.getDangerDamage (team, m_currentNodeIndex, m_currentNodeIndex); int damageValue = graph.getDangerDamage (team, m_currentNodeIndex, m_currentNodeIndex);
@ -1989,42 +1986,31 @@ int Bot::findNearestNode () {
// get the current nearest node to bot with visibility checks // get the current nearest node to bot with visibility checks
int index = kInvalidNodeIndex; int index = kInvalidNodeIndex;
float minimum = cr::square (1024.0f); float minimumDistance = cr::square (1024.0f);
auto &bucket = graph.getNodesInBucket (pev->origin); const auto &origin = pev->origin + pev->velocity * m_frameInterval;
const auto &bucket = graph.getNodesInBucket (origin);
for (const auto at : bucket) { for (const auto &i : bucket) {
if (at == m_currentNodeIndex) { const auto &path = graph[i];
if (!graph.exists (path.number) || !graph.exists (m_currentNodeIndex) || path.number == m_currentNodeIndex) {
continue; continue;
} }
float distance = graph[at].origin.distanceSq (pev->origin); const float distance = path.origin.distanceSq (pev->origin);
if (distance < minimum) { if (distance < minimumDistance) {
// if bot doing navigation, make sure node really visible and reacheable
// if bot doing navigation, make sure node really visible and not too high if (graph.isVisible (m_currentNodeIndex, path.number) && isReachableNode (path.number)) {
if ((m_currentNodeIndex != kInvalidNodeIndex && graph.isVisible (m_currentNodeIndex, at) && graph.isVisible (at, m_currentNodeIndex)) || isReachableNode (at)) { index = path.number;
index = at; minimumDistance = distance;
minimum = distance;
} }
// @pr-419: temporarily disabled due to cpu usage
#if 0
else {
TraceResult tr {};
game.testHull (getEyesPos (), graph[at].origin, TraceIgnore::Monsters, head_hull, ent (), &tr);
if (tr.flFraction >= 1.0f && !tr.fStartSolid) {
index = at;
minimum = distance;
}
}
#endif
} }
} }
// worst case, take any node... // worst case, take any node...
if (index == kInvalidNodeIndex) { if (index == kInvalidNodeIndex) {
index = graph.getNearestNoBuckets (pev->origin); index = graph.getNearestNoBuckets (origin);
} }
return index; return index;
} }
@ -2109,14 +2095,14 @@ int Bot::findDefendNode (const Vector &origin) {
} }
// find the best node now // find the best node now
for (int i = 0; i < graph.length (); ++i) { for (const auto &path : graph) {
// exclude ladder & current nodes // exclude ladder & current nodes
if ((graph[i].flags & NodeFlag::Ladder) || i == srcIndex || !graph.isVisible (i, posIndex)) { if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || !graph.isVisible (path.number, posIndex)) {
continue; continue;
} }
// use the 'real' pathfinding distances // use the 'real' pathfinding distances
int distance = graph.getPathDist (srcIndex, i); int distance = graph.getPathDist (srcIndex, path.number);
// skip wayponts too far // skip wayponts too far
if (distance > kMaxDistance) { if (distance > kMaxDistance) {
@ -2124,10 +2110,10 @@ int Bot::findDefendNode (const Vector &origin) {
} }
// skip occupied points // skip occupied points
if (isOccupiedNode (i)) { if (isOccupiedNode (path.number)) {
continue; continue;
} }
game.testLine (graph[i].origin, graph[posIndex].origin, TraceIgnore::Glass, ent (), &tr); game.testLine (path.origin, graph[posIndex].origin, TraceIgnore::Glass, ent (), &tr);
// check if line not hit anything // check if line not hit anything
if (!cr::fequal (tr.flFraction, 1.0f)) { if (!cr::fequal (tr.flFraction, 1.0f)) {
@ -2135,35 +2121,35 @@ int Bot::findDefendNode (const Vector &origin) {
} }
if (distance > minDistance[0]) { if (distance > minDistance[0]) {
nodeIndex[0] = i; nodeIndex[0] = path.number;
minDistance[0] = distance; minDistance[0] = distance;
} }
else if (distance > minDistance[1]) { else if (distance > minDistance[1]) {
nodeIndex[1] = i; nodeIndex[1] = path.number;
minDistance[1] = distance; minDistance[1] = distance;
} }
else if (distance > minDistance[2]) { else if (distance > minDistance[2]) {
nodeIndex[2] = i; nodeIndex[2] = path.number;
minDistance[2] = distance; minDistance[2] = distance;
} }
else if (distance > minDistance[3]) { else if (distance > minDistance[3]) {
nodeIndex[3] = i; nodeIndex[3] = path.number;
minDistance[3] = distance; minDistance[3] = distance;
} }
else if (distance > minDistance[4]) { else if (distance > minDistance[4]) {
nodeIndex[4] = i; nodeIndex[4] = path.number;
minDistance[4] = distance; minDistance[4] = distance;
} }
else if (distance > minDistance[5]) { else if (distance > minDistance[5]) {
nodeIndex[5] = i; nodeIndex[5] = path.number;
minDistance[5] = distance; minDistance[5] = distance;
} }
else if (distance > minDistance[6]) { else if (distance > minDistance[6]) {
nodeIndex[6] = i; nodeIndex[6] = path.number;
minDistance[6] = distance; minDistance[6] = distance;
} }
else if (distance > minDistance[7]) { else if (distance > minDistance[7]) {
nodeIndex[7] = i; nodeIndex[7] = path.number;
minDistance[7] = distance; minDistance[7] = distance;
} }
} }
@ -2198,9 +2184,9 @@ int Bot::findDefendNode (const Vector &origin) {
if (nodeIndex[0] == kInvalidNodeIndex) { if (nodeIndex[0] == kInvalidNodeIndex) {
IntArray found; IntArray found;
for (int i = 0; i < graph.length (); ++i) { for (const auto &path : graph) {
if (origin.distanceSq (graph[i].origin) < cr::square (static_cast <float> (kMaxDistance)) && !graph.isVisible (i, posIndex) && !isOccupiedNode (i)) { if (origin.distanceSq (path.origin) < cr::square (static_cast <float> (kMaxDistance)) && graph.isVisible (path.number, posIndex) && !isOccupiedNode (path.number)) {
found.push (i); found.push (path.number);
} }
} }
@ -2261,15 +2247,15 @@ int Bot::findCoverNode (float maxDistance) {
changeNodeIndex (srcIndex); changeNodeIndex (srcIndex);
// find the best node now // find the best node now
for (int i = 0; i < graph.length (); ++i) { for (const auto &path : graph) {
// exclude ladder, current node and nodes seen by the enemy // exclude ladder, current node and nodes seen by the enemy
if ((graph[i].flags & NodeFlag::Ladder) || i == srcIndex || graph.isVisible (enemyIndex, i)) { if ((path.flags & NodeFlag::Ladder) || path.number == srcIndex || graph.isVisible (enemyIndex, path.number)) {
continue; continue;
} }
bool neighbourVisible = false; // now check neighbour nodes for visibility bool neighbourVisible = false; // now check neighbour nodes for visibility
for (auto &enemy : enemies) { for (auto &enemy : enemies) {
if (graph.isVisible (enemy, i)) { if (graph.isVisible (enemy, path.number)) {
neighbourVisible = true; neighbourVisible = true;
break; break;
} }
@ -2281,43 +2267,43 @@ int Bot::findCoverNode (float maxDistance) {
} }
// use the 'real' pathfinding distances // use the 'real' pathfinding distances
int distance = graph.getPathDist (srcIndex, i); int distance = graph.getPathDist (srcIndex, path.number);
int enemyDistance = graph.getPathDist (enemyIndex, i); int enemyDistance = graph.getPathDist (enemyIndex, path.number);
if (distance >= enemyDistance) { if (distance >= enemyDistance) {
continue; continue;
} }
if (distance < minDistance[0]) { if (distance < minDistance[0]) {
nodeIndex[0] = i; nodeIndex[0] = path.number;
minDistance[0] = distance; minDistance[0] = distance;
} }
else if (distance < minDistance[1]) { else if (distance < minDistance[1]) {
nodeIndex[1] = i; nodeIndex[1] = path.number;
minDistance[1] = distance; minDistance[1] = distance;
} }
else if (distance < minDistance[2]) { else if (distance < minDistance[2]) {
nodeIndex[2] = i; nodeIndex[2] = path.number;
minDistance[2] = distance; minDistance[2] = distance;
} }
else if (distance < minDistance[3]) { else if (distance < minDistance[3]) {
nodeIndex[3] = i; nodeIndex[3] = path.number;
minDistance[3] = distance; minDistance[3] = distance;
} }
else if (distance < minDistance[4]) { else if (distance < minDistance[4]) {
nodeIndex[4] = i; nodeIndex[4] = path.number;
minDistance[4] = distance; minDistance[4] = distance;
} }
else if (distance < minDistance[5]) { else if (distance < minDistance[5]) {
nodeIndex[5] = i; nodeIndex[5] = path.number;
minDistance[5] = distance; minDistance[5] = distance;
} }
else if (distance < minDistance[6]) { else if (distance < minDistance[6]) {
nodeIndex[6] = i; nodeIndex[6] = path.number;
minDistance[6] = distance; minDistance[6] = distance;
} }
else if (distance < minDistance[7]) { else if (distance < minDistance[7]) {
nodeIndex[7] = i; nodeIndex[7] = path.number;
minDistance[7] = distance; minDistance[7] = distance;
} }
} }
@ -2326,7 +2312,7 @@ int Bot::findCoverNode (float maxDistance) {
for (int i = 0; i < kMaxNodeLinks; ++i) { for (int i = 0; i < kMaxNodeLinks; ++i) {
if (nodeIndex[i] != kInvalidNodeIndex) { if (nodeIndex[i] != kInvalidNodeIndex) {
int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]); int practice = graph.getDangerDamage (m_team, nodeIndex[i], nodeIndex[i]);
practice = (practice * 100) / kMaxPracticeDamageValue; practice = (practice * 100) / graph.getHighestDamageForTeam (m_team);
minDistance[i] = (practice * 100) / 8192; minDistance[i] = (practice * 100) / 8192;
minDistance[i] += practice; minDistance[i] += practice;
@ -2380,15 +2366,16 @@ bool Bot::selectBestNextNode () {
} }
for (auto &link : m_path->links) { for (auto &link : m_path->links) {
if (link.index != kInvalidNodeIndex && graph.isConnected (link.index, m_pathWalk.next ()) && graph.isConnected (m_currentNodeIndex, link.index)) { if (graph.exists (link.index) && m_pathWalk.first () != link.index && graph.isConnected (link.index, m_pathWalk.next ()) && graph.isConnected (m_currentNodeIndex, link.index)) {
// don't use ladder nodes as alternative // don't use ladder nodes as alternative
if (graph[link.index].flags & NodeFlag::Ladder) { if (graph[link.index].flags & (NodeFlag::Ladder | PathFlag::Jump)) {
continue; continue;
} }
if (graph[link.index].origin.z <= graph[m_currentNodeIndex].origin.z + 10.0f && !isOccupiedNode (link.index)) { if (graph[link.index].origin.z <= graph[m_currentNodeIndex].origin.z + 10.0f && !isOccupiedNode (link.index)) {
m_pathWalk.first () = link.index; m_pathWalk.first () = link.index;
return true; return true;
} }
} }
@ -2520,6 +2507,9 @@ bool Bot::advanceMovement () {
// is there a jump node right ahead and do we need to draw out the light weapon ? // is there a jump node right ahead and do we need to draw out the light weapon ?
if (willJump && !usesKnife () && m_currentWeapon != Weapon::Scout && !m_isReloading && !usesPistol () && (m_jumpDistance > 175.0f || (dst.z - 32.0f > src.z && m_jumpDistance > 135.0f)) && !(m_states & Sense::SeeingEnemy)) { if (willJump && !usesKnife () && m_currentWeapon != Weapon::Scout && !m_isReloading && !usesPistol () && (m_jumpDistance > 175.0f || (dst.z - 32.0f > src.z && m_jumpDistance > 135.0f)) && !(m_states & Sense::SeeingEnemy)) {
selectWeaponById (Weapon::Knife); // draw out the knife if we needed selectWeaponById (Weapon::Knife); // draw out the knife if we needed
// mark as switched
m_switchedToKnifeDuringJump = true;
} }
// bot not already on ladder but will be soon? // bot not already on ladder but will be soon?
@ -3100,16 +3090,13 @@ int Bot::getRandomCampDir () {
float distTab[kMaxNodesToSearch] {}; float distTab[kMaxNodesToSearch] {};
uint16_t visibility[kMaxNodesToSearch] {}; uint16_t visibility[kMaxNodesToSearch] {};
int currentNode = m_currentNodeIndex; for (const auto &path : graph) {
if (m_currentNodeIndex == path.number || !graph.isVisible (m_currentNodeIndex, path.number)) {
for (int i = 0; i < graph.length (); ++i) {
if (currentNode == i || !graph.isVisible (currentNode, i)) {
continue; continue;
} }
const auto &path = graph[i];
if (count < kMaxNodesToSearch) { if (count < kMaxNodesToSearch) {
indices[count] = i; indices[count] = path.number;
distTab[count] = pev->origin.distanceSq (path.origin); distTab[count] = pev->origin.distanceSq (path.origin);
visibility[count] = path.vis.crouch + path.vis.stand; visibility[count] = path.vis.crouch + path.vis.stand;
@ -3122,7 +3109,7 @@ int Bot::getRandomCampDir () {
for (int j = 0; j < kMaxNodesToSearch; ++j) { for (int j = 0; j < kMaxNodesToSearch; ++j) {
if (visBits >= visibility[j] && distance > distTab[j]) { if (visBits >= visibility[j] && distance > distTab[j]) {
indices[j] = i; indices[j] = path.number;
distTab[j] = distance; distTab[j] = distance;
visibility[j] = visBits; visibility[j] = visBits;
@ -3179,12 +3166,12 @@ void Bot::updateLookAngles () {
accelerate += 600.0f; accelerate += 600.0f;
} }
stiffness += 100.0f; stiffness += 100.0f;
damping += 5.0f; damping -= 5.0f;
} }
m_idealAngles = pev->v_angle; m_idealAngles = pev->v_angle;
float angleDiffPitch = cr::anglesDifference (direction.x, m_idealAngles.x); const float angleDiffPitch = cr::anglesDifference (direction.x, m_idealAngles.x);
float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); const float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y);
if (angleDiffYaw < 1.0f && angleDiffYaw > -1.0f) { if (angleDiffYaw < 1.0f && angleDiffYaw > -1.0f) {
m_lookYawVel = 0.0f; m_lookYawVel = 0.0f;
@ -3332,11 +3319,6 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
continue; continue;
} }
// just in case, if something happend, and we're not updated yet
if (!util.isAlive (client.ent)) {
continue;
}
// do not check clients far away from us // do not check clients far away from us
if (pev->origin.distanceSq (client.origin) > cr::square (320.0f)) { if (pev->origin.distanceSq (client.origin) > cr::square (320.0f)) {
continue; continue;
@ -3347,7 +3329,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
} }
auto length = client.origin.distanceSq (graph[index].origin); auto length = client.origin.distanceSq (graph[index].origin);
if (length < cr::clamp (cr::square (graph[index].radius) * 2.0f, cr::square (80.0f), cr::square (140.0f))) { if (length < cr::clamp (cr::square (graph[index].radius) * 2.0f, cr::square (40.0f), cr::square (90.0f))) {
return true; return true;
} }
auto bot = bots[client.ent]; auto bot = bots[client.ent];
@ -3355,7 +3337,7 @@ bool Bot::isOccupiedNode (int index, bool needZeroVelocity) {
if (bot == nullptr || bot == this || !bot->m_notKilled) { if (bot == nullptr || bot == this || !bot->m_notKilled) {
continue; continue;
} }
return index == bot->m_currentNodeIndex || bot->getTask ()->data == index; return bot->m_currentNodeIndex == index;
} }
return false; return false;
} }