nav: more fixes to ladder navigation

refactor: bot difficulty data and add graph refresh command
combat: fixes for smoke grenades (ref #743)
engine: fixes to spawn management (ref #744)
This commit is contained in:
jeefo 2025-11-12 21:31:23 +03:00
commit 17ed252b60
No known key found for this signature in database
GPG key ID: D696786B81B667C8
26 changed files with 506 additions and 408 deletions

@ -1 +1 @@
Subproject commit b5f7ccc23dec2d018048b40085f78255cc232e52 Subproject commit f7b1b02a301f900082d2e05ebbbc2d7edc2a4e09

View file

@ -29,16 +29,6 @@ public:
// mostly config stuff, and some stuff dealing with menus // mostly config stuff, and some stuff dealing with menus
class BotConfig final : public Singleton <BotConfig> { class BotConfig final : public Singleton <BotConfig> {
public:
struct DifficultyData {
float reaction[2] {};
int32_t headshotPct {};
int32_t seenThruPct {};
int32_t hearThruPct {};
int32_t maxRecoil {};
Vector aimError {};
};
private: private:
Array <StringArray> m_chat {}; Array <StringArray> m_chat {};
Array <Array <ChatterItem>> m_chatter {}; Array <Array <ChatterItem>> m_chatter {};
@ -52,7 +42,7 @@ private:
StringArray m_avatars {}; StringArray m_avatars {};
HashMap <uint32_t, String, Hash <int32_t>> m_language {}; HashMap <uint32_t, String, Hash <int32_t>> m_language {};
HashMap <int32_t, DifficultyData> m_difficulty {}; HashMap <int32_t, BotDifficultyData> m_difficulty {};
HashMap <String, String> m_custom {}; HashMap <String, String> m_custom {};
// default tables for personality weapon preferences, overridden by weapon.cfg // default tables for personality weapon preferences, overridden by weapon.cfg
@ -218,10 +208,7 @@ public:
} }
// get's the difficulty level tweaks // get's the difficulty level tweaks
DifficultyData *getDifficultyTweaks (int32_t level) { BotDifficultyData *getDifficultyTweaks (int32_t level) {
if (level < Difficulty::Noob || level > Difficulty::Expert) {
return &m_difficulty[Difficulty::Expert];
}
return &m_difficulty[level]; return &m_difficulty[level];
} }

View file

@ -69,8 +69,8 @@ CR_DECLARE_SCOPED_ENUM (Menu,
// bomb say string // bomb say string
CR_DECLARE_SCOPED_ENUM (BombPlantedSay, CR_DECLARE_SCOPED_ENUM (BombPlantedSay,
ChatSay = cr::bit (1), ChatSay = cr::bit (1),
Chatter = cr::bit (2) Chatter = cr::bit (2)
) )
// chat types id's // chat types id's
@ -419,7 +419,13 @@ CR_DECLARE_SCOPED_ENUM (GoalTactic,
RescueHostage RescueHostage
) )
// some hard-coded desire defines used to override calculated ones // ladder move direction
CR_DECLARE_SCOPED_ENUM (LadderDir,
Up = 0,
Down,
)
// some hard-coded desire defines used to override calculated ones
namespace TaskPri { namespace TaskPri {
constexpr auto Normal { 35.0f }; constexpr auto Normal { 35.0f };
constexpr auto Pause { 36.0f }; constexpr auto Pause { 36.0f };
@ -447,7 +453,7 @@ constexpr auto kSprayDistanceX2 = kSprayDistance * 2;
constexpr auto kMaxChatterRepeatInterval = 99.0f; constexpr auto kMaxChatterRepeatInterval = 99.0f;
constexpr auto kViewFrameUpdate = 1.0f / 25.0f; constexpr auto kViewFrameUpdate = 1.0f / 25.0f;
constexpr auto kGrenadeDamageRadius = 385.0f; constexpr auto kGrenadeDamageRadius = 385.0f;
constexpr auto kMinMovedDistance = 2.5f; constexpr auto kMinMovedDistance = cr::sqrf (2.0f);
constexpr auto kInfiniteDistanceLong = static_cast <int> (kInfiniteDistance); constexpr auto kInfiniteDistanceLong = static_cast <int> (kInfiniteDistance);
constexpr auto kMaxWeapons = 32; constexpr auto kMaxWeapons = 32;
@ -462,35 +468,35 @@ constexpr auto kConfigExtension = "cfg";
// weapon masks // weapon masks
constexpr auto kPrimaryWeaponMask = (cr::bit (Weapon::XM1014) | constexpr auto kPrimaryWeaponMask = (cr::bit (Weapon::XM1014) |
cr::bit (Weapon::M3) | cr::bit (Weapon::M3) |
cr::bit (Weapon::MAC10) | cr::bit (Weapon::MAC10) |
cr::bit (Weapon::UMP45) | cr::bit (Weapon::UMP45) |
cr::bit (Weapon::MP5) | cr::bit (Weapon::MP5) |
cr::bit (Weapon::TMP) | cr::bit (Weapon::TMP) |
cr::bit (Weapon::P90) | cr::bit (Weapon::P90) |
cr::bit (Weapon::AUG) | cr::bit (Weapon::AUG) |
cr::bit (Weapon::M4A1) | cr::bit (Weapon::M4A1) |
cr::bit (Weapon::SG552) | cr::bit (Weapon::SG552) |
cr::bit (Weapon::AK47) | cr::bit (Weapon::AK47) |
cr::bit (Weapon::Scout) | cr::bit (Weapon::Scout) |
cr::bit (Weapon::SG550) | cr::bit (Weapon::SG550) |
cr::bit (Weapon::AWP) | cr::bit (Weapon::AWP) |
cr::bit (Weapon::G3SG1) | cr::bit (Weapon::G3SG1) |
cr::bit (Weapon::M249) | cr::bit (Weapon::M249) |
cr::bit (Weapon::Famas) | cr::bit (Weapon::Famas) |
cr::bit (Weapon::Galil)); cr::bit (Weapon::Galil));
constexpr auto kSecondaryWeaponMask = (cr::bit (Weapon::P228) constexpr auto kSecondaryWeaponMask = (cr::bit (Weapon::P228)
| cr::bit (Weapon::Elite) | cr::bit (Weapon::Elite)
| cr::bit (Weapon::USP) | cr::bit (Weapon::USP)
| cr::bit (Weapon::Glock18) | cr::bit (Weapon::Glock18)
| cr::bit (Weapon::Deagle) | cr::bit (Weapon::Deagle)
| cr::bit (Weapon::FiveSeven)); | cr::bit (Weapon::FiveSeven));
constexpr auto kSniperWeaponMask = (cr::bit (Weapon::Scout) constexpr auto kSniperWeaponMask = (cr::bit (Weapon::Scout)
| cr::bit (Weapon::SG550) | cr::bit (Weapon::SG550)
| cr::bit (Weapon::AWP) | cr::bit (Weapon::AWP)
| cr::bit (Weapon::G3SG1)); | cr::bit (Weapon::G3SG1));
// weapons < 7 are secondary // weapons < 7 are secondary
constexpr auto kPrimaryWeaponMinIndex = 7; constexpr auto kPrimaryWeaponMinIndex = 7;

View file

@ -23,8 +23,8 @@ CR_DECLARE_SCOPED_ENUM (PrintQueueDestination,
// bot command manager // bot command manager
class BotControl final : public Singleton <BotControl> { class BotControl final : public Singleton <BotControl> {
public: public:
using Handler = int (BotControl::*) (); using Handler = int (BotControl:: *) ();
using MenuHandler = int (BotControl::*) (int); using MenuHandler = int (BotControl:: *) (int);
public: public:
// generic bot command // generic bot command
@ -36,8 +36,7 @@ public:
public: public:
explicit BotCmd () = default; explicit BotCmd () = default;
BotCmd (StringRef name, StringRef format, StringRef help, Handler handler, bool visible = true) : name (name), format (format), help (help), handler (cr::move (handler)), visible (visible) BotCmd (StringRef name, StringRef format, StringRef help, Handler handler, bool visible = true) : name (name), format (format), help (help), handler (cr::move (handler)), visible (visible) {}
{ }
}; };
// single bot menu // single bot menu
@ -47,8 +46,7 @@ public:
MenuHandler handler {}; MenuHandler handler {};
public: public:
explicit BotMenu (int ident, int slots, StringRef text, MenuHandler handler) : ident (ident), slots (slots), text (text), handler (cr::move (handler)) explicit BotMenu (int ident, int slots, StringRef text, MenuHandler handler) : ident (ident), slots (slots), text (text), handler (cr::move (handler)) {}
{ }
}; };
// queued text message to prevent overflow with rapid output // queued text message to prevent overflow with rapid output
@ -57,10 +55,9 @@ public:
String text {}; String text {};
public: public:
explicit PrintQueue () = default; explicit PrintQueue () = default;
PrintQueue (int32_t destination, StringRef text) : destination (destination), text (text) PrintQueue (int32_t destination, StringRef text) : destination (destination), text (text) {}
{ }
}; };
// save old values of changed cvars to revert them back when editing turned off // save old values of changed cvars to revert them back when editing turned off
@ -118,6 +115,7 @@ private:
int cmdNodeSave (); int cmdNodeSave ();
int cmdNodeLoad (); int cmdNodeLoad ();
int cmdNodeErase (); int cmdNodeErase ();
int cmdNodeRefresh ();
int cmdNodeEraseTraining (); int cmdNodeEraseTraining ();
int cmdNodeDelete (); int cmdNodeDelete ();
int cmdNodeCheck (); int cmdNodeCheck ();
@ -255,7 +253,7 @@ public:
} }
} }
edict_t *getIssuer() { edict_t *getIssuer () {
return m_ent; return m_ent;
} }

View file

@ -60,7 +60,7 @@ CR_DECLARE_SCOPED_ENUM (MapFlags,
Escape = cr::bit (3), Escape = cr::bit (3),
KnifeArena = cr::bit (4), KnifeArena = cr::bit (4),
FightYard = cr::bit (5), FightYard = cr::bit (5),
GrenadeWar = cr::bit(6), GrenadeWar = cr::bit (6),
HasDoors = cr::bit (10), // additional flags HasDoors = cr::bit (10), // additional flags
HasButtons = cr::bit (11) // map has buttons HasButtons = cr::bit (11) // map has buttons
) )
@ -190,6 +190,9 @@ public:
// initialize levels // initialize levels
void levelInitialize (edict_t *entities, int max); void levelInitialize (edict_t *entities, int max);
// when entity spawns
void onSpawnEntity (edict_t *ent);
// shutdown levels // shutdown levels
void levelShutdown (); void levelShutdown ();
@ -291,7 +294,7 @@ public:
bool isFakeClientEntity (edict_t *ent) const; bool isFakeClientEntity (edict_t *ent) const;
// check if entity is a player // check if entity is a player
bool isPlayerEntity (edict_t *ent) const ; bool isPlayerEntity (edict_t *ent) const;
// check if entity is a monster // check if entity is a monster
bool isMonsterEntity (edict_t *ent) const; bool isMonsterEntity (edict_t *ent) const;
@ -333,7 +336,10 @@ public:
// gets custom engine args for client command // gets custom engine args for client command
const char *botArgs () const { const char *botArgs () const {
return strings.format (String::join (m_botArgs, " ", m_botArgs[0].startsWith ("say") ? 1 : 0).chars ()); auto result = strings.chars ();
strings.copy (result, String::join (m_botArgs, " ", m_botArgs[0].startsWith ("say") ? 1 : 0).chars (), cr::StringBuffer::StaticBufferSize);
return result;
} }
// gets custom engine argv for client command // gets custom engine argv for client command

View file

@ -227,6 +227,7 @@ public:
bool loadGraphData (); bool loadGraphData ();
bool canDownload (); bool canDownload ();
bool isAnalyzed () const; bool isAnalyzed () const;
bool isConverted () const;
void saveOldFormat (); void saveOldFormat ();
void reset (); void reset ();

View file

@ -69,7 +69,7 @@ CR_DECLARE_SCOPED_ENUM (StatusIconCache,
class MessageDispatcher final : public Singleton <MessageDispatcher> { class MessageDispatcher final : public Singleton <MessageDispatcher> {
private: private:
using MsgFunc = void (MessageDispatcher::*) (); using MsgFunc = void (MessageDispatcher:: *) ();
using MsgHash = Hash <int32_t>; using MsgHash = Hash <int32_t>;
private: private:
@ -81,9 +81,9 @@ private:
}; };
public: public:
Args (float value) : float_ (value) { } Args (float value) : float_ (value) {}
Args (int32_t value) : long_ (value) { } Args (int32_t value) : long_ (value) {}
Args (const char *value) : chars_ (value) { } Args (const char *value) : chars_ (value) {}
}; };
private: private:

View file

@ -21,10 +21,10 @@ CR_DECLARE_SCOPED_ENUM (AStarResult,
Success = 0, Success = 0,
Failed, Failed,
InternalError, InternalError,
) )
// node added // node added
using NodeAdderFn = const Lambda <bool (int)> &; using NodeAdderFn = const Lambda <bool (int)> &;
// route twin node // route twin node
template <typename HT> struct RouteTwin final { template <typename HT> struct RouteTwin final {
@ -58,7 +58,7 @@ public:
static float gfunctionKillsDist (int team, int currentIndex, int parentIndex); static float gfunctionKillsDist (int team, int currentIndex, int parentIndex);
// least kills and number of nodes to goal for a team (when with hostage) // least kills and number of nodes to goal for a team (when with hostage)
static float gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex); static float gfunctionKillsDistCTWithHostage (int team, int currentIndex, int parentIndex);
// least kills to goal for a team // least kills to goal for a team
static float gfunctionKills (int team, int currentIndex, int); static float gfunctionKills (int team, int currentIndex, int);

View file

@ -22,7 +22,8 @@ CR_DECLARE_SCOPED_ENUM (StorageOption,
Official = cr::bit (4), // this is additional flag for graph indicates graph are official Official = cr::bit (4), // this is additional flag for graph indicates graph are official
Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad Recovered = cr::bit (5), // this is additional flag indicates graph converted from podbot and was bad
Exten = cr::bit (6), // this is additional flag indicates that there's extension info Exten = cr::bit (6), // this is additional flag indicates that there's extension info
Analyzed = cr::bit (7) // this graph has been analyzed Analyzed = cr::bit (7), // this graph has been analyzed
Converted = cr::bit (8) // converted from a pwf format
) )
// storage header versions // storage header versions

View file

@ -54,6 +54,11 @@ public:
bool isReady () const { bool isReady () const {
return !m_rebuild; return !m_rebuild;
} }
// is visible fromr both points ?
bool visibleBothSides (int srcIndex, int destIndex, VisIndex vis = VisIndex::Any) const {
return visible (srcIndex, destIndex, vis) && visible (destIndex, srcIndex, vis);
}
}; };
// expose global // expose global

View file

@ -23,7 +23,7 @@ using namespace cr;
// tasks definition // tasks definition
struct BotTask { struct BotTask {
using Function = void (Bot::*) (); using Function = void (Bot:: *) ();
public: public:
Function func {}; // corresponding exec function in bot class Function func {}; // corresponding exec function in bot class
@ -34,7 +34,7 @@ public:
bool resume {}; // if task can be continued if interrupted bool resume {}; // if task can be continued if interrupted
public: public:
BotTask (Function func, Task id, float desire, int data, float time, bool resume) : func (func), id (id), desire (desire), data (data), time (time), resume (resume) { } BotTask (Function func, Task id, float desire, int data, float time, bool resume) : func (func), id (id), desire (desire), data (data), time (time), resume (resume) {}
}; };
// weapon properties structure // weapon properties structure
@ -82,10 +82,9 @@ public:
int penetratePower, int penetratePower,
int maxClip, int maxClip,
int type, int type,
bool fireHold) : id (id), name (name), model (model), price (price), minPrimaryAmmo (minPriAmmo), teamStandard (teamStd), bool fireHold) : id (id), name (name), model (model), price (price), minPrimaryAmmo (minPriAmmo), teamStandard (teamStd),
teamAS (teamAs), buyGroup (buyGroup), buySelect (buySelect), buySelectT (buySelectT), buySelectCT (buySelectCT), teamAS (teamAs), buyGroup (buyGroup), buySelect (buySelect), buySelectT (buySelectT), buySelectCT (buySelectCT),
penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) {}
{ }
}; };
// clients noise // clients noise
@ -123,6 +122,16 @@ struct BotTeamData {
int32_t lastRadioSlot = { kInvalidRadioSlot }; // last radio message for team int32_t lastRadioSlot = { kInvalidRadioSlot }; // last radio message for team
}; };
// bot difficulty data
struct BotDifficultyData {
float reaction[2] {};
int32_t headshotPct {};
int32_t seenThruPct {};
int32_t hearThruPct {};
int32_t maxRecoil {};
Vector aimError {};
};
// include bot graph stuff // include bot graph stuff
#include <graph.h> #include <graph.h>
#include <vision.h> #include <vision.h>
@ -210,6 +219,8 @@ private:
mutable Mutex m_pathFindLock {}; mutable Mutex m_pathFindLock {};
mutable Mutex m_predictLock {}; mutable Mutex m_predictLock {};
float f_wpt_tim_str_chg;
private: private:
uint32_t m_states {}; // sensing bitstates uint32_t m_states {}; // sensing bitstates
uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements
@ -240,11 +251,10 @@ private:
int m_lastPredictLength {}; // last predicted path length int m_lastPredictLength {}; // last predicted path length
int m_pickupType {}; // type of entity which needs to be used/picked up int m_pickupType {}; // type of entity which needs to be used/picked up
float m_headedTime {}; float m_headedTime {}; // last time followed by radio entity
float m_prevTime {}; // time previously checked movement speed float m_prevTime {}; // time previously checked movement speed
float m_heavyTimestamp {}; // is it time to execute heavy-weight functions float m_heavyTimestamp {}; // is it time to execute heavy-weight functions
float m_prevSpeed {}; // speed some frames before float m_prevSpeed {}; // speed some frames before
float m_prevVelocity {}; // velocity some frames before
float m_timeDoorOpen {}; // time to next door open check float m_timeDoorOpen {}; // time to next door open check
float m_timeHitDoor {}; // specific time after hitting the door float m_timeHitDoor {}; // specific time after hitting the door
float m_lastChatTime {}; // time bot last chatted float m_lastChatTime {}; // time bot last chatted
@ -361,7 +371,6 @@ private:
Vector m_lookAtPredict {}; // aiming vector when predicting Vector m_lookAtPredict {}; // aiming vector when predicting
Vector m_desiredVelocity {}; // desired velocity for jump nodes Vector m_desiredVelocity {}; // desired velocity for jump nodes
Vector m_breakableOrigin {}; // origin of breakable Vector m_breakableOrigin {}; // origin of breakable
Vector m_rightRef {}; // right referential vector
Vector m_checkFallPoint[2] {}; // check fall point Vector m_checkFallPoint[2] {}; // check fall point
Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables Array <edict_t *> m_ignoredBreakable {}; // list of ignored breakables
@ -370,6 +379,7 @@ private:
UniquePtr <class PlayerHitboxEnumerator> m_hitboxEnumerator {}; UniquePtr <class PlayerHitboxEnumerator> m_hitboxEnumerator {};
BotDifficultyData *m_difficultyData {};
Path *m_path {}; // pointer to the current path node Path *m_path {}; // pointer to the current path node
String m_chatBuffer {}; // space for strings (say text...) String m_chatBuffer {}; // space for strings (say text...)
Frustum::Planes m_viewFrustum {}; Frustum::Planes m_viewFrustum {};
@ -529,7 +539,6 @@ private:
void syncUpdatePredictedIndex (); void syncUpdatePredictedIndex ();
void updatePredictedIndex (); void updatePredictedIndex ();
void refreshCreatureStatus (char *infobuffer); void refreshCreatureStatus (char *infobuffer);
void updateRightRef ();
void donateC4ToHuman (); void donateC4ToHuman ();
void clearAmmoInfo (); void clearAmmoInfo ();
void handleChatterTaskChange (Task tid); void handleChatterTaskChange (Task tid);
@ -681,6 +690,7 @@ public:
int m_ammoInClip[kMaxWeapons] {}; // ammo in clip for each weapons int m_ammoInClip[kMaxWeapons] {}; // ammo in clip for each weapons
int m_ammo[MAX_AMMO_SLOTS] {}; // total ammo amounts int m_ammo[MAX_AMMO_SLOTS] {}; // total ammo amounts
int m_deathCount {}; // number of bot deaths int m_deathCount {}; // number of bot deaths
int m_ladderDir {}; // ladder move direction
bool m_isVIP {}; // bot is vip? bool m_isVIP {}; // bot is vip?
bool m_isAlive {}; // has the player been killed or has he just respawned bool m_isAlive {}; // has the player been killed or has he just respawned
@ -700,7 +710,7 @@ public:
bool m_hasHostage {}; // does bot owns some hostages bool m_hasHostage {}; // does bot owns some hostages
bool m_hasProgressBar {}; // has progress bar on a HUD bool m_hasProgressBar {}; // has progress bar on a HUD
bool m_jumpReady {}; // is double jump ready bool m_jumpReady {}; // is double jump ready
bool m_canChooseAimDirection {}; // can choose aiming direction bool m_canSetAimDirection {}; // can choose aiming direction
bool m_isEnemyReachable {}; // direct line to enemy bool m_isEnemyReachable {}; // direct line to enemy
bool m_kickedByRotation {}; // is bot kicked due to rotation ? bool m_kickedByRotation {}; // is bot kicked due to rotation ?
bool m_kickMeFromServer {}; // kick the bot off the server? bool m_kickMeFromServer {}; // kick the bot off the server?
@ -768,6 +778,7 @@ public:
void startDoubleJump (edict_t *ent); void startDoubleJump (edict_t *ent);
void sendBotToOrigin (const Vector &origin); void sendBotToOrigin (const Vector &origin);
void markStale (); void markStale ();
void setNewDifficulty (int32_t newDifficulty);
bool hasHostage (); bool hasHostage ();
bool hasPrimaryWeapon () const; bool hasPrimaryWeapon () const;
bool hasSecondaryWeapon () const; bool hasSecondaryWeapon () const;
@ -942,6 +953,7 @@ extern ConVar cv_ignore_enemies_after_spawn_time;
extern ConVar cv_camping_time_min; extern ConVar cv_camping_time_min;
extern ConVar cv_camping_time_max; extern ConVar cv_camping_time_max;
extern ConVar cv_smoke_grenade_checks; extern ConVar cv_smoke_grenade_checks;
extern ConVar cv_smoke_greande_checks_radius;
extern ConVar cv_check_darkness; extern ConVar cv_check_darkness;
extern ConVar cv_use_hitbox_enemy_targeting; extern ConVar cv_use_hitbox_enemy_targeting;
extern ConVar cv_restricted_weapons; extern ConVar cv_restricted_weapons;

View file

@ -130,7 +130,7 @@ void GraphAnalyze::update () {
} }
void GraphAnalyze::suspend () { void GraphAnalyze::suspend () {
m_updateInterval = 0.0f; m_updateInterval = kInfiniteDistance;
m_isAnalyzing = false; m_isAnalyzing = false;
m_isAnalyzed = false; m_isAnalyzed = false;
m_basicsCreated = false; m_basicsCreated = false;
@ -325,7 +325,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
if (cr::fequal (tr.flFraction, 1.0f)) { if (cr::fequal (tr.flFraction, 1.0f)) {
return; return;
} }
Vector nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f }; const Vector &nextPos = { tr.vecEndPos.x, tr.vecEndPos.y, tr.vecEndPos.z + 19.0f };
const int endIndex = graph.getForAnalyzer (nextPos, range); const int endIndex = graph.getForAnalyzer (nextPos, range);
const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f); const int targetIndex = graph.getNearestNoBuckets (nextPos, 250.0f);
@ -333,7 +333,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
if (graph.exists (endIndex) || !graph.exists (targetIndex)) { if (graph.exists (endIndex) || !graph.exists (targetIndex)) {
return; return;
} }
auto targetPos = graph[targetIndex].origin; const auto &targetPos = graph[targetIndex].origin;
// re-check there's nothing nearby, and add something we're want // re-check there's nothing nearby, and add something we're want
if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) { if (!graph.exists (graph.getNearestNoBuckets (nextPos, range))) {
@ -348,6 +348,7 @@ void GraphAnalyze::flood (const Vector &pos, const Vector &next, float range) {
if ((graph.isNodeReacheable (targetPos, testPos) if ((graph.isNodeReacheable (targetPos, testPos)
&& graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos)
&& graph.isNodeReacheableWithJump (targetPos, testPos))) { && graph.isNodeReacheableWithJump (targetPos, testPos))) {
graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos); graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos);
} }
} }

View file

@ -37,7 +37,6 @@ ConVar cv_pickup_custom_items ("pickup_custom_items", "0", "Allows or disallows
ConVar cv_pickup_ammo_and_kits ("pickup_ammo_and_kits", "0", "Allows bots to pick up mod items like ammo, health kits, and suits."); ConVar cv_pickup_ammo_and_kits ("pickup_ammo_and_kits", "0", "Allows bots to pick up mod items like ammo, health kits, and suits.");
ConVar cv_pickup_best ("pickup_best", "1", "Allows or disallows bots to pick up the best weapons."); ConVar cv_pickup_best ("pickup_best", "1", "Allows or disallows bots to pick up the best weapons.");
ConVar cv_ignore_objectives ("ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and save hostages."); ConVar cv_ignore_objectives ("ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and save hostages.");
ConVar cv_smoke_grenade_checks ("smoke_grenade_checks", "1", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f);
// game console variables // game console variables
ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef);
@ -100,7 +99,7 @@ void Bot::avoidGrenades () {
if (!(m_states & Sense::SeeingEnemy)) { if (!(m_states & Sense::SeeingEnemy)) {
m_lookAt.y = cr::wrapAngle ((game.getEntityOrigin (pent) - getEyesPos ()).angles ().y + 180.0f); m_lookAt.y = cr::wrapAngle ((game.getEntityOrigin (pent) - getEyesPos ()).angles ().y + 180.0f);
m_canChooseAimDirection = false; m_canSetAimDirection = false;
m_preventFlashing = game.time () + rg (1.0f, 2.0f); m_preventFlashing = game.time () + rg (1.0f, 2.0f);
} }
} }
@ -187,6 +186,7 @@ void Bot::checkBreakablesAround () {
|| !game.hasBreakables () || !game.hasBreakables ()
|| m_seeEnemyTime + 4.0f > game.time () || m_seeEnemyTime + 4.0f > game.time ()
|| !game.isNullEntity (m_enemy) || !game.isNullEntity (m_enemy)
|| (m_aimFlags & (AimFlags::PredictPath | AimFlags::Danger))
|| !hasPrimaryWeapon ()) { || !hasPrimaryWeapon ()) {
return; return;
} }
@ -320,15 +320,14 @@ void Bot::setIdealReactionTimers (bool actual) {
return; // zero out reaction times for extreme mode return; // zero out reaction times for extreme mode
} }
const auto tweak = conf.getDifficultyTweaks (m_difficulty);
if (actual) { if (actual) {
m_idealReactionTime = tweak->reaction[0]; m_idealReactionTime = m_difficultyData->reaction[0];
m_actualReactionTime = tweak->reaction[0]; m_actualReactionTime = m_difficultyData->reaction[0];
return; return;
} }
m_idealReactionTime = rg (tweak->reaction[0], tweak->reaction[1]); m_idealReactionTime = rg (m_difficultyData->reaction[0], m_difficultyData->reaction[1]);
} }
void Bot::updatePickups () { void Bot::updatePickups () {
@ -1644,47 +1643,30 @@ void Bot::buyStuff () {
} }
void Bot::updateEmotions () { void Bot::updateEmotions () {
// slowly increase/decrease dynamic emotions back to their base level constexpr auto kEmotionUpdateStep = 0.05f;
if (m_nextEmotionUpdate > game.time ()) { if (m_nextEmotionUpdate > game.time ()) {
return; return;
} }
if (m_seeEnemyTime + 1.0f > game.time ()) { if (m_seeEnemyTime + 1.0f > game.time ()) {
m_agressionLevel += 0.05f; m_agressionLevel = cr::min (m_agressionLevel + kEmotionUpdateStep, 1.0f);
if (m_agressionLevel > 1.0f) {
m_agressionLevel = 1.0f;
}
} }
else if (m_seeEnemyTime + 5.0f < game.time ()) { else if (m_seeEnemyTime + 5.0f < game.time ()) {
// smoothly return aggression to base level
if (m_agressionLevel > m_baseAgressionLevel) { if (m_agressionLevel > m_baseAgressionLevel) {
m_agressionLevel -= 0.05f; m_agressionLevel = cr::max (m_agressionLevel - kEmotionUpdateStep, m_baseAgressionLevel);
} }
else { else {
m_agressionLevel += 0.05f; m_agressionLevel = cr::min (m_agressionLevel + kEmotionUpdateStep, m_baseAgressionLevel);
} }
// smoothly return fear to base level
if (m_fearLevel > m_baseFearLevel) { if (m_fearLevel > m_baseFearLevel) {
m_fearLevel -= 0.05f; m_fearLevel = cr::max (m_fearLevel - kEmotionUpdateStep, m_baseFearLevel);
} }
else { else {
m_fearLevel += 0.05f; m_fearLevel = cr::min (m_fearLevel + kEmotionUpdateStep, m_baseFearLevel);
}
if (m_agressionLevel > 1.0f) {
m_agressionLevel = 1.0f;
}
if (m_fearLevel > 1.0f) {
m_fearLevel = 1.0f;
}
if (m_agressionLevel < 0.0f) {
m_agressionLevel = 0.0f;
}
if (m_fearLevel < 0.0f) {
m_fearLevel = 0.0f;
} }
} }
m_nextEmotionUpdate = game.time () + 0.5f; m_nextEmotionUpdate = game.time () + 0.5f;
@ -1922,7 +1904,6 @@ void Bot::setConditions () {
} }
else if (rg.chance (60)) { else if (rg.chance (60)) {
if (m_lastVictim->v.weapons & kSniperWeaponMask) { if (m_lastVictim->v.weapons & kSniperWeaponMask) {
pushChatterMessage (Chatter::SniperKilled); pushChatterMessage (Chatter::SniperKilled);
} }
else { else {
@ -3101,7 +3082,7 @@ void Bot::frame () {
void Bot::update () { void Bot::update () {
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
m_canChooseAimDirection = true; m_canSetAimDirection = true;
m_isAlive = game.isAliveEntity (ent ()); m_isAlive = game.isAliveEntity (ent ());
m_team = game.getPlayerTeam (ent ()); m_team = game.getPlayerTeam (ent ());
m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f); m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f);
@ -3242,7 +3223,7 @@ void Bot::logicDuringFreezetime () {
if (ent) { if (ent) {
m_lookAt = ent->v.origin + ent->v.view_ofs; m_lookAt = ent->v.origin + ent->v.view_ofs;
if (m_buyingFinished) { if (m_buyingFinished && game.getPlayerTeam (ent) != m_team) {
m_enemy = ent; m_enemy = ent;
m_enemyOrigin = ent->v.origin; m_enemyOrigin = ent->v.origin;
} }
@ -3338,12 +3319,11 @@ void Bot::checkSpawnConditions () {
void Bot::logic () { void Bot::logic () {
// this function gets called each frame and is the core of all bot ai. from here all other subroutines are called // this function gets called each frame and is the core of all bot ai. from here all other subroutines are called
m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved)
resetMovement (); resetMovement ();
// increase reaction time // increase reaction time
m_actualReactionTime += 0.3f; m_actualReactionTime += 0.3f;
m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved)
if (m_actualReactionTime > m_idealReactionTime) { if (m_actualReactionTime > m_idealReactionTime) {
m_actualReactionTime = m_idealReactionTime; m_actualReactionTime = m_idealReactionTime;
@ -3364,13 +3344,11 @@ void Bot::logic () {
if (m_prevTime <= game.time ()) { if (m_prevTime <= game.time ()) {
// see how far bot has moved since the previous position... // see how far bot has moved since the previous position...
if (m_checkTerrain) { m_movedDistance = m_prevOrigin.distanceSq (pev->origin);
m_movedDistance = m_prevOrigin.distance (pev->origin);
}
// save current position as previous // save current position as previous
m_prevOrigin = pev->origin; m_prevOrigin = pev->origin;
m_prevTime = game.time () + (0.15f - m_frameInterval * 2.0f); m_prevTime = game.time () + 0.2f - m_frameInterval;
} }
// if there's some radio message to respond, check it // if there's some radio message to respond, check it
@ -3463,7 +3441,7 @@ void Bot::logic () {
// ensure we're not stuck picking something // ensure we're not stuck picking something
if (m_moveToGoal && m_moveSpeed > 0.0f if (m_moveToGoal && m_moveSpeed > 0.0f
&& rg (2.5f, 3.5f) + m_navTimeset + m_destOrigin.distanceSq2d (pev->origin) / cr::sqrf (m_moveSpeed) < game.time () && rg (2.5f, 3.5f) + m_navTimeset + m_destOrigin.distanceSq2d (pev->origin) / cr::sqrf (cr::max (1.0f, m_moveSpeed)) < game.time ()
&& !(m_states & Sense::SeeingEnemy)) { && !(m_states & Sense::SeeingEnemy)) {
ensurePickupEntitiesClear (); ensurePickupEntitiesClear ();
} }
@ -3478,8 +3456,6 @@ void Bot::logic () {
// save the previous speed (for checking if stuck) // save the previous speed (for checking if stuck)
m_prevSpeed = cr::abs (m_moveSpeed); m_prevSpeed = cr::abs (m_moveSpeed);
m_prevVelocity = cr::abs (pev->velocity.length2d ());
m_lastDamageType = -1; // reset damage m_lastDamageType = -1; // reset damage
} }
@ -3643,7 +3619,7 @@ void Bot::showDebugOverlay () {
} }
bool Bot::hasHostage () { bool Bot::hasHostage () {
if (cv_ignore_objectives || game.mapIs (MapFlags::Demolition)) { if (cv_ignore_objectives || !game.mapIs (MapFlags::HostageRescue)) {
return false; return false;
} }
@ -3930,9 +3906,6 @@ void Bot::resetDoubleJump () {
} }
void Bot::debugMsgInternal (StringRef str) { void Bot::debugMsgInternal (StringRef str) {
if (game.isDedicated ()) {
return;
}
const int level = cv_debug.as <int> (); const int level = cv_debug.as <int> ();
if (level <= 2) { if (level <= 2) {
@ -3943,7 +3916,10 @@ void Bot::debugMsgInternal (StringRef str) {
bool playMessage = false; bool playMessage = false;
if (level == 3 && !game.isNullEntity (game.getLocalEntity ()) && game.getLocalEntity ()->v.iuser2 == entindex ()) { if (level == 3
&& !game.isNullEntity (game.getLocalEntity ())
&& game.getLocalEntity ()->v.iuser2 == entindex ()) {
playMessage = true; playMessage = true;
} }
else if (level != 3) { else if (level != 3) {
@ -3954,7 +3930,7 @@ void Bot::debugMsgInternal (StringRef str) {
logger.message (printBuf.chars ()); logger.message (printBuf.chars ());
} }
if (playMessage) { if (playMessage && !game.isDedicated ()) {
ctrl.msg (printBuf.chars ()); ctrl.msg (printBuf.chars ());
sendToChat (printBuf, false); sendToChat (printBuf, false);
} }
@ -4125,7 +4101,7 @@ void Bot::updateHearing () {
if (!(client.flags & ClientFlags::Used) if (!(client.flags & ClientFlags::Used)
|| !(client.flags & ClientFlags::Alive) || !(client.flags & ClientFlags::Alive)
|| client.ent == ent () || client.ent == ent ()
|| client.team == m_team || client.team2 == m_team
|| !client.ent || !client.ent
|| client.noise.last < game.time ()) { || client.noise.last < game.time ()) {
@ -4225,7 +4201,7 @@ void Bot::updateHearing () {
else { else {
if (cv_shoots_thru_walls if (cv_shoots_thru_walls
&& m_lastEnemy == m_hearedEnemy && m_lastEnemy == m_hearedEnemy
&& rg.chance (conf.getDifficultyTweaks (m_difficulty)->hearThruPct) && rg.chance (m_difficultyData->hearThruPct)
&& m_seeEnemyTime + 3.0f > game.time () && m_seeEnemyTime + 3.0f > game.time ()
&& isPenetrableObstacle (m_hearedEnemy->v.origin)) { && isPenetrableObstacle (m_hearedEnemy->v.origin)) {
@ -4244,7 +4220,7 @@ void Bot::updateHearing () {
void Bot::enteredBuyZone (int buyState) { void Bot::enteredBuyZone (int buyState) {
// this function is gets called when bot enters a buyzone, to allow bot to buy some stuff // this function is gets called when bot enters a buyzone, to allow bot to buy some stuff
if (m_isCreature) { if (m_isCreature || hasHostage ()) {
return; // creatures can't buy anything return; // creatures can't buy anything
} }
const int *econLimit = conf.getEconLimit (); const int *econLimit = conf.getEconLimit ();

View file

@ -614,7 +614,7 @@ bool Bot::lookupEnemies () {
// if no enemy visible check if last one shoot able through wall // if no enemy visible check if last one shoot able through wall
if (cv_shoots_thru_walls if (cv_shoots_thru_walls
&& rg.chance (conf.getDifficultyTweaks (m_difficulty)->seenThruPct) && rg.chance (m_difficultyData->seenThruPct)
&& isPenetrableObstacle (newEnemy->v.origin)) { && isPenetrableObstacle (newEnemy->v.origin)) {
m_seeEnemyTime = game.time (); m_seeEnemyTime = game.time ();
@ -671,7 +671,7 @@ Vector Bot::getBodyOffsetError (float distance) {
rg (mins.y * hitError, maxs.y * hitError), rg (mins.y * hitError, maxs.y * hitError),
rg (mins.z * hitError * 0.5f, maxs.z * hitError * 0.5f)); rg (mins.z * hitError * 0.5f, maxs.z * hitError * 0.5f));
const auto &aimError = conf.getDifficultyTweaks (m_difficulty)->aimError; const auto &aimError = m_difficultyData->aimError;
m_aimLastError += Vector (rg (-aimError.x, aimError.x), rg (-aimError.y, aimError.y), rg (-aimError.z, aimError.z)); m_aimLastError += Vector (rg (-aimError.x, aimError.x), rg (-aimError.y, aimError.y), rg (-aimError.z, aimError.z));
m_aimErrorTime = game.time () + rg (0.4f, 0.8f); m_aimErrorTime = game.time () + rg (0.4f, 0.8f);
@ -722,7 +722,7 @@ Vector Bot::getEnemyBodyOffset () {
else if (game.isPlayerEntity (m_enemy)) { else if (game.isPlayerEntity (m_enemy)) {
// 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 = m_difficultyData->headshotPct;
// with to much recoil or using specific weapons choice to aim to the chest // with to much recoil or using specific weapons choice to aim to the chest
if (distance > kSprayDistance && (isRecoilHigh () || usesShotgun ())) { if (distance > kSprayDistance && (isRecoilHigh () || usesShotgun ())) {
@ -789,10 +789,10 @@ Vector Bot::getCustomHeight (float distance) const {
}; };
constexpr float kOffsetRanges[9][3] = { constexpr float kOffsetRanges[9][3] = {
{ 0.0f, 0.0f, 0.0f }, // none { 0.0f, 0.0f, 0.0f }, // none
{ 0.0f, 0.0f, 0.0f }, // melee { 0.0f, 0.0f, 0.0f }, // melee
{ 0.5f, -0.1f, -1.5f }, // pistol { 0.5f, -0.1f, -1.5f }, // pistol
{ 6.5f, 6.0f, -2.0f }, // shotgun { 6.5f, 6.0f, -2.0f }, // shotgun
{ 0.5f, -7.5f, -9.5f }, // zoomrifle { 0.5f, -7.5f, -9.5f }, // zoomrifle
{ 0.5f, -7.5f, -9.5f }, // rifle { 0.5f, -7.5f, -9.5f }, // rifle
{ 0.5f, -7.5f, -9.5f }, // smg { 0.5f, -7.5f, -9.5f }, // smg
@ -1014,7 +1014,7 @@ bool Bot::needToPauseFiring (float distance) {
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 > kSprayDistance ? 0.55f : 0.38f; const float baseTime = distance > kSprayDistance ? 0.55f : 0.38f;
const float maxRecoil = static_cast <float> (conf.getDifficultyTweaks (m_difficulty)->maxRecoil); const float maxRecoil = static_cast <float> (m_difficultyData->maxRecoil);
// check if we need to compensate recoil // check if we need to compensate recoil
if (cr::tanf (cr::sqrtf (cr::abs (xPunch) + cr::abs (yPunch))) * distance > offset + maxRecoil + tolerance) { if (cr::tanf (cr::sqrtf (cr::abs (xPunch) + cr::abs (yPunch))) * distance > offset + maxRecoil + tolerance) {
@ -1626,9 +1626,9 @@ void Bot::attackMovement () {
if (m_difficulty >= Difficulty::Normal if (m_difficulty >= Difficulty::Normal
&& distanceSq < cr::sqrf (kSprayDistance) && distanceSq < cr::sqrf (kSprayDistance)
&& (m_jumpTime + 5.0f < game.time () && (m_jumpTime + 5.0f < game.time ()
&& isOnFloor () && isOnFloor ()
&& rg (0, 1000) < (m_isReloading ? 8 : 2) && rg (0, 1000) < (m_isReloading ? 8 : 2)
&& pev->velocity.length2d () > 150.0f) && !usesSniper () && isEnemyCone) { && pev->velocity.length2d () > 150.0f) && !usesSniper () && isEnemyCone) {
pev->button |= IN_JUMP; pev->button |= IN_JUMP;
} }
@ -1646,8 +1646,7 @@ void Bot::attackMovement () {
const int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); const int enemyNearestIndex = graph.getNearest (m_enemy->v.origin);
if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) if (vistab.visibleBothSides (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch)) {
&& vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) {
m_duckTime = game.time () + m_frameInterval * 3.0f; m_duckTime = game.time () + m_frameInterval * 3.0f;
} }
} }

View file

@ -46,7 +46,7 @@ void BotConfig::loadMainConfig (bool isFirstLoad) {
return false; return false;
}; };
auto storeVarValue = [] (cvar_t *c, StringRef value) { auto storeVarValue = [] (cvar_t *c, StringRef value) {
auto &cvars = game.getCvars (); auto &cvars = game.getCvars ();
for (auto &var : cvars) { for (auto &var : cvars) {
@ -303,9 +303,9 @@ void BotConfig::loadChatterConfig () {
{ "Chatter_GuardingEscapeZone", Chatter::GuardingEscapeZone, kMaxChatterRepeatInterval }, { "Chatter_GuardingEscapeZone", Chatter::GuardingEscapeZone, kMaxChatterRepeatInterval },
{ "Chatter_GuardingVipSafety", Chatter::GuardingVIPSafety, kMaxChatterRepeatInterval }, { "Chatter_GuardingVipSafety", Chatter::GuardingVIPSafety, kMaxChatterRepeatInterval },
{ "Chatter_PlantingC4", Chatter::PlantingBomb, 10.0f }, { "Chatter_PlantingC4", Chatter::PlantingBomb, 10.0f },
{ "Chatter_InCombat", Chatter::InCombat, kMaxChatterRepeatInterval }, { "Chatter_InCombat", Chatter::InCombat, kMaxChatterRepeatInterval },
{ "Chatter_SeeksEnemy", Chatter::SeekingEnemies, kMaxChatterRepeatInterval }, { "Chatter_SeeksEnemy", Chatter::SeekingEnemies, kMaxChatterRepeatInterval },
{ "Chatter_Nothing", Chatter::Nothing, kMaxChatterRepeatInterval }, { "Chatter_Nothing", Chatter::Nothing, kMaxChatterRepeatInterval },
{ "Chatter_EnemyDown", Chatter::EnemyDown, 10.0f }, { "Chatter_EnemyDown", Chatter::EnemyDown, 10.0f },
{ "Chatter_UseHostage", Chatter::UsingHostages, kMaxChatterRepeatInterval }, { "Chatter_UseHostage", Chatter::UsingHostages, kMaxChatterRepeatInterval },
{ "Chatter_WonTheRound", Chatter::WonTheRound, kMaxChatterRepeatInterval }, { "Chatter_WonTheRound", Chatter::WonTheRound, kMaxChatterRepeatInterval },
@ -350,7 +350,7 @@ void BotConfig::loadChatterConfig () {
{ "Chatter_BombSiteSecured", Chatter::BombsiteSecured, 3.5f }, { "Chatter_BombSiteSecured", Chatter::BombsiteSecured, 3.5f },
{ "Chatter_GoingToCamp", Chatter::GoingToCamp, 30.0f }, { "Chatter_GoingToCamp", Chatter::GoingToCamp, 30.0f },
{ "Chatter_Camp", Chatter::Camping, 10.0f }, { "Chatter_Camp", Chatter::Camping, 10.0f },
{ "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval}, { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval },
}; };
Array <String> missingWaves {}; Array <String> missingWaves {};
@ -632,7 +632,7 @@ void BotConfig::loadDifficultyConfig () {
}; };
m_difficulty[Difficulty::Expert] = { m_difficulty[Difficulty::Expert] = {
{ 0.1f, 0.2f }, 100, 90, 90, 21, { 0.0f, 0.0f, 0.0f } { 0.1f, 0.2f }, 100, 90, 90, 21, { 0.0f, 0.0f, 0.0f }
}; };
// currently, mindelay, maxdelay, headprob, seenthruprob, heardthruprob, recoil, aim_error {x,y,z} // currently, mindelay, maxdelay, headprob, seenthruprob, heardthruprob, recoil, aim_error {x,y,z}
@ -721,12 +721,12 @@ void BotConfig::loadCustomConfig () {
auto setDefaults = [&] () { auto setDefaults = [&] () {
m_custom = { m_custom = {
{ "C4ModelName", "c4.mdl" }, { "C4ModelName", "c4.mdl" },
{ "AMXParachuteCvar", "sv_parachute" }, { "AMXParachuteCvar", "sv_parachute" },
{ "CustomCSDMSpawnPoint", "view_spawn" }, { "CustomCSDMSpawnPoint", "view_spawn" },
{ "CSDMDetectCvar", "csdm_active" }, { "CSDMDetectCvar", "csdm_active" },
{ "ZMDetectCvar", "zp_delay" }, { "ZMDetectCvar", "zp_delay" },
{ "ZMDelayCvar", "zp_delay" }, { "ZMDelayCvar", "zp_delay" },
{ "ZMInfectedTeam", "T" }, { "ZMInfectedTeam", "T" },
{ "EnableFakeBotFeatures", "no" }, { "EnableFakeBotFeatures", "no" },
{ "DisableLogFile", "no" }, { "DisableLogFile", "no" },

View file

@ -423,6 +423,7 @@ int BotControl::cmdNode () {
addGraphCmd ("stats", "stats [noarguments]", "Shows the stats about node types on the map.", &BotControl::cmdNodeShowStats); addGraphCmd ("stats", "stats [noarguments]", "Shows the stats about node types on the map.", &BotControl::cmdNodeShowStats);
addGraphCmd ("fileinfo", "fileinfo [noarguments]", "Shows basic information about graph file.", &BotControl::cmdNodeFileInfo); addGraphCmd ("fileinfo", "fileinfo [noarguments]", "Shows basic information about graph file.", &BotControl::cmdNodeFileInfo);
addGraphCmd ("adjust_height", "adjust_height [height offset]", "Modifies all the graph nodes height (z-component) with specified offset.", &BotControl::cmdNodeAdjustHeight); addGraphCmd ("adjust_height", "adjust_height [height offset]", "Modifies all the graph nodes height (z-component) with specified offset.", &BotControl::cmdNodeAdjustHeight);
addGraphCmd ("refresh", "refresh [noarguments]", "Deletes a current graph and downloads one from graph database.", &BotControl::cmdNodeRefresh);
// add path commands // add path commands
addGraphCmd ("path_create", "path_create [noarguments]", "Opens and displays path creation menu.", &BotControl::cmdNodePathCreate); addGraphCmd ("path_create", "path_create [noarguments]", "Opens and displays path creation menu.", &BotControl::cmdNodePathCreate);
@ -598,6 +599,13 @@ int BotControl::cmdNodeAddBasic () {
int BotControl::cmdNodeSave () { int BotControl::cmdNodeSave () {
enum args { graph_cmd = 1, cmd, option }; enum args { graph_cmd = 1, cmd, option };
// prevent some commands while analyzing graph
if (analyzer.isAnalyzing ()) {
msg ("This command is unavailable while map analysis is ongoing.");
return BotCommandResult::Handled;
}
// if no check is set save anyway // if no check is set save anyway
if (arg <StringRef> (option) == "nocheck") { if (arg <StringRef> (option) == "nocheck") {
graph.saveGraphData (); graph.saveGraphData ();
@ -629,6 +637,13 @@ int BotControl::cmdNodeSave () {
int BotControl::cmdNodeLoad () { int BotControl::cmdNodeLoad () {
enum args { graph_cmd = 1, cmd }; enum args { graph_cmd = 1, cmd };
// prevent some commands while analyzing graph
if (analyzer.isAnalyzing ()) {
msg ("This command is unavailable while map analysis is ongoing.");
return BotCommandResult::Handled;
}
// just save graph on request // just save graph on request
if (graph.loadGraphData ()) { if (graph.loadGraphData ()) {
msg ("Graph successfully loaded."); msg ("Graph successfully loaded.");
@ -642,6 +657,13 @@ int BotControl::cmdNodeLoad () {
int BotControl::cmdNodeErase () { int BotControl::cmdNodeErase () {
enum args { graph_cmd = 1, cmd, iamsure }; enum args { graph_cmd = 1, cmd, iamsure };
// prevent some commands while analyzing graph
if (analyzer.isAnalyzing ()) {
msg ("This command is unavailable while map analysis is ongoing.");
return BotCommandResult::Handled;
}
// prevent accidents when graph are deleted unintentionally // prevent accidents when graph are deleted unintentionally
if (arg <StringRef> (iamsure) == "iamsure") { if (arg <StringRef> (iamsure) == "iamsure") {
bstor.unlinkFromDisk (false, false); bstor.unlinkFromDisk (false, false);
@ -652,6 +674,26 @@ int BotControl::cmdNodeErase () {
return BotCommandResult::Handled; return BotCommandResult::Handled;
} }
int BotControl::cmdNodeRefresh () {
enum args { graph_cmd = 1, cmd, iamsure };
if (!graph.canDownload ()) {
msg ("Can't sync graph with database while graph url is not set.");
return BotCommandResult::Handled;
}
// prevent accidents when graph are deleted unintentionally
if (arg <StringRef> (iamsure) == "iamsure") {
bstor.unlinkFromDisk (false, false);
graph.loadGraphData ();
}
else {
msg ("Please, append \"iamsure\" as parameter to get graph refreshed from the graph database.");
}
return BotCommandResult::Handled;
}
int BotControl::cmdNodeEraseTraining () { int BotControl::cmdNodeEraseTraining () {
enum args { graph_cmd = 1, cmd }; enum args { graph_cmd = 1, cmd };

View file

@ -61,9 +61,6 @@ void Game::levelInitialize (edict_t *entities, int max) {
// startup threaded worker // startup threaded worker
worker.startup (cv_threadpool_workers.as <int> ()); worker.startup (cv_threadpool_workers.as <int> ());
m_spawnCount[Team::CT] = 0;
m_spawnCount[Team::Terrorist] = 0;
// clear all breakables before initialization // clear all breakables before initialization
m_breakables.clear (); m_breakables.clear ();
m_checkedBreakables.clear (); m_checkedBreakables.clear ();
@ -126,15 +123,11 @@ void Game::levelInitialize (edict_t *entities, int max) {
ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency
ent->v.renderamt = 127; // set its transparency amount ent->v.renderamt = 127; // set its transparency amount
ent->v.effects |= EF_NODRAW; ent->v.effects |= EF_NODRAW;
++m_spawnCount[Team::CT];
} }
else if (classname == "info_player_deathmatch") { else if (classname == "info_player_deathmatch") {
ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency
ent->v.renderamt = 127; // set its transparency amount ent->v.renderamt = 127; // set its transparency amount
ent->v.effects |= EF_NODRAW; ent->v.effects |= EF_NODRAW;
++m_spawnCount[Team::Terrorist];
} }
else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") { else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") {
m_mapFlags |= MapFlags::Assassination; // assassination map m_mapFlags |= MapFlags::Assassination; // assassination map
@ -187,6 +180,24 @@ void Game::levelInitialize (edict_t *entities, int max) {
m_halfSecondFrame = 0.0f; m_halfSecondFrame = 0.0f;
} }
void Game::onSpawnEntity (edict_t *ent) {
constexpr auto kEntityInfoPlayerStart = StringRef::fnv1a32 ("info_player_start");
constexpr auto kEntityInfoVIPStart = StringRef::fnv1a32 ("info_vip_start");
constexpr auto kEntityInfoPlayerDeathmatch = StringRef::fnv1a32 ("info_player_deathmatch");
if (game.isNullEntity (ent) || ent->v.classname == 0) {
return;
}
const auto classNameHash = ent->v.classname.str ().hash ();
if (classNameHash == kEntityInfoPlayerStart || classNameHash == kEntityInfoVIPStart) {
++m_spawnCount[Team::CT];
}
else if (classNameHash == kEntityInfoPlayerDeathmatch) {
++m_spawnCount[Team::Terrorist];
}
}
void Game::levelShutdown () { void Game::levelShutdown () {
// save collected practice on shutdown // save collected practice on shutdown
practice.save (); practice.save ();
@ -223,6 +234,10 @@ void Game::levelShutdown () {
// disable command handling // disable command handling
ctrl.setDenyCommands (true); ctrl.setDenyCommands (true);
// reset spawn counts
for (auto &sc : m_spawnCount) {
sc = 0;
}
} }
void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) const { void Game::drawLine (edict_t *ent, const Vector &start, const Vector &end, int width, int noise, const Color &color, int brightness, int speed, int life, DrawLine type) const {
@ -368,9 +383,9 @@ void Game::playSound (edict_t *ent, const char *sound) {
void Game::setPlayerStartDrawModels () { void Game::setPlayerStartDrawModels () {
static HashMap <String, String> models { static HashMap <String, String> models {
{"info_player_start", "models/player/urban/urban.mdl"}, { "info_player_start", "models/player/urban/urban.mdl" },
{"info_player_deathmatch", "models/player/terror/terror.mdl"}, { "info_player_deathmatch", "models/player/terror/terror.mdl" },
{"info_vip_start", "models/player/vip/vip.mdl"} { "info_vip_start", "models/player/vip/vip.mdl" }
}; };
models.foreach ([&] (const String &key, const String &val) { models.foreach ([&] (const String &key, const String &val) {
@ -791,53 +806,50 @@ void Game::registerCvars (bool gameVars) {
} }
void Game::constructCSBinaryName (StringArray &libs) { void Game::constructCSBinaryName (StringArray &libs) {
String libSuffix {}; // construct library suffix String suffix {};
if (plat.android) { if (plat.android) {
libSuffix += "_android"; suffix = "_android";
if (plat.x64) {
suffix += "_arm64";
}
else if (plat.arm) {
suffix += "_armv7l";
}
} }
else if (plat.psvita) { else if (plat.psvita) {
libSuffix += "_psvita"; suffix = "_psvita";
} }
else if (plat.x64) {
if (plat.x64) {
if (plat.arm) { if (plat.arm) {
libSuffix += "_arm64"; suffix = "_arm64";
} }
else if (plat.ppc) { else if (plat.ppc) {
libSuffix += "_ppc64le"; suffix = "_ppc64le";
} }
else { else {
libSuffix += "_amd64"; suffix = "_amd64";
} }
} }
else { else if (plat.arm) {
if (plat.arm) { // non-android arm32
// don't want to put whole build.h logic from xash3d, just set whatever is supported by the YaPB suffix = "_armv7hf";
if (plat.android) {
libSuffix += "_armv7l";
}
else {
libSuffix += "_armv7hf";
}
}
else if (!plat.nix && !plat.win && !plat.macos) {
libSuffix += "_i386";
}
} }
else if (!plat.nix && !plat.win && !plat.macos) {
// fallback for unknown 32-bit x86 (e.g., legacy linux/bsd)
suffix = "_i386";
}
// else: suffix remains empty (e.g., x86 linux/windows/macos)
if (libSuffix.empty ()) // build base names
libs.insert (0, { "mp", "cs", "cs_i386" }); if (plat.android) {
// only "libcs" with suffix (no "mp", and must have "lib" prefix)
libs.insert (0, "libcs" + suffix);
}
else { else {
// on Android, it's important to have `lib` prefix, otherwise package manager won't unpack the libraries // Standard: "mp" and "cs" with suffix
if (plat.android) libs.insert (0, "cs" + suffix);
libs.insert (0, { "libcs" }); libs.insert (0, "mp" + suffix);
else
libs.insert (0, { "mp", "cs" });
for (auto &lib : libs) {
lib += libSuffix;
}
} }
} }
@ -881,10 +893,10 @@ bool Game::loadCSBinary () {
} }
if (plat.emscripten) { if (plat.emscripten) {
path = String(plat.env ("XASH3D_GAMELIBPATH")); // defined by launcher path = String (plat.env ("XASH3D_GAMELIBPATH")); // defined by launcher
} }
if (path.empty()) { if (path.empty ()) {
path = strings.joinPath (modname, "dlls", lib) + kLibrarySuffix; path = strings.joinPath (modname, "dlls", lib) + kLibrarySuffix;
// if we can't read file, skip it // if we can't read file, skip it
@ -940,7 +952,7 @@ bool Game::loadCSBinary () {
// no fake pings on xash3d // no fake pings on xash3d
if (!(m_gameFlags & (GameFlags::Xash3D | GameFlags::Xash3DLegacy))) { if (!(m_gameFlags & (GameFlags::Xash3D | GameFlags::Xash3DLegacy))) {
m_gameFlags |= GameFlags::HasFakePings; m_gameFlags |= GameFlags::HasFakePings;
} }
} }
else { else {

View file

@ -603,6 +603,10 @@ bool BotGraph::isAnalyzed () const {
return (m_info.header.options & StorageOption::Analyzed); return (m_info.header.options & StorageOption::Analyzed);
} }
bool BotGraph::isConverted () const {
return (m_info.header.options & StorageOption::Converted);
}
void BotGraph::add (int type, const Vector &pos) { void BotGraph::add (int type, const Vector &pos) {
if (!hasEditor () && !analyzer.isAnalyzing ()) { if (!hasEditor () && !analyzer.isAnalyzing ()) {
return; return;
@ -1292,6 +1296,7 @@ void BotGraph::showFileInfo () {
msg (" uncompressed_size: %dkB", info.uncompressed / 1024); msg (" uncompressed_size: %dkB", info.uncompressed / 1024);
msg (" options: %d", info.options); // display as string ? msg (" options: %d", info.options); // display as string ?
msg (" analyzed: %s", isAnalyzed () ? conf.translate ("yes") : conf.translate ("no")); // display as string ? msg (" analyzed: %s", isAnalyzed () ? conf.translate ("yes") : conf.translate ("no")); // display as string ?
msg (" converted: %s", isConverted () ? conf.translate ("yes") : conf.translate ("no")); // display as string ?
msg (" pathfinder: %s", planner.isPathsCheckFailed () ? "floyd" : "astar"); msg (" pathfinder: %s", planner.isPathsCheckFailed () ? "floyd" : "astar");
msg (""); msg ("");
@ -1755,6 +1760,8 @@ bool BotGraph::convertOldFormat () {
m_info.author = header.author; m_info.author = header.author;
m_info.header.options = StorageOption::Converted;
// clean editor so graph will be saved with header's author // clean editor so graph will be saved with header's author
auto editor = m_editor; auto editor = m_editor;
m_editor = nullptr; m_editor = nullptr;
@ -1813,9 +1820,10 @@ bool BotGraph::loadGraphData () {
if (!modified.empty () && !modified.contains ("(none)")) { if (!modified.empty () && !modified.contains ("(none)")) {
m_info.modified.assign (exten.modified); m_info.modified.assign (exten.modified);
} }
vistab.load (); // load/initialize visibility
planner.init (); // initialize our little path planner planner.init (); // initialize our little path planner
practice.load (); // load bots practice practice.load (); // load bots practice
vistab.load (); // load/initialize visibility
populateNodes (); populateNodes ();
@ -1851,7 +1859,7 @@ bool BotGraph::canDownload () {
} }
bool BotGraph::saveGraphData () { bool BotGraph::saveGraphData () {
auto options = StorageOption::Graph | StorageOption::Exten; auto options = m_info.header.options | StorageOption::Graph | StorageOption::Exten;
String editorName {}; String editorName {};
if (!hasEditor () && !m_info.author.empty ()) { if (!hasEditor () && !m_info.author.empty ()) {
@ -1907,7 +1915,7 @@ void BotGraph::saveOldFormat () {
String editorName {}; String editorName {};
if (!hasEditor () && !m_info.author.empty ()) { if (!hasEditor () && !m_info.author.empty ()) {
editorName = m_info.author; editorName = m_info.author;
} }
else if (!game.isNullEntity (m_editor)) { else if (!game.isNullEntity (m_editor)) {
@ -2296,17 +2304,20 @@ void BotGraph::frame () {
if (path.radius > 0.0f) { if (path.radius > 0.0f) {
const float sqr = cr::sqrtf (cr::sqrf (path.radius) * 0.5f); const float sqr = cr::sqrtf (cr::sqrf (path.radius) * 0.5f);
game.drawLine (m_editor, origin + Vector (path.radius, 0.0f, 0.0f), origin + Vector (sqr, -sqr, 0.0f), 5, 0, radiusColor, 200, 0, 10); const Vector points[] = {
game.drawLine (m_editor, origin + Vector (sqr, -sqr, 0.0f), origin + Vector (0.0f, -path.radius, 0.0f), 5, 0, radiusColor, 200, 0, 10); { path.radius, 0.0f, 0.0f },
{ sqr, -sqr, 0.0f },
{ 0.0f, -path.radius, 0.0f },
{ -sqr, -sqr, 0.0f },
{ -path.radius, 0.0f, 0.0f },
{ -sqr, sqr, 0.0f },
{ 0.0f, path.radius, 0.0f },
{ sqr, sqr, 0.0f }
};
game.drawLine (m_editor, origin + Vector (0.0f, -path.radius, 0.0f), origin + Vector (-sqr, -sqr, 0.0f), 5, 0, radiusColor, 200, 0, 10); for (auto i = 0; i < kMaxNodeLinks; ++i) {
game.drawLine (m_editor, origin + Vector (-sqr, -sqr, 0.0f), origin + Vector (-path.radius, 0.0f, 0.0f), 5, 0, radiusColor, 200, 0, 10); game.drawLine (m_editor, origin + points[i], origin + points[(i + 1) % 8], 5, 0, radiusColor, 200, 0, 10);
}
game.drawLine (m_editor, origin + Vector (-path.radius, 0.0f, 0.0f), origin + Vector (-sqr, sqr, 0.0f), 5, 0, radiusColor, 200, 0, 10);
game.drawLine (m_editor, origin + Vector (-sqr, sqr, 0.0f), origin + Vector (0.0f, path.radius, 0.0f), 5, 0, radiusColor, 200, 0, 10);
game.drawLine (m_editor, origin + Vector (0.0f, path.radius, 0.0f), origin + Vector (sqr, sqr, 0.0f), 5, 0, radiusColor, 200, 0, 10);
game.drawLine (m_editor, origin + Vector (sqr, sqr, 0.0f), origin + Vector (path.radius, 0.0f, 0.0f), 5, 0, radiusColor, 200, 0, 10);
} }
else { else {
const float sqr = cr::sqrtf (32.0f); const float sqr = cr::sqrtf (32.0f);
@ -2391,7 +2402,7 @@ void BotGraph::frame () {
" Node %d of %d, Radius: %.1f, Light: %s\n" " Node %d of %d, Radius: %.1f, Light: %s\n"
" Flags: %s\n" " Flags: %s\n"
" Origin: (%.1f, %.1f, %.1f)\n", " Origin: (%.1f, %.1f, %.1f)\n",
type, node, m_paths.length () - 1, p.radius, type, node, m_paths.length () - 1, p.radius,
cr::fequal (p.light, kInvalidLightLevel) ? "Invalid" : strings.format ("%1.f", p.light), cr::fequal (p.light, kInvalidLightLevel) ? "Invalid" : strings.format ("%1.f", p.light),
flags, p.origin.x, p.origin.y, p.origin.z flags, p.origin.x, p.origin.y, p.origin.z
); );

View file

@ -130,6 +130,9 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
// precache everything // precache everything
game.precache (); game.precache ();
// notify about entity spawn
game.onSpawnEntity (ent);
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
RETURN_META_VALUE (MRES_IGNORED, 0); RETURN_META_VALUE (MRES_IGNORED, 0);
} }
@ -483,6 +486,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) {
} }
} }
} }
if (game.is (GameFlags::Metamod)) { if (game.is (GameFlags::Metamod)) {
RETURN_META (MRES_IGNORED); RETURN_META (MRES_IGNORED);
} }

View file

@ -186,9 +186,9 @@ BotCreateResult BotManager::create (StringRef name, int difficulty, int personal
// try to set proffered personality // try to set proffered personality
static HashMap <String, Personality> personalityMap { static HashMap <String, Personality> personalityMap {
{"normal", Personality::Normal }, { "normal", Personality::Normal },
{"careful", Personality::Careful }, { "careful", Personality::Careful },
{"rusher", Personality::Rusher }, { "rusher", Personality::Rusher },
}; };
// set personality if requested // set personality if requested
@ -332,13 +332,17 @@ void BotManager::addbot (StringRef name, StringRef difficulty, StringRef persona
// this function is same as the function above, but accept as parameters string instead of integers // this function is same as the function above, but accept as parameters string instead of integers
BotRequest request {}; BotRequest request {};
static StringRef any = "*"; constexpr StringRef ANY = "*";
request.name = (name.empty () || name == any) ? StringRef ("\0") : name; auto handleParam = [&ANY] (StringRef value) {
request.difficulty = (difficulty.empty () || difficulty == any) ? -1 : difficulty.as <int> (); return value.empty () || value == ANY ? -1 : value.as <int> ();
request.team = (team.empty () || team == any) ? -1 : team.as <int> (); };
request.skin = (skin.empty () || skin == any) ? -1 : skin.as <int> ();
request.personality = (personality.empty () || personality == any) ? -1 : personality.as <int> (); request.name = name.empty () || name == ANY ? StringRef ("\0") : name;
request.difficulty = handleParam (difficulty);
request.team = handleParam (team);
request.skin = handleParam (skin);
request.personality = handleParam (personality);
request.manual = manual; request.manual = manual;
addbot (request.name, request.difficulty, request.personality, request.team, request.skin, request.manual); addbot (request.name, request.difficulty, request.personality, request.team, request.skin, request.manual);
@ -617,7 +621,7 @@ void BotManager::serverFill (int selection, int personality, int difficulty, int
} }
const auto maxToAdd = maxClients - (getHumansCount () + getBotCount ()); const auto maxToAdd = maxClients - (getHumansCount () + getBotCount ());
constexpr char kTeams[6][12] = { "", {"Terrorists"}, {"CTs"}, "", "", {"Random"}, }; constexpr char kTeams[6][12] = { "", { "Terrorists" }, { "CTs" }, "", "", { "Random" }, };
auto toAdd = numToAdd == -1 ? maxToAdd : numToAdd; auto toAdd = numToAdd == -1 ? maxToAdd : numToAdd;
// limit manually added count as well // limit manually added count as well
@ -884,25 +888,25 @@ void BotManager::setWeaponMode (int selection) {
selection--; selection--;
constexpr int kStdMaps[7][kNumWeapons] = { constexpr int kStdMaps[7][kNumWeapons] = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Knife only
{-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only { -1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Pistols only
{-1, -1, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only { -1, -1, -1, -1, -1, -1, -1, 2, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Shotgun only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 1, 2, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1}, // Machine Guns only { -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 1, 2, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1 }, // Machine Guns only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1}, // Rifles only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1 }, // Rifles only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 2, 0, 1, -1, -1}, // Snipers only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, 2, 0, 1, -1, -1 }, // Snipers only
{-1, -1, -1, 2, 2, 0, 1, 2, 2, 2, 1, 2, 0, 2, 0, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1} // Standard { -1, -1, -1, 2, 2, 0, 1, 2, 2, 2, 1, 2, 0, 2, 0, 0, 1, 0, 1, 1, 2, 2, 0, 1, 2, 1 } // Standard
}; };
constexpr int kAsMaps[7][kNumWeapons] = { constexpr int kAsMaps[7][kNumWeapons] = {
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Knife only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Knife only
{-1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Pistols only { -1, -1, -1, 2, 2, 0, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Pistols only
{-1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // Shotgun only { -1, -1, -1, -1, -1, -1, -1, 1, 1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }, // Shotgun only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1}, // Machine Guns only { -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 1, 1, 0, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1 }, // Machine Guns only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1}, // Rifles only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 1, 0, 1, 1, -1, -1, -1, -1, -1, -1 }, // Rifles only
{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 1, -1, -1}, // Snipers only { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 1, -1, -1 }, // Snipers only
{-1, -1, -1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 2, 0, -1, 1, 0, 1, 1, 0, 0, -1, 1, 1, 1} // Standard { -1, -1, -1, 2, 2, 0, 1, 1, 1, 1, 1, 1, 0, 2, 0, -1, 1, 0, 1, 1, 0, 0, -1, 1, 1, 1 } // Standard
}; };
constexpr char kModes[7][12] = { {"Knife"}, {"Pistol"}, {"Shotgun"}, {"Machine Gun"}, {"Rifle"}, {"Sniper"}, {"Standard"} }; constexpr char kModes[7][12] = { { "Knife" }, { "Pistol" }, { "Shotgun" }, { "Machine Gun" }, { "Rifle" }, { "Sniper" }, { "Standard" } };
// get the raw weapons array // get the raw weapons array
auto tab = conf.getRawWeapons (); auto tab = conf.getRawWeapons ();
@ -1080,7 +1084,7 @@ void BotManager::updateBotDifficulties () {
// sets new difficulty for all bots // sets new difficulty for all bots
for (const auto &bot : m_bots) { for (const auto &bot : m_bots) {
bot->m_difficulty = difficulty; bot->setNewDifficulty (difficulty);
} }
m_lastDifficulty = difficulty; m_lastDifficulty = difficulty;
} }
@ -1089,7 +1093,7 @@ void BotManager::updateBotDifficulties () {
void BotManager::balanceBotDifficulties () { void BotManager::balanceBotDifficulties () {
// difficulty changing once per round (time) // difficulty changing once per round (time)
auto updateDifficulty = [] (Bot *bot, int32_t offset) { auto updateDifficulty = [] (Bot *bot, int32_t offset) {
bot->m_difficulty = cr::clamp (static_cast <Difficulty> (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert); bot->setNewDifficulty (cr::clamp (static_cast <Difficulty> (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert));
}; };
// with nightmare difficulty, there is no balance // with nightmare difficulty, there is no balance
@ -1192,7 +1196,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
m_isAlive = false; m_isAlive = false;
m_weaponBurstMode = BurstMode::Off; m_weaponBurstMode = BurstMode::Off;
m_difficulty = cr::clamp (static_cast <Difficulty> (difficulty), Difficulty::Noob, Difficulty::Expert); setNewDifficulty (cr::clamp (static_cast <Difficulty> (difficulty), Difficulty::Noob, Difficulty::Expert));
auto minDifficulty = cv_difficulty_min.as <int> (); auto minDifficulty = cv_difficulty_min.as <int> ();
auto maxDifficulty = cv_difficulty_max.as <int> (); auto maxDifficulty = cv_difficulty_max.as <int> ();
@ -1202,7 +1206,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) {
if (maxDifficulty > minDifficulty) { if (maxDifficulty > minDifficulty) {
cr::swap (maxDifficulty, minDifficulty); cr::swap (maxDifficulty, minDifficulty);
} }
m_difficulty = rg (minDifficulty, maxDifficulty); setNewDifficulty (rg (minDifficulty, maxDifficulty));
} }
m_pingBase = fakeping.randomBase (); m_pingBase = fakeping.randomBase ();
m_ping = fakeping.randomBase (); m_ping = fakeping.randomBase ();
@ -1515,14 +1519,13 @@ void Bot::newRound () {
m_isLeader = false; m_isLeader = false;
m_hasProgressBar = false; m_hasProgressBar = false;
m_canChooseAimDirection = true; m_canSetAimDirection = true;
m_preventFlashing = 0.0f; m_preventFlashing = 0.0f;
m_timeTeamOrder = 0.0f; m_timeTeamOrder = 0.0f;
m_askCheckTime = rg (30.0f, 90.0f); m_askCheckTime = rg (30.0f, 90.0f);
m_minSpeed = 260.0f; m_minSpeed = 260.0f;
m_prevSpeed = 0.0f; m_prevSpeed = 0.0f;
m_prevVelocity = 0.0f;
m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance); m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance);
m_prevTime = game.time (); m_prevTime = game.time ();
m_lookUpdateTime = game.time (); m_lookUpdateTime = game.time ();
@ -1722,9 +1725,6 @@ void Bot::newRound () {
m_enemyIgnoreTimer = 0.0f; m_enemyIgnoreTimer = 0.0f;
} }
// update refvec for blocked movement
updateRightRef ();
// and put buying into its message queue // and put buying into its message queue
pushMsgQueue (BotMsg::Buy); pushMsgQueue (BotMsg::Buy);
startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true); startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true);
@ -1828,6 +1828,16 @@ void Bot::markStale () {
pev->flags |= FL_DORMANT; pev->flags |= FL_DORMANT;
} }
void Bot::setNewDifficulty (int32_t newDifficulty) {
if (newDifficulty < Difficulty::Noob || newDifficulty > Difficulty::Expert) {
m_difficulty = Difficulty::Hard;
m_difficultyData = conf.getDifficultyTweaks (Difficulty::Hard);
}
m_difficulty = newDifficulty;
m_difficultyData = conf.getDifficultyTweaks (newDifficulty);
}
void Bot::updateTeamJoin () { void Bot::updateTeamJoin () {
// this function handles the selection of teams & class // this function handles the selection of teams & class

View file

@ -470,7 +470,7 @@ void Bot::resetCollision () {
void Bot::ignoreCollision () { void Bot::ignoreCollision () {
resetCollision (); resetCollision ();
m_lastCollTime = game.time () + 0.5f; m_lastCollTime = game.time () + 0.65f;
m_checkTerrain = false; m_checkTerrain = false;
} }
@ -602,11 +602,11 @@ void Bot::checkTerrain (const Vector &dirNormal) {
const auto tid = getCurrentTaskId (); const auto tid = getCurrentTaskId ();
// minimal speed for consider stuck // minimal speed for consider stuck
const float minimalSpeed = isDucking () ? kMinMovedDistance : kMinMovedDistance * 4; const float minimalSpeed = isDucking () ? 7.0f : 10.0f;
const auto randomProbeTime = rg (0.50f, 0.75f); const auto randomProbeTime = rg (0.50f, 0.75f);
// standing still, no need to check? // standing still, no need to check?
if ((cr::abs (m_moveSpeed) >= minimalSpeed || cr::abs (m_strafeSpeed) >= minimalSpeed) if ((m_moveSpeed >= minimalSpeed || m_strafeSpeed >= minimalSpeed)
&& m_lastCollTime < game.time () && m_lastCollTime < game.time ()
&& tid != Task::Attack && tid != Task::Attack
&& tid != Task::Camp) { && tid != Task::Camp) {
@ -617,7 +617,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
m_firstCollideTime = 0.0f; m_firstCollideTime = 0.0f;
} }
// didn't we move enough previously? // didn't we move enough previously?
else if (m_movedDistance < kMinMovedDistance && (m_prevSpeed > 20.0f || m_prevVelocity < m_moveSpeed / 2)) { else if (m_movedDistance < kMinMovedDistance && m_prevSpeed > 20.0f) {
m_prevTime = game.time (); // then consider being stuck m_prevTime = game.time (); // then consider being stuck
m_isStuck = true; m_isStuck = true;
@ -653,14 +653,16 @@ void Bot::checkTerrain (const Vector &dirNormal) {
resetCollision (); // reset collision memory if not being stuck for 0.5 secs resetCollision (); // reset collision memory if not being stuck for 0.5 secs
} }
else { else {
const auto state = m_collideMoves[m_collStateIndex];
// remember to keep pressing stuff if it was necessary ago // remember to keep pressing stuff if it was necessary ago
if (m_collideMoves[m_collStateIndex] == CollisionState::Duck && (isOnFloor () || isInWater ())) { if (state == CollisionState::Duck && (isOnFloor () || isInWater ())) {
pev->button |= IN_DUCK; pev->button |= IN_DUCK;
} }
else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeLeft) { else if (state == CollisionState::StrafeLeft) {
setStrafeSpeed (dirNormal, -pev->maxspeed); setStrafeSpeed (dirNormal, -pev->maxspeed);
} }
else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeRight) { else if (state == CollisionState::StrafeRight) {
setStrafeSpeed (dirNormal, pev->maxspeed); setStrafeSpeed (dirNormal, pev->maxspeed);
} }
} }
@ -683,6 +685,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
bits |= CollisionProbe::Duck; bits |= CollisionProbe::Duck;
} }
// collision check allowed if not flying through the air // collision check allowed if not flying through the air
if (isOnFloor () || isOnLadder () || isInWater ()) { if (isOnFloor () || isOnLadder () || isInWater ()) {
uint32_t state[kMaxCollideMoves * 2 + 1] {}; uint32_t state[kMaxCollideMoves * 2 + 1] {};
@ -696,7 +699,6 @@ void Bot::checkTerrain (const Vector &dirNormal) {
state[i++] = CollisionState::StrafeRight; state[i++] = CollisionState::StrafeRight;
state[i++] = CollisionState::Duck; state[i++] = CollisionState::Duck;
// now weight all possible states
if (bits & CollisionProbe::Jump) { if (bits & CollisionProbe::Jump) {
state[i] = 0; state[i] = 0;
@ -745,7 +747,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
} }
++i; ++i;
// now weight all possible states
if (bits & CollisionProbe::Strafe) { if (bits & CollisionProbe::Strafe) {
state[i] = 0; state[i] = 0;
state[i + 1] = 0; state[i + 1] = 0;
@ -813,6 +815,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
} }
} }
if (bits & CollisionProbe::Duck) { if (bits & CollisionProbe::Duck) {
state[i] = 0; state[i] = 0;
@ -901,7 +904,7 @@ void Bot::checkTerrain (const Vector &dirNormal) {
} }
void Bot::checkFall () { void Bot::checkFall () {
if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder)) { if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder) || isOnLadder ()) {
return; return;
} }
@ -945,26 +948,28 @@ void Bot::checkFall () {
const float nowDistanceSq = pev->origin.distanceSq (m_checkFallPoint[1]); const float nowDistanceSq = pev->origin.distanceSq (m_checkFallPoint[1]);
if (nowDistanceSq > baseDistanceSq if (nowDistanceSq > baseDistanceSq
&& (nowDistanceSq > baseDistanceSq * 1.2f || nowDistanceSq > baseDistanceSq + 200.0f) && (nowDistanceSq > baseDistanceSq * 1.8f || nowDistanceSq > baseDistanceSq + 260.0f)
&& baseDistanceSq >= cr::sqrf (80.0f) && nowDistanceSq >= cr::sqrf (100.0f)) { && baseDistanceSq >= cr::sqrf (124.0f) && nowDistanceSq >= cr::sqrf (146.0f)) {
fixFall = true; fixFall = true;
} }
else if (m_checkFallPoint[1].z > pev->origin.z + 128.0f else if (m_checkFallPoint[1].z > pev->origin.z + 138.0f
&& m_checkFallPoint[0].z > pev->origin.z + 128.0f) { || m_checkFallPoint[0].z > pev->origin.z + 138.0f) {
fixFall = true; fixFall = true;
} }
else if (m_currentNodeIndex != kInvalidNodeIndex else if (m_currentNodeIndex != kInvalidNodeIndex
&& nowDistanceSq > cr::sqrf (16.0f) && nowDistanceSq > cr::sqrf (32.0f)
&& m_checkFallPoint[1].z > pev->origin.z + 62.0f) { && m_checkFallPoint[1].z > pev->origin.z + 72.0f) {
fixFall = true; fixFall = true;
} }
if (fixFall) { if (fixFall) {
m_currentNodeIndex = kInvalidNodeIndex; if (graph.exists (m_currentNodeIndex) && !isReachableNode (m_currentNodeIndex)) {
findValidNode (); m_currentNodeIndex = kInvalidNodeIndex;
findValidNode ();
m_fixFallTimer.start (1.0f); m_fixFallTimer.start (1.0f);
}
} }
} }
@ -1000,22 +1005,22 @@ void Bot::moveToGoal () {
// press jump button if we need to leave the ladder // press jump button if we need to leave the ladder
if (!(m_pathFlags & NodeFlag::Ladder) if (!(m_pathFlags & NodeFlag::Ladder)
&& isPreviousLadder () && isPreviousLadder ()
&& isOnFloor ()
&& isOnLadder () && isOnLadder ()
&& m_moveSpeed > 50.0f && m_moveSpeed > 50.0f
&& pev->velocity.lengthSq () < 50.0f) { && pev->velocity.lengthSq () < 50.0f
&& m_ladderDir == LadderDir::Down) {
pev->button |= IN_JUMP; pev->button |= IN_JUMP;
m_jumpTime = game.time () + 1.0f; m_jumpTime = game.time () + 1.0f;
} }
#if 0
const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval); const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval);
if (distanceSq2d < cr::sqrf (m_moveSpeed) * m_frameInterval && getTask ()->data != kInvalidNodeIndex) { if (distanceSq2d < cr::sqrf (m_moveSpeed * m_frameInterval) && getTask ()->data != kInvalidNodeIndex) {
m_moveSpeed = distanceSq2d; m_moveSpeed = distanceSq2d * 2.0f;
}
if (m_moveSpeed > pev->maxspeed) {
m_moveSpeed = pev->maxspeed;
} }
m_moveSpeed = cr::clamp (m_moveSpeed, 4.0f, pev->maxspeed);
#endif
m_lastUsedNodesTime = game.time (); m_lastUsedNodesTime = game.time ();
// special movement for swimming here // special movement for swimming here
@ -1135,10 +1140,11 @@ bool Bot::updateNavigation () {
m_jumpFinished = true; m_jumpFinished = true;
m_checkTerrain = false; m_checkTerrain = false;
m_desiredVelocity.clear (); m_desiredVelocity.clear ();
m_currentTravelFlags &= ~PathFlag::Jump;
// cool down a little if next path after current will be jump // cool down a little if next path after current will be jump
if (m_jumpSequence) { if (m_jumpSequence) {
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + rg (0.75f, 1.2f) + m_frameInterval, false); startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + rg (0.75f, 1.25f) + m_frameInterval, false);
m_jumpSequence = false; m_jumpSequence = false;
} }
} }
@ -1149,19 +1155,13 @@ bool Bot::updateNavigation () {
} }
if (m_pathFlags & NodeFlag::Ladder) { if (m_pathFlags & NodeFlag::Ladder) {
constexpr auto kLadderOffset = Vector { 0.0f, 0.0f, 36.0f };
const auto prevNodeIndex = m_previousNodes[0]; const auto prevNodeIndex = m_previousNodes[0];
const float ladderDistance = pev->origin.distance (m_pathOrigin); const auto ladderDistanceSq = pev->origin.distanceSq (m_pathOrigin);
// do a precise movement when very near // do a precise movement when very near
if (graph.exists (prevNodeIndex) && !(graph[prevNodeIndex].flags & NodeFlag::Ladder) && ladderDistance < 64.0f) { if (graph.exists (prevNodeIndex)
if (m_pathOrigin.z >= pev->origin.z + 16.0f) { && !(graph[prevNodeIndex].flags & NodeFlag::Ladder)
m_pathOrigin = m_path->origin + kLadderOffset; && ladderDistanceSq < cr::sqrf (64.0f)) {
}
else if (m_pathOrigin.z < pev->origin.z - 16.0f) {
m_pathOrigin = m_path->origin - kLadderOffset;
}
if (!isDucking ()) { if (!isDucking ()) {
m_moveSpeed = pev->maxspeed * 0.4f; m_moveSpeed = pev->maxspeed * 0.4f;
@ -1171,20 +1171,14 @@ bool Bot::updateNavigation () {
if (!isOnLadder ()) { if (!isOnLadder ()) {
pev->button &= ~IN_DUCK; pev->button &= ~IN_DUCK;
} }
m_approachingLadderTimer.start (m_frameInterval * 6.0f); m_approachingLadderTimer.start (2.0f * m_frameInterval);
} }
if (!isOnLadder () && isOnFloor () && !isDucking ()) { if (!isOnLadder () && isOnFloor () && !isDucking ()) {
if (!isPreviousLadder ()) { if (!isPreviousLadder ()) {
m_moveSpeed = ladderDistance; m_moveSpeed = cr::sqrtf (ladderDistanceSq);
}
if (m_moveSpeed < 150.0f) {
m_moveSpeed = 150.0f;
}
else if (m_moveSpeed > pev->maxspeed) {
m_moveSpeed = pev->maxspeed;
} }
m_moveSpeed = cr::clamp (m_moveSpeed, 160.0f, pev->maxspeed);
} }
// special detection if someone is using the ladder (to prevent to have bots-towers on ladders) // special detection if someone is using the ladder (to prevent to have bots-towers on ladders)
@ -1238,7 +1232,7 @@ bool Bot::updateNavigation () {
ignoreCollision (); // don't consider being stuck ignoreCollision (); // don't consider being stuck
// also 'use' the door randomly // also 'use' the door randomly
if (rg.chance (50)) { if (m_buttonPushTime < game.time () && rg.chance (50)) {
// do not use door directly under xash, or we will get failed assert in gamedll code // do not use door directly under xash, or we will get failed assert in gamedll code
if (game.is (GameFlags::Xash3D)) { if (game.is (GameFlags::Xash3D)) {
pev->button |= IN_USE; pev->button |= IN_USE;
@ -1252,7 +1246,7 @@ bool Bot::updateNavigation () {
// make sure we are always facing the door when going through it // make sure we are always facing the door when going through it
m_aimFlags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); m_aimFlags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath);
m_canChooseAimDirection = false; m_canSetAimDirection = false;
// delay task // delay task
if (m_buttonPushTime < game.time ()) { if (m_buttonPushTime < game.time ()) {
@ -1322,26 +1316,35 @@ bool Bot::updateNavigation () {
} }
} }
float desiredDistanceSq = cr::sqrf (32.0f); float desiredDistanceSq = cr::sqrf (48.0f);
const float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin);
// initialize the radius for a special node type, where the node is considered to be reached // initialize the radius for a special node type, where the node is considered to be reached
if (m_pathFlags & NodeFlag::Lift) { if (m_pathFlags & NodeFlag::Lift) {
desiredDistanceSq = cr::sqrf (50.0f); desiredDistanceSq = cr::sqrf (50.0f);
} }
else if (isDucking () || (m_pathFlags & NodeFlag::Goal)) { else if (isDucking () || (m_pathFlags & NodeFlag::Goal)) {
desiredDistanceSq = cr::sqrf (9.0f); desiredDistanceSq = cr::sqrf (25.0f);
// on cs_ maps goals are usually hostages, so increase reachability distance for them, they (hostages) picked anyway // on cs_ maps goals are usually hostages, so increase reachability distance for them, they (hostages) picked anyway
if (game.mapIs (MapFlags::HostageRescue) && (m_pathFlags & NodeFlag::Goal)) { if (game.mapIs (MapFlags::HostageRescue)
&& (m_pathFlags & NodeFlag::Goal)) {
desiredDistanceSq = cr::sqrf (96.0f); desiredDistanceSq = cr::sqrf (96.0f);
} }
} }
else if (m_pathFlags & NodeFlag::Ladder) { else if (isOnLadder () || (m_pathFlags & NodeFlag::Ladder)) {
desiredDistanceSq = cr::sqrf (16.0f); desiredDistanceSq = cr::sqrf (15.0f);
} }
else if (m_currentTravelFlags & PathFlag::Jump) { else if (m_currentTravelFlags & PathFlag::Jump) {
desiredDistanceSq = 0.0f; desiredDistanceSq = 0.0f;
if (pev->velocity.z > 16.0f) {
desiredDistanceSq = cr::sqrf (8.0f);
}
}
else if (m_pathFlags & NodeFlag::Crouch) {
desiredDistanceSq = cr::sqrf (6.0f);
} }
else if (m_path->number == cv_debug_goal.as <int> ()) { else if (m_path->number == cv_debug_goal.as <int> ()) {
desiredDistanceSq = 0.0f; desiredDistanceSq = 0.0f;
@ -1368,14 +1371,14 @@ bool Bot::updateNavigation () {
// if just recalculated path, assume reached current node // if just recalculated path, assume reached current node
if (!m_repathTimer.elapsed () && !pathHasFlags) { if (!m_repathTimer.elapsed () && !pathHasFlags) {
desiredDistanceSq = cr::sqrf (72.0f); desiredDistanceSq = cr::sqrf (48.0f);
} }
// needs precise placement - check if we get past the point // needs precise placement - check if we get past the point
if (desiredDistanceSq < cr::sqrf (16.0f) && nodeDistanceSq < cr::sqrf (30.0f)) { if (desiredDistanceSq < cr::sqrf (16.0f) && nodeDistanceSq < cr::sqrf (30.0f)) {
const auto predictRangeSq = m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval); const auto predictRangeSq = m_pathOrigin.distanceSq (pev->origin + pev->velocity * m_frameInterval);
if (predictRangeSq >= nodeDistanceSq || predictRangeSq <= desiredDistanceSq) { if (predictRangeSq > nodeDistanceSq || predictRangeSq <= desiredDistanceSq) {
desiredDistanceSq = nodeDistanceSq + 1.0f; desiredDistanceSq = nodeDistanceSq + 1.0f;
} }
} }
@ -1410,6 +1413,9 @@ bool Bot::updateNavigation () {
// update the practice for team // update the practice for team
practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue);
// ignore collision
ignoreCollision ();
} }
return true; return true;
} }
@ -1870,7 +1876,7 @@ bool Bot::findNextBestNodeEx (const IntArray &data, bool handleFails) {
} }
// in case low node density do not skip previous ones, in case of fail reduce max nodes to skip // in case low node density do not skip previous ones, in case of fail reduce max nodes to skip
const auto &numToSkip = graph.length () < 512 ? 0 : (handleFails ? 1 : rg (1, 4)); const auto &numToSkip = graph.length () < 512 || m_isStuck ? 0 : (handleFails ? 1 : rg (1, 4));
for (const auto &i : data) { for (const auto &i : data) {
const auto &path = graph[i]; const auto &path = graph[i];
@ -2460,7 +2466,7 @@ bool Bot::selectBestNextNode () {
const auto currentNodeIndex = m_pathWalk.first (); const auto currentNodeIndex = m_pathWalk.first ();
const auto prevNodeIndex = m_currentNodeIndex; const auto prevNodeIndex = m_currentNodeIndex;
if (!isOccupiedNode (currentNodeIndex)) { if (isOnLadder () || !isOccupiedNode (currentNodeIndex)) {
return false; return false;
} }
@ -2590,10 +2596,11 @@ bool Bot::advanceMovement () {
bool isCurrentJump = false; bool isCurrentJump = false;
// find out about connection flags // find out about connection flags
if (destIndex != kInvalidNodeIndex && m_currentNodeIndex != kInvalidNodeIndex) { if (graph.exists (destIndex) && m_path) {
for (const auto &link : m_path->links) { for (const auto &link : m_path->links) {
if (link.index == destIndex) { if (link.index == destIndex) {
m_currentTravelFlags = link.flags; m_currentTravelFlags = link.flags;
m_desiredVelocity = link.velocity; m_desiredVelocity = link.velocity;
m_jumpFinished = false; m_jumpFinished = false;
@ -2603,7 +2610,7 @@ bool Bot::advanceMovement () {
} }
// if graph is analyzed try our special jumps // if graph is analyzed try our special jumps
if (graph.isAnalyzed () || analyzer.isAnalyzed ()) { if ((graph.isAnalyzed () || analyzer.isAnalyzed ()) && !(m_path->flags & NodeFlag::Ladder)) {
for (const auto &link : m_path->links) { for (const auto &link : m_path->links) {
if (link.index == destIndex) { if (link.index == destIndex) {
const float diff = cr::abs (m_path->origin.z - graph[destIndex].origin.z); const float diff = cr::abs (m_path->origin.z - graph[destIndex].origin.z);
@ -2666,6 +2673,7 @@ bool Bot::advanceMovement () {
// get ladder nodes used by other (first moving) bots // get ladder nodes used by other (first moving) bots
for (const auto &other : bots) { for (const auto &other : bots) {
// if another bot uses this ladder, wait 3 secs // if another bot uses this ladder, wait 3 secs
if (other.get () != this && other->m_isAlive && other->m_currentNodeIndex == destIndex && other->isOnLadder ()) { if (other.get () != this && other->m_isAlive && other->m_currentNodeIndex == destIndex && other->isOnLadder ()) {
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 3.0f, false); startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 3.0f, false);
@ -2725,21 +2733,30 @@ void Bot::setPathOrigin () {
else if (radius > 0.0f) { else if (radius > 0.0f) {
setNonZeroPathOrigin (); setNonZeroPathOrigin ();
} }
if (isOnLadder ()) { if (isOnLadder ()) {
TraceResult tr {}; edict_t *ladder = nullptr;
game.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_pathOrigin, TraceIgnore::Everything, ent (), &tr);
if (tr.flFraction < 1.0f) { game.searchEntities (m_pathOrigin, 96.0f, [&] (edict_t *e) {
m_pathOrigin = m_pathOrigin + (pev->origin - m_pathOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f); if (e->v.classname.str () == "func_ladder") {
ladder = e;
return EntitySearchResult::Break;
}
return EntitySearchResult::Continue;
});
if (!game.isNullEntity (ladder)) {
TraceResult tr {};
game.testLine ({ pev->origin.x, pev->origin.y, ladder->v.absmin.z }, m_pathOrigin, TraceIgnore::Monsters, ent (), &tr);
if (tr.flFraction < 1.0f) {
m_pathOrigin = graph[m_currentNodeIndex].origin + (pev->origin - m_pathOrigin) * 0.5 + Vector (0.0f, 0.0f, 32.0f);
}
m_ladderDir = m_pathOrigin.z < pev->origin.z ? LadderDir::Down : LadderDir::Up;
} }
} }
} }
void Bot::updateRightRef () {
m_rightRef = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // convert current view angle to vectors for traceline math...
}
bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) { bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
// checks if bot is blocked in his movement direction (excluding doors) // checks if bot is blocked in his movement direction (excluding doors)
@ -2774,12 +2791,12 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
constexpr auto kVec00N16 = Vector (0.0f, 0.0f, -16.0f); constexpr auto kVec00N16 = Vector (0.0f, 0.0f, -16.0f);
// right referential vector // right referential vector
updateRightRef (); auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right ();
// bot's head is clear, check at shoulder level... // bot's head is clear, check at shoulder level...
// trace from the bot's shoulder left diagonal forward to the right shoulder... // trace from the bot's shoulder left diagonal forward to the right shoulder...
src = getEyesPos () + kVec00N16 - m_rightRef * -16.0f; src = getEyesPos () + kVec00N16 - right * -16.0f;
forward = getEyesPos () + kVec00N16 + m_rightRef * 16.0f + normal * 24.0f; forward = getEyesPos () + kVec00N16 + right * 16.0f + normal * 24.0f;
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2790,8 +2807,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
// bot's head is clear, check at shoulder level... // bot's head is clear, check at shoulder level...
// trace from the bot's shoulder right diagonal forward to the left shoulder... // trace from the bot's shoulder right diagonal forward to the left shoulder...
src = getEyesPos () + kVec00N16 + m_rightRef * 16.0f; src = getEyesPos () + kVec00N16 + right * 16.0f;
forward = getEyesPos () + kVec00N16 - m_rightRef * -16.0f + normal * 24.0f; forward = getEyesPos () + kVec00N16 - right * -16.0f + normal * 24.0f;
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2826,8 +2843,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
constexpr auto kVec00N24 = Vector (0.0f, 0.0f, -24.0f); constexpr auto kVec00N24 = Vector (0.0f, 0.0f, -24.0f);
// trace from the left waist to the right forward waist pos // trace from the left waist to the right forward waist pos
src = pev->origin + kVec00N17 - m_rightRef * -16.0f; src = pev->origin + kVec00N17 - right * -16.0f;
forward = pev->origin + kVec00N17 + m_rightRef * 16.0f + normal * 24.0f; forward = pev->origin + kVec00N17 + right * 16.0f + normal * 24.0f;
// trace from the bot's waist straight forward... // trace from the bot's waist straight forward...
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2838,8 +2855,8 @@ bool Bot::isBlockedForward (const Vector &normal, TraceResult *tr) {
} }
// trace from the left waist to the right forward waist pos // trace from the left waist to the right forward waist pos
src = pev->origin + kVec00N24 + m_rightRef * 16.0f; src = pev->origin + kVec00N24 + right * 16.0f;
forward = pev->origin + kVec00N24 - m_rightRef * -16.0f + normal * 24.0f; forward = pev->origin + kVec00N24 - right * -16.0f + normal * 24.0f;
game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr);
@ -2918,7 +2935,7 @@ bool Bot::canJumpUp (const Vector &normal) {
if (!isOnFloor () && (isOnLadder () || !isInWater ())) { if (!isOnFloor () && (isOnLadder () || !isInWater ())) {
return false; return false;
} }
updateRightRef (); auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // convert current view angle to vectors for traceline math...
// check for normal jump height first... // check for normal jump height first...
auto src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 45.0f); auto src = pev->origin + Vector (0.0f, 0.0f, -36.0f + 45.0f);
@ -2928,7 +2945,7 @@ bool Bot::canJumpUp (const Vector &normal) {
game.testLine (src, dest, TraceIgnore::Monsters, ent (), &tr); game.testLine (src, dest, TraceIgnore::Monsters, ent (), &tr);
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return doneCanJumpUp (normal, m_rightRef); return doneCanJumpUp (normal, right);
} }
else { else {
// now trace from jump height upward to check for obstructions... // now trace from jump height upward to check for obstructions...
@ -2943,7 +2960,7 @@ bool Bot::canJumpUp (const Vector &normal) {
} }
// now check same height to one side of the bot... // now check same height to one side of the bot...
src = pev->origin + m_rightRef * 16.0f + Vector (0.0f, 0.0f, -36.0f + 45.0f); src = pev->origin + right * 16.0f + Vector (0.0f, 0.0f, -36.0f + 45.0f);
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at maximum jump height... // trace a line forward at maximum jump height...
@ -2951,7 +2968,7 @@ bool Bot::canJumpUp (const Vector &normal) {
// if trace hit something, return false // if trace hit something, return false
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return doneCanJumpUp (normal, m_rightRef); return doneCanJumpUp (normal, right);
} }
// now trace from jump height upward to check for obstructions... // now trace from jump height upward to check for obstructions...
@ -2966,7 +2983,7 @@ bool Bot::canJumpUp (const Vector &normal) {
} }
// now check same height on the other side of the bot... // now check same height on the other side of the bot...
src = pev->origin + (-m_rightRef * 16.0f) + Vector (0.0f, 0.0f, -36.0f + 45.0f); src = pev->origin + (-right * 16.0f) + Vector (0.0f, 0.0f, -36.0f + 45.0f);
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at maximum jump height... // trace a line forward at maximum jump height...
@ -2974,7 +2991,7 @@ bool Bot::canJumpUp (const Vector &normal) {
// if trace hit something, return false // if trace hit something, return false
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return doneCanJumpUp (normal, m_rightRef); return doneCanJumpUp (normal, right);
} }
// now trace from jump height upward to check for obstructions... // now trace from jump height upward to check for obstructions...
@ -3082,10 +3099,10 @@ bool Bot::canDuckUnder (const Vector &normal) {
if (tr.flFraction < 1.0f) { if (tr.flFraction < 1.0f) {
return false; return false;
} }
updateRightRef (); auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right ();
// now check same height to one side of the bot... // now check same height to one side of the bot...
src = baseHeight + m_rightRef * 16.0f; src = baseHeight + right * 16.0f;
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at duck height... // trace a line forward at duck height...
@ -3097,7 +3114,7 @@ bool Bot::canDuckUnder (const Vector &normal) {
} }
// now check same height on the other side of the bot... // now check same height on the other side of the bot...
src = baseHeight + (-m_rightRef * 16.0f); src = baseHeight + (-right * 16.0f);
dest = src + normal * 32.0f; dest = src + normal * 32.0f;
// trace a line forward at duck height... // trace a line forward at duck height...
@ -3114,11 +3131,11 @@ bool Bot::isBlockedLeft () {
if (m_moveSpeed < 0.0f) { if (m_moveSpeed < 0.0f) {
direction = -48.0f; direction = -48.0f;
} }
Vector right {}, forward {}; Vector left {}, forward {};
pev->angles.angleVectors (&forward, &right, nullptr); pev->angles.angleVectors (&forward, &left, nullptr);
// do a trace to the left... // do a trace to the left...
game.testLine (pev->origin, pev->origin - forward * direction - right * 48.0f, TraceIgnore::Monsters, ent (), &tr); game.testLine (pev->origin, pev->origin + forward * direction - left * -48.0f, TraceIgnore::Monsters, ent (), &tr);
// check if the trace hit something... // check if the trace hit something...
if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !game.isDoorEntity (tr.pHit)) { if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !game.isDoorEntity (tr.pHit)) {
@ -3131,8 +3148,8 @@ bool Bot::isBlockedRight () {
TraceResult tr {}; TraceResult tr {};
float direction = 48.0f; float direction = 48.0f;
if (m_moveSpeed > 0.0f) { if (m_moveSpeed < 0.0f) {
direction = 48.0f; direction = -48.0f;
} }
Vector right {}, forward {}; Vector right {}, forward {};
pev->angles.angleVectors (&forward, &right, nullptr); pev->angles.angleVectors (&forward, &right, nullptr);

View file

@ -184,7 +184,7 @@ bool AStarAlgo::cantSkipNode (const int a, const int b, bool skipVisCheck) {
} }
if (!skipVisCheck) { if (!skipVisCheck) {
const bool notVisible = !vistab.visible (ag.number, bg.number) || !vistab.visible (bg.number, ag.number); const bool notVisible = !vistab.visibleBothSides (ag.number, bg.number);
if (notVisible) { if (notVisible) {
return true; return true;
@ -372,7 +372,7 @@ void FloydWarshallAlgo::syncRebuild () {
for (int k = 0; k < m_length; ++k) { for (int k = 0; k < m_length; ++k) {
for (int i = 0; i < m_length; ++i) { for (int i = 0; i < m_length; ++i) {
for (int j = 0; j < m_length; ++j) { for (int j = 0; j < m_length; ++j) {
int distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist; const auto distance = (matrix + (i * m_length) + k)->dist + (matrix + (k * m_length) + j)->dist;
if (distance < (matrix + (i * m_length) + j)->dist) { if (distance < (matrix + (i * m_length) + j)->dist) {
*(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance }; *(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance };

View file

@ -17,7 +17,7 @@ template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *
// graphs can be downloaded... // graphs can be downloaded...
const bool isGraph = !!(type.option & StorageOption::Graph); const bool isGraph = !!(type.option & StorageOption::Graph);
const bool isDebug = cv_debug; const bool isDebug = cv_debug || game.isDeveloperMode ();
MemFile file (filename); // open the file MemFile file (filename); // open the file
data.clear (); data.clear ();
@ -85,11 +85,7 @@ template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *
if (tryReload ()) { if (tryReload ()) {
return true; return true;
} }
return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename);
if (game.isDeveloperMode ()) {
return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename);
}
return false;
} }
// erase the current graph just in case // erase the current graph just in case
@ -178,7 +174,7 @@ template <typename U> bool BotStorage::load (SmallArray <U> &data, ExtenHeader *
if (isGraph) { if (isGraph) {
resetRetries (); resetRetries ();
ExtenHeader extenHeader; ExtenHeader extenHeader {};
strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author)); strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author));
if (extenSize <= actuallyRead) { if (extenSize <= actuallyRead) {

View file

@ -11,6 +11,9 @@ ConVar cv_display_welcome_text ("display_welcome_text", "1", "Enables or disable
ConVar cv_enable_query_hook ("enable_query_hook", "0", "Enables or disables fake server query responses, which show bots as real players in the server browser."); ConVar cv_enable_query_hook ("enable_query_hook", "0", "Enables or disables fake server query responses, which show bots as real players in the server browser.");
ConVar cv_enable_fake_steamids ("enable_fake_steamids", "0", "Allows or disallows bots to return a fake Steam ID."); ConVar cv_enable_fake_steamids ("enable_fake_steamids", "0", "Allows or disallows bots to return a fake Steam ID.");
ConVar cv_smoke_grenade_checks ("smoke_grenade_checks", "2", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f);
ConVar cv_smoke_greande_checks_radius ("greande_checks_radius", "220", "Radius to check for smoke clouds around a detonated grenade.", true, 32.0f, 320.0f);
BotSupport::BotSupport () { BotSupport::BotSupport () {
m_needToSendWelcome = false; m_needToSendWelcome = false;
m_welcomeReceiveTime = 0.0f; m_welcomeReceiveTime = 0.0f;
@ -369,7 +372,6 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) {
if (!gameState.hasActiveGrenades ()) { if (!gameState.hasActiveGrenades ()) {
return false; return false;
} }
constexpr auto kSmokeGrenadeRadius = 115.0f;
// distance along line of sight covered by smoke // distance along line of sight covered by smoke
float totalSmokedLength = 0.0f; float totalSmokedLength = 0.0f;
@ -397,7 +399,7 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) {
continue; continue;
} }
const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius); const float smokeRadiusSq = cr::sqrf (cv_smoke_greande_checks_radius.as <float> ());
const Vector &smokeOrigin = game.getEntityOrigin (pent); const Vector &smokeOrigin = game.getEntityOrigin (pent);
Vector toGrenade = smokeOrigin - from; Vector toGrenade = smokeOrigin - from;
@ -474,7 +476,7 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) {
} }
// define how much smoke a bot can see thru // define how much smoke a bot can see thru
const float maxSmokedLength = 0.7f * kSmokeGrenadeRadius; const float maxSmokedLength = 0.7f * cv_smoke_greande_checks_radius.as <float> ();
// return true if the total length of smoke-covered line-of-sight is too much // return true if the total length of smoke-covered line-of-sight is too much
return totalSmokedLength > maxSmokedLength; return totalSmokedLength > maxSmokedLength;

View file

@ -21,7 +21,7 @@ void Bot::normal_ () {
const int debugGoal = cv_debug_goal.as <int> (); const int debugGoal = cv_debug_goal.as <int> ();
// user forced a node as a goal? // user forced a node as a goal?
if (debugGoal != kInvalidNodeIndex) { if (graph.exists (debugGoal)) {
if (getTask ()->data != debugGoal) { if (getTask ()->data != debugGoal) {
clearSearchNodes (); clearSearchNodes ();
@ -105,7 +105,7 @@ void Bot::normal_ () {
// spray logo sometimes if allowed to do so // spray logo sometimes if allowed to do so
if (!(m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy)) if (!(m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy))
&& m_seeEnemyTime + 5.0f < game.time () && m_seeEnemyTime + 5.0f < game.time ()
&& m_reloadState == Reload::None && m_reloadState == Reload::None
&& m_timeLogoSpray < game.time () && m_timeLogoSpray < game.time ()
&& cv_spraypaints && cv_spraypaints
&& pev->groundentity == game.getStartEntity () && pev->groundentity == game.getStartEntity ()
@ -665,7 +665,7 @@ void Bot::camp_ () {
predictNode = findAimingNode (m_lastEnemyOrigin, pathLength); predictNode = findAimingNode (m_lastEnemyOrigin, pathLength);
if (isNodeValidForPredict (predictNode) && pathLength > 1 if (isNodeValidForPredict (predictNode) && pathLength > 1
&& vistab.visible ( predictNode, m_currentNodeIndex)) { && vistab.visible (predictNode, m_currentNodeIndex)) {
m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; m_lookAtSafe = graph[predictNode].origin + pev->view_ofs;
} }
@ -1477,7 +1477,7 @@ void Bot::shootBreakable_ () {
} }
else { else {
TraceResult tr {}; TraceResult tr {};
game.testLine (pev->origin, m_breakableOrigin, TraceIgnore::Monsters , ent (), &tr); game.testLine (pev->origin, m_breakableOrigin, TraceIgnore::Monsters, ent (), &tr);
if (tr.pHit != m_breakableEntity && !cr::fequal (tr.flFraction, 1.0f)) { if (tr.pHit != m_breakableEntity && !cr::fequal (tr.flFraction, 1.0f)) {
m_ignoredBreakable.push (tr.pHit); m_ignoredBreakable.push (tr.pHit);
@ -1507,7 +1507,7 @@ void Bot::shootBreakable_ () {
m_shootTime = game.time (); m_shootTime = game.time ();
// enforce shooting // enforce shooting
if (!usesKnife () && !m_isReloading && !(pev->button & IN_RELOAD) && getAmmoInClip () > 0) { if (!usesKnife () && !m_isReloading && !(pev->button & IN_RELOAD) && getAmmoInClip () > 0) {
if (!(m_oldButtons & IN_ATTACK)) { if (!(m_oldButtons & IN_ATTACK)) {
pev->button |= IN_ATTACK; pev->button |= IN_ATTACK;
} }

View file

@ -230,7 +230,11 @@ void Bot::updateLookAngles () {
float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y);
// prevent reverse facing angles when navigating normally // prevent reverse facing angles when navigating normally
if (m_moveToGoal && !importantAimFlags && !m_pathOrigin.empty ()) { if (m_moveToGoal
&& !importantAimFlags
&& !m_pathOrigin.empty ()
&& !isOnLadder ()) {
const float forward = (m_lookAt - pev->origin).yaw (); const float forward = (m_lookAt - pev->origin).yaw ();
if (!cr::fzero (forward)) { if (!cr::fzero (forward)) {
@ -436,7 +440,7 @@ void Bot::setAimDirection () {
|| (m_currentTravelFlags & PathFlag::Jump)) { || (m_currentTravelFlags & PathFlag::Jump)) {
flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath);
m_canChooseAimDirection = false; m_canSetAimDirection = false;
} }
// don't switch view right away after loosing focus with current enemy // don't switch view right away after loosing focus with current enemy
@ -532,7 +536,8 @@ void Bot::setAimDirection () {
} }
const float distToPredictNodeSq = graph[predictNode].origin.distanceSq (pev->origin); const float distToPredictNodeSq = graph[predictNode].origin.distanceSq (pev->origin);
if (distToPredictNodeSq >= cr::sqrf (2048.0f)) { if (distToPredictNodeSq >= cr::sqrf (2048.0f) ||
distToPredictNodeSq <= cr::sqrf (256.0f)) {
return false; return false;
} }
@ -542,7 +547,8 @@ void Bot::setAimDirection () {
return false; return false;
} }
return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as <int> (); return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as <int> ()
&& numEnemiesNear (graph[predictNode].origin, 1024.0f) > 0;
}; };
if (changePredictedEnemy) { if (changePredictedEnemy) {
@ -573,47 +579,21 @@ void Bot::setAimDirection () {
const auto &destOrigin = m_destOrigin + pev->view_ofs; const auto &destOrigin = m_destOrigin + pev->view_ofs;
m_lookAt = destOrigin; m_lookAt = destOrigin;
const bool horizontalMovement = (m_pathFlags & NodeFlag::Ladder) || isOnLadder (); const bool verticalMove = (m_pathFlags & NodeFlag::Ladder) || isOnLadder () || isPreviousLadder () || pev->velocity.z > 16.0f;
if (!horizontalMovement && m_moveToGoal && m_seeEnemyTime + 4.0f < game.time ()
&& !m_isStuck && !(pev->button & IN_DUCK)
&& m_currentNodeIndex != kInvalidNodeIndex
&& !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch))
&& m_pathWalk.hasNext () && !isOnLadder ()
&& pev->origin.distanceSq (destOrigin) < cr::sqrf (512.0f)) {
const auto nextPathIndex = m_pathWalk.next ();
const auto nextPathX2 = m_pathWalk.nextX2 ();
if (vistab.visible (m_currentNodeIndex, nextPathX2)) {
const auto &gn = graph[nextPathX2];
m_lookAt = gn.origin + pev->view_ofs;
}
else if (vistab.visible (m_currentNodeIndex, nextPathIndex)) {
const auto &gn = graph[nextPathIndex];
m_lookAt = gn.origin + pev->view_ofs;
}
else {
m_lookAt = pev->origin + pev->view_ofs + pev->v_angle.forward () * 300.0f;
}
}
else {
m_lookAt = destOrigin;
}
if (m_numEnemiesLeft > 0 if (m_numEnemiesLeft > 0
&& m_canChooseAimDirection && m_canSetAimDirection
&& m_seeEnemyTime + 4.0f < game.time () && m_seeEnemyTime + 4.0f < game.time ()
&& m_currentNodeIndex != kInvalidNodeIndex && graph.exists (m_currentNodeIndex)
&& !horizontalMovement) { && !(m_aimFlags & AimFlags::PredictPath)) {
const auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); const auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
if (graph.exists (dangerIndex) if (graph.exists (dangerIndex)
&& vistab.visible (m_currentNodeIndex, dangerIndex) && vistab.visibleBothSides (m_currentNodeIndex, dangerIndex)
&& !(graph[dangerIndex].flags & NodeFlag::Crouch)) { && !(graph[dangerIndex].flags & NodeFlag::Crouch)) {
if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (512.0f)) { if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (240.0f)) {
m_lookAt = destOrigin; m_lookAt = destOrigin;
} }
else { else {
@ -624,24 +604,56 @@ void Bot::setAimDirection () {
} }
} }
} }
else if (!verticalMove
&& m_moveToGoal
&& m_canSetAimDirection
&& isOnFloor ()
&& !isDucking ()
&& graph.exists (m_currentNodeIndex)
&& m_pathWalk.hasNext ()
&& pev->origin.distanceSq (destOrigin) < cr::sqrf (384.0f)
&& m_path->radius >= 16.0f
&& m_path->flags == 0 && graph[m_pathWalk.next ()].flags == 0) {
const auto nextPathIndex = m_pathWalk.next ();
const auto isNarrowPlace = isInNarrowPlace ();
if (graph.exists (nextPathIndex)
&& cr::abs (graph[nextPathIndex].origin.z - m_pathOrigin.z) < 8.0f) {
if (m_pathWalk.length () > 2 && !isNarrowPlace) {
const auto nextPathIndexX2 = m_pathWalk.nextX2 ();
if (vistab.visibleBothSides (m_currentNodeIndex, nextPathIndexX2)) {
m_lookAt = graph[nextPathIndexX2].origin + pev->view_ofs;
}
}
else if (!isNarrowPlace && vistab.visibleBothSides (m_currentNodeIndex, nextPathIndex)) {
m_lookAt = graph[nextPathIndex].origin + pev->view_ofs;
}
else {
m_lookAt = destOrigin;
}
}
else {
m_lookAt = destOrigin;
}
}
// try look at next node if on ladder // try look at next node if on ladder
if (horizontalMovement if (verticalMove && m_pathWalk.hasNext ()) {
&& m_pathWalk.hasNext ()
&& !(m_currentTravelFlags & PathFlag::Jump)) {
const auto &nextPath = graph[m_pathWalk.next ()]; const auto &nextPath = graph[m_pathWalk.next ()];
if ((nextPath.flags & NodeFlag::Ladder) if ((nextPath.flags & NodeFlag::Ladder)
&& m_destOrigin.distanceSq (pev->origin) < cr::sqrf (64.0f) && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (96.0f)
&& nextPath.origin.z > m_pathOrigin.z + 30.0f) { && nextPath.origin.z > m_pathOrigin.z + 26.0f) {
m_lookAt = nextPath.origin; m_lookAt = nextPath.origin + pev->view_ofs;
} }
} }
// don't look at bottom of node, if reached it // don't look at bottom of node, if reached it
if (m_lookAt == destOrigin && !horizontalMovement) { if (m_lookAt == destOrigin && !verticalMove) {
m_lookAt.z = getEyesPos ().z; m_lookAt.z = getEyesPos ().z;
} }