diff --git a/ext/crlib b/ext/crlib index b5f7ccc..f7b1b02 160000 --- a/ext/crlib +++ b/ext/crlib @@ -1 +1 @@ -Subproject commit b5f7ccc23dec2d018048b40085f78255cc232e52 +Subproject commit f7b1b02a301f900082d2e05ebbbc2d7edc2a4e09 diff --git a/inc/config.h b/inc/config.h index 494d6e7..412c468 100644 --- a/inc/config.h +++ b/inc/config.h @@ -29,16 +29,6 @@ public: // mostly config stuff, and some stuff dealing with menus class BotConfig final : public Singleton { -public: - struct DifficultyData { - float reaction[2] {}; - int32_t headshotPct {}; - int32_t seenThruPct {}; - int32_t hearThruPct {}; - int32_t maxRecoil {}; - Vector aimError {}; - }; - private: Array m_chat {}; Array > m_chatter {}; @@ -52,7 +42,7 @@ private: StringArray m_avatars {}; HashMap > m_language {}; - HashMap m_difficulty {}; + HashMap m_difficulty {}; HashMap m_custom {}; // default tables for personality weapon preferences, overridden by weapon.cfg @@ -218,10 +208,7 @@ public: } // get's the difficulty level tweaks - DifficultyData *getDifficultyTweaks (int32_t level) { - if (level < Difficulty::Noob || level > Difficulty::Expert) { - return &m_difficulty[Difficulty::Expert]; - } + BotDifficultyData *getDifficultyTweaks (int32_t level) { return &m_difficulty[level]; } diff --git a/inc/constant.h b/inc/constant.h index ebc8794..f29975d 100644 --- a/inc/constant.h +++ b/inc/constant.h @@ -69,8 +69,8 @@ CR_DECLARE_SCOPED_ENUM (Menu, // bomb say string CR_DECLARE_SCOPED_ENUM (BombPlantedSay, - ChatSay = cr::bit (1), - Chatter = cr::bit (2) + ChatSay = cr::bit (1), + Chatter = cr::bit (2) ) // chat types id's @@ -419,7 +419,13 @@ CR_DECLARE_SCOPED_ENUM (GoalTactic, 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 { constexpr auto Normal { 35.0f }; constexpr auto Pause { 36.0f }; @@ -447,7 +453,7 @@ constexpr auto kSprayDistanceX2 = kSprayDistance * 2; constexpr auto kMaxChatterRepeatInterval = 99.0f; constexpr auto kViewFrameUpdate = 1.0f / 25.0f; constexpr auto kGrenadeDamageRadius = 385.0f; -constexpr auto kMinMovedDistance = 2.5f; +constexpr auto kMinMovedDistance = cr::sqrf (2.0f); constexpr auto kInfiniteDistanceLong = static_cast (kInfiniteDistance); constexpr auto kMaxWeapons = 32; @@ -462,35 +468,35 @@ constexpr auto kConfigExtension = "cfg"; // weapon masks constexpr auto kPrimaryWeaponMask = (cr::bit (Weapon::XM1014) | - cr::bit (Weapon::M3) | - cr::bit (Weapon::MAC10) | - cr::bit (Weapon::UMP45) | - cr::bit (Weapon::MP5) | - cr::bit (Weapon::TMP) | - cr::bit (Weapon::P90) | - cr::bit (Weapon::AUG) | - cr::bit (Weapon::M4A1) | - cr::bit (Weapon::SG552) | - cr::bit (Weapon::AK47) | - cr::bit (Weapon::Scout) | - cr::bit (Weapon::SG550) | - cr::bit (Weapon::AWP) | - cr::bit (Weapon::G3SG1) | - cr::bit (Weapon::M249) | - cr::bit (Weapon::Famas) | - cr::bit (Weapon::Galil)); + cr::bit (Weapon::M3) | + cr::bit (Weapon::MAC10) | + cr::bit (Weapon::UMP45) | + cr::bit (Weapon::MP5) | + cr::bit (Weapon::TMP) | + cr::bit (Weapon::P90) | + cr::bit (Weapon::AUG) | + cr::bit (Weapon::M4A1) | + cr::bit (Weapon::SG552) | + cr::bit (Weapon::AK47) | + cr::bit (Weapon::Scout) | + cr::bit (Weapon::SG550) | + cr::bit (Weapon::AWP) | + cr::bit (Weapon::G3SG1) | + cr::bit (Weapon::M249) | + cr::bit (Weapon::Famas) | + cr::bit (Weapon::Galil)); constexpr auto kSecondaryWeaponMask = (cr::bit (Weapon::P228) - | cr::bit (Weapon::Elite) - | cr::bit (Weapon::USP) - | cr::bit (Weapon::Glock18) - | cr::bit (Weapon::Deagle) - | cr::bit (Weapon::FiveSeven)); + | cr::bit (Weapon::Elite) + | cr::bit (Weapon::USP) + | cr::bit (Weapon::Glock18) + | cr::bit (Weapon::Deagle) + | cr::bit (Weapon::FiveSeven)); constexpr auto kSniperWeaponMask = (cr::bit (Weapon::Scout) - | cr::bit (Weapon::SG550) - | cr::bit (Weapon::AWP) - | cr::bit (Weapon::G3SG1)); + | cr::bit (Weapon::SG550) + | cr::bit (Weapon::AWP) + | cr::bit (Weapon::G3SG1)); // weapons < 7 are secondary constexpr auto kPrimaryWeaponMinIndex = 7; diff --git a/inc/control.h b/inc/control.h index b11c602..3f12c9e 100644 --- a/inc/control.h +++ b/inc/control.h @@ -23,8 +23,8 @@ CR_DECLARE_SCOPED_ENUM (PrintQueueDestination, // bot command manager class BotControl final : public Singleton { public: - using Handler = int (BotControl::*) (); - using MenuHandler = int (BotControl::*) (int); + using Handler = int (BotControl:: *) (); + using MenuHandler = int (BotControl:: *) (int); public: // generic bot command @@ -36,8 +36,7 @@ public: public: 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 @@ -47,8 +46,7 @@ public: MenuHandler handler {}; 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 @@ -57,10 +55,9 @@ public: String text {}; 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 @@ -118,6 +115,7 @@ private: int cmdNodeSave (); int cmdNodeLoad (); int cmdNodeErase (); + int cmdNodeRefresh (); int cmdNodeEraseTraining (); int cmdNodeDelete (); int cmdNodeCheck (); @@ -255,7 +253,7 @@ public: } } - edict_t *getIssuer() { + edict_t *getIssuer () { return m_ent; } diff --git a/inc/engine.h b/inc/engine.h index 60d67ef..5f883ad 100644 --- a/inc/engine.h +++ b/inc/engine.h @@ -60,7 +60,7 @@ CR_DECLARE_SCOPED_ENUM (MapFlags, Escape = cr::bit (3), KnifeArena = cr::bit (4), FightYard = cr::bit (5), - GrenadeWar = cr::bit(6), + GrenadeWar = cr::bit (6), HasDoors = cr::bit (10), // additional flags HasButtons = cr::bit (11) // map has buttons ) @@ -190,6 +190,9 @@ public: // initialize levels void levelInitialize (edict_t *entities, int max); + // when entity spawns + void onSpawnEntity (edict_t *ent); + // shutdown levels void levelShutdown (); @@ -291,7 +294,7 @@ public: bool isFakeClientEntity (edict_t *ent) const; // check if entity is a player - bool isPlayerEntity (edict_t *ent) const ; + bool isPlayerEntity (edict_t *ent) const; // check if entity is a monster bool isMonsterEntity (edict_t *ent) const; @@ -333,7 +336,10 @@ public: // gets custom engine args for client command 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 @@ -486,7 +492,7 @@ public: // helper to sending the client message void sendClientMessage (bool console, edict_t *ent, StringRef message); - + // helper to sending the server message void sendServerMessage (StringRef message); diff --git a/inc/graph.h b/inc/graph.h index d3e5518..6bd6782 100644 --- a/inc/graph.h +++ b/inc/graph.h @@ -227,6 +227,7 @@ public: bool loadGraphData (); bool canDownload (); bool isAnalyzed () const; + bool isConverted () const; void saveOldFormat (); void reset (); diff --git a/inc/message.h b/inc/message.h index 8b02679..c745784 100644 --- a/inc/message.h +++ b/inc/message.h @@ -69,7 +69,7 @@ CR_DECLARE_SCOPED_ENUM (StatusIconCache, class MessageDispatcher final : public Singleton { private: - using MsgFunc = void (MessageDispatcher::*) (); + using MsgFunc = void (MessageDispatcher:: *) (); using MsgHash = Hash ; private: @@ -81,9 +81,9 @@ private: }; public: - Args (float value) : float_ (value) { } - Args (int32_t value) : long_ (value) { } - Args (const char *value) : chars_ (value) { } + Args (float value) : float_ (value) {} + Args (int32_t value) : long_ (value) {} + Args (const char *value) : chars_ (value) {} }; private: diff --git a/inc/planner.h b/inc/planner.h index e3c2ebf..9128ebb 100644 --- a/inc/planner.h +++ b/inc/planner.h @@ -21,10 +21,10 @@ CR_DECLARE_SCOPED_ENUM (AStarResult, Success = 0, Failed, InternalError, -) + ) -// node added -using NodeAdderFn = const Lambda &; + // node added + using NodeAdderFn = const Lambda &; // route twin node template struct RouteTwin final { @@ -58,7 +58,7 @@ public: static float gfunctionKillsDist (int team, int currentIndex, int parentIndex); // 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 static float gfunctionKills (int team, int currentIndex, int); diff --git a/inc/storage.h b/inc/storage.h index 3cfff02..ca3aba1 100644 --- a/inc/storage.h +++ b/inc/storage.h @@ -22,7 +22,8 @@ CR_DECLARE_SCOPED_ENUM (StorageOption, 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 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 diff --git a/inc/vistable.h b/inc/vistable.h index 9f5c288..3239ff1 100644 --- a/inc/vistable.h +++ b/inc/vistable.h @@ -54,6 +54,11 @@ public: bool isReady () const { 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 diff --git a/inc/yapb.h b/inc/yapb.h index 8a68bb7..1ba1447 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -23,7 +23,7 @@ using namespace cr; // tasks definition struct BotTask { - using Function = void (Bot::*) (); + using Function = void (Bot:: *) (); public: Function func {}; // corresponding exec function in bot class @@ -34,7 +34,7 @@ public: bool resume {}; // if task can be continued if interrupted 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 @@ -68,7 +68,7 @@ struct WeaponInfo { bool primaryFireHold {}; // hold down primary fire button to use? public: - WeaponInfo (int id, + WeaponInfo (int id, StringRef name, StringRef model, int price, @@ -78,14 +78,13 @@ public: int buyGroup, int buySelect, int buySelectT, - int buySelectCT, + int buySelectCT, int penetratePower, int maxClip, 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), - penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) - { } + penetratePower (penetratePower), maxClip (maxClip), type (type), primaryFireHold (fireHold) {} }; // clients noise @@ -123,6 +122,16 @@ struct BotTeamData { 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 #include @@ -210,6 +219,8 @@ private: mutable Mutex m_pathFindLock {}; mutable Mutex m_predictLock {}; + float f_wpt_tim_str_chg; + private: uint32_t m_states {}; // sensing bitstates uint32_t m_collideMoves[kMaxCollideMoves] {}; // sorted array of movements @@ -240,11 +251,10 @@ private: int m_lastPredictLength {}; // last predicted path length 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_heavyTimestamp {}; // is it time to execute heavy-weight functions 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_timeHitDoor {}; // specific time after hitting the door float m_lastChatTime {}; // time bot last chatted @@ -361,7 +371,6 @@ private: Vector m_lookAtPredict {}; // aiming vector when predicting Vector m_desiredVelocity {}; // desired velocity for jump nodes Vector m_breakableOrigin {}; // origin of breakable - Vector m_rightRef {}; // right referential vector Vector m_checkFallPoint[2] {}; // check fall point Array m_ignoredBreakable {}; // list of ignored breakables @@ -370,6 +379,7 @@ private: UniquePtr m_hitboxEnumerator {}; + BotDifficultyData *m_difficultyData {}; Path *m_path {}; // pointer to the current path node String m_chatBuffer {}; // space for strings (say text...) Frustum::Planes m_viewFrustum {}; @@ -400,7 +410,7 @@ private: int numEnemiesNear (const Vector &origin, const float radius) const; int numFriendsNear (const Vector &origin, const float radius) const; - + float getEstimatedNodeReachTime (); float isInFOV (const Vector &dest) const; float getShiftSpeed (); @@ -529,7 +539,6 @@ private: void syncUpdatePredictedIndex (); void updatePredictedIndex (); void refreshCreatureStatus (char *infobuffer); - void updateRightRef (); void donateC4ToHuman (); void clearAmmoInfo (); void handleChatterTaskChange (Task tid); @@ -681,6 +690,7 @@ public: int m_ammoInClip[kMaxWeapons] {}; // ammo in clip for each weapons int m_ammo[MAX_AMMO_SLOTS] {}; // total ammo amounts int m_deathCount {}; // number of bot deaths + int m_ladderDir {}; // ladder move direction bool m_isVIP {}; // bot is vip? 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_hasProgressBar {}; // has progress bar on a HUD 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_kickedByRotation {}; // is bot kicked due to rotation ? bool m_kickMeFromServer {}; // kick the bot off the server? @@ -768,6 +778,7 @@ public: void startDoubleJump (edict_t *ent); void sendBotToOrigin (const Vector &origin); void markStale (); + void setNewDifficulty (int32_t newDifficulty); bool hasHostage (); bool hasPrimaryWeapon () const; bool hasSecondaryWeapon () const; @@ -792,7 +803,7 @@ public: bool isDucking () const { return !!(pev->flags & FL_DUCKING); } - + Vector getCenter () const { return (pev->absmax + pev->absmin) * 0.5; }; @@ -942,6 +953,7 @@ extern ConVar cv_ignore_enemies_after_spawn_time; extern ConVar cv_camping_time_min; extern ConVar cv_camping_time_max; extern ConVar cv_smoke_grenade_checks; +extern ConVar cv_smoke_greande_checks_radius; extern ConVar cv_check_darkness; extern ConVar cv_use_hitbox_enemy_targeting; extern ConVar cv_restricted_weapons; diff --git a/src/analyze.cpp b/src/analyze.cpp index c366d7d..9a29aec 100644 --- a/src/analyze.cpp +++ b/src/analyze.cpp @@ -130,7 +130,7 @@ void GraphAnalyze::update () { } void GraphAnalyze::suspend () { - m_updateInterval = 0.0f; + m_updateInterval = kInfiniteDistance; m_isAnalyzing = false; m_isAnalyzed = 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)) { 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 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)) { return; } - auto targetPos = graph[targetIndex].origin; + const auto &targetPos = graph[targetIndex].origin; // re-check there's nothing nearby, and add something we're want 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) && graph.isNodeReacheable (testPos, targetPos)) || (graph.isNodeReacheableWithJump (testPos, targetPos) && graph.isNodeReacheableWithJump (targetPos, testPos))) { + graph.add (NodeAddFlag::Normal, m_isCrouch ? Vector { nextPos.x, nextPos.y, nextPos.z - 9.0f } : nextPos); } } diff --git a/src/botlib.cpp b/src/botlib.cpp index 1a36839..1817ecf 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -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_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_smoke_grenade_checks ("smoke_grenade_checks", "1", "Affects the bot's vision by smoke clouds.", true, 0.0f, 2.0f); // game console variables ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); @@ -100,7 +99,7 @@ void Bot::avoidGrenades () { if (!(m_states & Sense::SeeingEnemy)) { 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); } } @@ -187,6 +186,7 @@ void Bot::checkBreakablesAround () { || !game.hasBreakables () || m_seeEnemyTime + 4.0f > game.time () || !game.isNullEntity (m_enemy) + || (m_aimFlags & (AimFlags::PredictPath | AimFlags::Danger)) || !hasPrimaryWeapon ()) { return; } @@ -320,15 +320,14 @@ void Bot::setIdealReactionTimers (bool actual) { return; // zero out reaction times for extreme mode } - const auto tweak = conf.getDifficultyTweaks (m_difficulty); if (actual) { - m_idealReactionTime = tweak->reaction[0]; - m_actualReactionTime = tweak->reaction[0]; + m_idealReactionTime = m_difficultyData->reaction[0]; + m_actualReactionTime = m_difficultyData->reaction[0]; return; } - m_idealReactionTime = rg (tweak->reaction[0], tweak->reaction[1]); + m_idealReactionTime = rg (m_difficultyData->reaction[0], m_difficultyData->reaction[1]); } void Bot::updatePickups () { @@ -1644,47 +1643,30 @@ void Bot::buyStuff () { } void Bot::updateEmotions () { - // slowly increase/decrease dynamic emotions back to their base level + constexpr auto kEmotionUpdateStep = 0.05f; + if (m_nextEmotionUpdate > game.time ()) { return; } if (m_seeEnemyTime + 1.0f > game.time ()) { - m_agressionLevel += 0.05f; - - if (m_agressionLevel > 1.0f) { - m_agressionLevel = 1.0f; - } + m_agressionLevel = cr::min (m_agressionLevel + kEmotionUpdateStep, 1.0f); } else if (m_seeEnemyTime + 5.0f < game.time ()) { + // smoothly return aggression to base level if (m_agressionLevel > m_baseAgressionLevel) { - m_agressionLevel -= 0.05f; + m_agressionLevel = cr::max (m_agressionLevel - kEmotionUpdateStep, m_baseAgressionLevel); } 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) { - m_fearLevel -= 0.05f; + m_fearLevel = cr::max (m_fearLevel - kEmotionUpdateStep, m_baseFearLevel); } else { - m_fearLevel += 0.05f; - } - - 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_fearLevel = cr::min (m_fearLevel + kEmotionUpdateStep, m_baseFearLevel); } } m_nextEmotionUpdate = game.time () + 0.5f; @@ -1922,7 +1904,6 @@ void Bot::setConditions () { } else if (rg.chance (60)) { if (m_lastVictim->v.weapons & kSniperWeaponMask) { - pushChatterMessage (Chatter::SniperKilled); } else { @@ -2500,7 +2481,7 @@ void Bot::executeChatterFrameEvents () { pushChatterMessage (Chatter::GottaFindC4); bots.clearBombSay (BombPlantedSay::Chatter); } - + } void Bot::checkRadioQueue () { @@ -3101,7 +3082,7 @@ void Bot::frame () { void Bot::update () { const auto tid = getCurrentTaskId (); - m_canChooseAimDirection = true; + m_canSetAimDirection = true; m_isAlive = game.isAliveEntity (ent ()); m_team = game.getPlayerTeam (ent ()); m_healthValue = cr::clamp (pev->health, 0.0f, 99999.9f); @@ -3242,7 +3223,7 @@ void Bot::logicDuringFreezetime () { if (ent) { m_lookAt = ent->v.origin + ent->v.view_ofs; - if (m_buyingFinished) { + if (m_buyingFinished && game.getPlayerTeam (ent) != m_team) { m_enemy = ent; m_enemyOrigin = ent->v.origin; } @@ -3338,12 +3319,11 @@ void Bot::checkSpawnConditions () { void Bot::logic () { // 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 (); // increase reaction time m_actualReactionTime += 0.3f; + m_movedDistance = kMinMovedDistance + 0.1f; // length of different vector (distance bot moved) if (m_actualReactionTime > m_idealReactionTime) { m_actualReactionTime = m_idealReactionTime; @@ -3364,13 +3344,11 @@ void Bot::logic () { if (m_prevTime <= game.time ()) { // see how far bot has moved since the previous position... - if (m_checkTerrain) { - m_movedDistance = m_prevOrigin.distance (pev->origin); - } + m_movedDistance = m_prevOrigin.distanceSq (pev->origin); // save current position as previous 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 @@ -3463,7 +3441,7 @@ void Bot::logic () { // ensure we're not stuck picking something 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)) { ensurePickupEntitiesClear (); } @@ -3478,8 +3456,6 @@ void Bot::logic () { // save the previous speed (for checking if stuck) m_prevSpeed = cr::abs (m_moveSpeed); - m_prevVelocity = cr::abs (pev->velocity.length2d ()); - m_lastDamageType = -1; // reset damage } @@ -3643,7 +3619,7 @@ void Bot::showDebugOverlay () { } bool Bot::hasHostage () { - if (cv_ignore_objectives || game.mapIs (MapFlags::Demolition)) { + if (cv_ignore_objectives || !game.mapIs (MapFlags::HostageRescue)) { return false; } @@ -3930,9 +3906,6 @@ void Bot::resetDoubleJump () { } void Bot::debugMsgInternal (StringRef str) { - if (game.isDedicated ()) { - return; - } const int level = cv_debug.as (); if (level <= 2) { @@ -3943,7 +3916,10 @@ void Bot::debugMsgInternal (StringRef str) { 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; } else if (level != 3) { @@ -3954,7 +3930,7 @@ void Bot::debugMsgInternal (StringRef str) { logger.message (printBuf.chars ()); } - if (playMessage) { + if (playMessage && !game.isDedicated ()) { ctrl.msg (printBuf.chars ()); sendToChat (printBuf, false); } @@ -4125,7 +4101,7 @@ void Bot::updateHearing () { if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () - || client.team == m_team + || client.team2 == m_team || !client.ent || client.noise.last < game.time ()) { @@ -4225,7 +4201,7 @@ void Bot::updateHearing () { else { if (cv_shoots_thru_walls && m_lastEnemy == m_hearedEnemy - && rg.chance (conf.getDifficultyTweaks (m_difficulty)->hearThruPct) + && rg.chance (m_difficultyData->hearThruPct) && m_seeEnemyTime + 3.0f > game.time () && isPenetrableObstacle (m_hearedEnemy->v.origin)) { @@ -4244,7 +4220,7 @@ void Bot::updateHearing () { void Bot::enteredBuyZone (int buyState) { // 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 } const int *econLimit = conf.getEconLimit (); diff --git a/src/combat.cpp b/src/combat.cpp index 64d4b38..f6ea202 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -614,7 +614,7 @@ bool Bot::lookupEnemies () { // if no enemy visible check if last one shoot able through wall if (cv_shoots_thru_walls - && rg.chance (conf.getDifficultyTweaks (m_difficulty)->seenThruPct) + && rg.chance (m_difficultyData->seenThruPct) && isPenetrableObstacle (newEnemy->v.origin)) { m_seeEnemyTime = game.time (); @@ -667,11 +667,11 @@ Vector Bot::getBodyOffsetError (float distance) { const auto &maxs = m_enemy->v.maxs, &mins = m_enemy->v.mins; m_aimLastError = Vector ( - rg (mins.x * hitError, maxs.x * hitError), + rg (mins.x * hitError, maxs.x * hitError), rg (mins.y * hitError, maxs.y * hitError), 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_aimErrorTime = game.time () + rg (0.4f, 0.8f); @@ -722,7 +722,7 @@ Vector Bot::getEnemyBodyOffset () { else if (game.isPlayerEntity (m_enemy)) { // now take in account different parts of enemy 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 if (distance > kSprayDistance && (isRecoilHigh () || usesShotgun ())) { @@ -789,10 +789,10 @@ Vector Bot::getCustomHeight (float distance) const { }; constexpr float kOffsetRanges[9][3] = { - { 0.0f, 0.0f, 0.0f }, // none - { 0.0f, 0.0f, 0.0f }, // melee + { 0.0f, 0.0f, 0.0f }, // none + { 0.0f, 0.0f, 0.0f }, // melee { 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 }, // rifle { 0.5f, -7.5f, -9.5f }, // smg @@ -1014,7 +1014,7 @@ bool Bot::needToPauseFiring (float distance) { const float tolerance = (100.0f - static_cast (m_difficulty) * 25.0f) / 99.0f; const float baseTime = distance > kSprayDistance ? 0.55f : 0.38f; - const float maxRecoil = static_cast (conf.getDifficultyTweaks (m_difficulty)->maxRecoil); + const float maxRecoil = static_cast (m_difficultyData->maxRecoil); // check if we need to compensate recoil 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 && distanceSq < cr::sqrf (kSprayDistance) && (m_jumpTime + 5.0f < game.time () - && isOnFloor () - && rg (0, 1000) < (m_isReloading ? 8 : 2) - && pev->velocity.length2d () > 150.0f) && !usesSniper () && isEnemyCone) { + && isOnFloor () + && rg (0, 1000) < (m_isReloading ? 8 : 2) + && pev->velocity.length2d () > 150.0f) && !usesSniper () && isEnemyCone) { pev->button |= IN_JUMP; } @@ -1646,8 +1646,7 @@ void Bot::attackMovement () { const int enemyNearestIndex = graph.getNearest (m_enemy->v.origin); - if (vistab.visible (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch) - && vistab.visible (enemyNearestIndex, m_currentNodeIndex, VisIndex::Crouch)) { + if (vistab.visibleBothSides (m_currentNodeIndex, enemyNearestIndex, VisIndex::Crouch)) { m_duckTime = game.time () + m_frameInterval * 3.0f; } } diff --git a/src/config.cpp b/src/config.cpp index 1989db9..873ccfd 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -46,7 +46,7 @@ void BotConfig::loadMainConfig (bool isFirstLoad) { return false; }; - auto storeVarValue = [] (cvar_t *c, StringRef value) { + auto storeVarValue = [] (cvar_t *c, StringRef value) { auto &cvars = game.getCvars (); for (auto &var : cvars) { @@ -303,9 +303,9 @@ void BotConfig::loadChatterConfig () { { "Chatter_GuardingEscapeZone", Chatter::GuardingEscapeZone, kMaxChatterRepeatInterval }, { "Chatter_GuardingVipSafety", Chatter::GuardingVIPSafety, kMaxChatterRepeatInterval }, { "Chatter_PlantingC4", Chatter::PlantingBomb, 10.0f }, - { "Chatter_InCombat", Chatter::InCombat, kMaxChatterRepeatInterval }, + { "Chatter_InCombat", Chatter::InCombat, kMaxChatterRepeatInterval }, { "Chatter_SeeksEnemy", Chatter::SeekingEnemies, kMaxChatterRepeatInterval }, - { "Chatter_Nothing", Chatter::Nothing, kMaxChatterRepeatInterval }, + { "Chatter_Nothing", Chatter::Nothing, kMaxChatterRepeatInterval }, { "Chatter_EnemyDown", Chatter::EnemyDown, 10.0f }, { "Chatter_UseHostage", Chatter::UsingHostages, kMaxChatterRepeatInterval }, { "Chatter_WonTheRound", Chatter::WonTheRound, kMaxChatterRepeatInterval }, @@ -350,7 +350,7 @@ void BotConfig::loadChatterConfig () { { "Chatter_BombSiteSecured", Chatter::BombsiteSecured, 3.5f }, { "Chatter_GoingToCamp", Chatter::GoingToCamp, 30.0f }, { "Chatter_Camp", Chatter::Camping, 10.0f }, - { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval}, + { "Chatter_OnARoll", Chatter::OnARoll, kMaxChatterRepeatInterval }, }; Array missingWaves {}; @@ -632,7 +632,7 @@ void BotConfig::loadDifficultyConfig () { }; 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} @@ -721,12 +721,12 @@ void BotConfig::loadCustomConfig () { auto setDefaults = [&] () { m_custom = { - { "C4ModelName", "c4.mdl" }, - { "AMXParachuteCvar", "sv_parachute" }, - { "CustomCSDMSpawnPoint", "view_spawn" }, + { "C4ModelName", "c4.mdl" }, + { "AMXParachuteCvar", "sv_parachute" }, + { "CustomCSDMSpawnPoint", "view_spawn" }, { "CSDMDetectCvar", "csdm_active" }, { "ZMDetectCvar", "zp_delay" }, - { "ZMDelayCvar", "zp_delay" }, + { "ZMDelayCvar", "zp_delay" }, { "ZMInfectedTeam", "T" }, { "EnableFakeBotFeatures", "no" }, { "DisableLogFile", "no" }, diff --git a/src/control.cpp b/src/control.cpp index e39cedc..62761f0 100644 --- a/src/control.cpp +++ b/src/control.cpp @@ -423,6 +423,7 @@ int BotControl::cmdNode () { 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 ("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 addGraphCmd ("path_create", "path_create [noarguments]", "Opens and displays path creation menu.", &BotControl::cmdNodePathCreate); @@ -598,6 +599,13 @@ int BotControl::cmdNodeAddBasic () { int BotControl::cmdNodeSave () { 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 (arg (option) == "nocheck") { graph.saveGraphData (); @@ -629,6 +637,13 @@ int BotControl::cmdNodeSave () { int BotControl::cmdNodeLoad () { 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 if (graph.loadGraphData ()) { msg ("Graph successfully loaded."); @@ -642,6 +657,13 @@ int BotControl::cmdNodeLoad () { int BotControl::cmdNodeErase () { 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 if (arg (iamsure) == "iamsure") { bstor.unlinkFromDisk (false, false); @@ -652,6 +674,26 @@ int BotControl::cmdNodeErase () { 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 (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 () { enum args { graph_cmd = 1, cmd }; diff --git a/src/engine.cpp b/src/engine.cpp index 341939b..9e7fb6f 100644 --- a/src/engine.cpp +++ b/src/engine.cpp @@ -61,9 +61,6 @@ void Game::levelInitialize (edict_t *entities, int max) { // startup threaded worker worker.startup (cv_threadpool_workers.as ()); - m_spawnCount[Team::CT] = 0; - m_spawnCount[Team::Terrorist] = 0; - // clear all breakables before initialization m_breakables.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.renderamt = 127; // set its transparency amount ent->v.effects |= EF_NODRAW; - - ++m_spawnCount[Team::CT]; } else if (classname == "info_player_deathmatch") { ent->v.rendermode = kRenderTransAlpha; // set its render mode to transparency ent->v.renderamt = 127; // set its transparency amount ent->v.effects |= EF_NODRAW; - - ++m_spawnCount[Team::Terrorist]; } else if (classname == "func_vip_safetyzone" || classname == "info_vip_safetyzone") { m_mapFlags |= MapFlags::Assassination; // assassination map @@ -187,6 +180,24 @@ void Game::levelInitialize (edict_t *entities, int max) { 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 () { // save collected practice on shutdown practice.save (); @@ -223,6 +234,10 @@ void Game::levelShutdown () { // disable command handling 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 { @@ -368,9 +383,9 @@ void Game::playSound (edict_t *ent, const char *sound) { void Game::setPlayerStartDrawModels () { static HashMap models { - {"info_player_start", "models/player/urban/urban.mdl"}, - {"info_player_deathmatch", "models/player/terror/terror.mdl"}, - {"info_vip_start", "models/player/vip/vip.mdl"} + { "info_player_start", "models/player/urban/urban.mdl" }, + { "info_player_deathmatch", "models/player/terror/terror.mdl" }, + { "info_vip_start", "models/player/vip/vip.mdl" } }; models.foreach ([&] (const String &key, const String &val) { @@ -791,53 +806,50 @@ void Game::registerCvars (bool gameVars) { } void Game::constructCSBinaryName (StringArray &libs) { - String libSuffix {}; // construct library suffix + String suffix {}; if (plat.android) { - libSuffix += "_android"; + suffix = "_android"; + if (plat.x64) { + suffix += "_arm64"; + } + else if (plat.arm) { + suffix += "_armv7l"; + } } else if (plat.psvita) { - libSuffix += "_psvita"; + suffix = "_psvita"; } - - if (plat.x64) { + else if (plat.x64) { if (plat.arm) { - libSuffix += "_arm64"; + suffix = "_arm64"; } else if (plat.ppc) { - libSuffix += "_ppc64le"; + suffix = "_ppc64le"; } else { - libSuffix += "_amd64"; + suffix = "_amd64"; } } - else { - if (plat.arm) { - // don't want to put whole build.h logic from xash3d, just set whatever is supported by the YaPB - if (plat.android) { - libSuffix += "_armv7l"; - } - else { - libSuffix += "_armv7hf"; - } - } - else if (!plat.nix && !plat.win && !plat.macos) { - libSuffix += "_i386"; - } + else if (plat.arm) { + // non-android arm32 + suffix = "_armv7hf"; } + 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 ()) - libs.insert (0, { "mp", "cs", "cs_i386" }); + // build base names + if (plat.android) { + // only "libcs" with suffix (no "mp", and must have "lib" prefix) + libs.insert (0, "libcs" + suffix); + } else { - // on Android, it's important to have `lib` prefix, otherwise package manager won't unpack the libraries - if (plat.android) - libs.insert (0, { "libcs" }); - else - libs.insert (0, { "mp", "cs" }); - - for (auto &lib : libs) { - lib += libSuffix; - } + // Standard: "mp" and "cs" with suffix + libs.insert (0, "cs" + suffix); + libs.insert (0, "mp" + suffix); } } @@ -881,10 +893,10 @@ bool Game::loadCSBinary () { } 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; // if we can't read file, skip it @@ -940,7 +952,7 @@ bool Game::loadCSBinary () { // no fake pings on xash3d if (!(m_gameFlags & (GameFlags::Xash3D | GameFlags::Xash3DLegacy))) { - m_gameFlags |= GameFlags::HasFakePings; + m_gameFlags |= GameFlags::HasFakePings; } } else { diff --git a/src/graph.cpp b/src/graph.cpp index 3054b5d..11ffc14 100644 --- a/src/graph.cpp +++ b/src/graph.cpp @@ -603,6 +603,10 @@ bool BotGraph::isAnalyzed () const { 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) { if (!hasEditor () && !analyzer.isAnalyzing ()) { return; @@ -1292,6 +1296,7 @@ void BotGraph::showFileInfo () { msg (" uncompressed_size: %dkB", info.uncompressed / 1024); msg (" options: %d", info.options); // 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 (""); @@ -1755,6 +1760,8 @@ bool BotGraph::convertOldFormat () { m_info.author = header.author; + m_info.header.options = StorageOption::Converted; + // clean editor so graph will be saved with header's author auto editor = m_editor; m_editor = nullptr; @@ -1813,9 +1820,10 @@ bool BotGraph::loadGraphData () { if (!modified.empty () && !modified.contains ("(none)")) { m_info.modified.assign (exten.modified); } + vistab.load (); // load/initialize visibility + planner.init (); // initialize our little path planner practice.load (); // load bots practice - vistab.load (); // load/initialize visibility populateNodes (); @@ -1851,7 +1859,7 @@ bool BotGraph::canDownload () { } bool BotGraph::saveGraphData () { - auto options = StorageOption::Graph | StorageOption::Exten; + auto options = m_info.header.options | StorageOption::Graph | StorageOption::Exten; String editorName {}; if (!hasEditor () && !m_info.author.empty ()) { @@ -1907,7 +1915,7 @@ void BotGraph::saveOldFormat () { String editorName {}; - if (!hasEditor () && !m_info.author.empty ()) { + if (!hasEditor () && !m_info.author.empty ()) { editorName = m_info.author; } else if (!game.isNullEntity (m_editor)) { @@ -2296,17 +2304,20 @@ void BotGraph::frame () { if (path.radius > 0.0f) { 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); - game.drawLine (m_editor, origin + Vector (sqr, -sqr, 0.0f), origin + Vector (0.0f, -path.radius, 0.0f), 5, 0, radiusColor, 200, 0, 10); + const Vector points[] = { + { 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); - 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 + 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); + for (auto i = 0; i < kMaxNodeLinks; ++i) { + game.drawLine (m_editor, origin + points[i], origin + points[(i + 1) % 8], 5, 0, radiusColor, 200, 0, 10); + } } else { const float sqr = cr::sqrtf (32.0f); @@ -2390,8 +2401,8 @@ void BotGraph::frame () { message.assignf (" %s node:\n" " Node %d of %d, Radius: %.1f, Light: %s\n" " Flags: %s\n" - " Origin: (%.1f, %.1f, %.1f)\n", - type, node, m_paths.length () - 1, p.radius, + " Origin: (%.1f, %.1f, %.1f)\n", + type, node, m_paths.length () - 1, p.radius, cr::fequal (p.light, kInvalidLightLevel) ? "Invalid" : strings.format ("%1.f", p.light), flags, p.origin.x, p.origin.y, p.origin.z ); diff --git a/src/linkage.cpp b/src/linkage.cpp index 186f576..079e61b 100644 --- a/src/linkage.cpp +++ b/src/linkage.cpp @@ -130,6 +130,9 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { // precache everything game.precache (); + // notify about entity spawn + game.onSpawnEntity (ent); + if (game.is (GameFlags::Metamod)) { RETURN_META_VALUE (MRES_IGNORED, 0); } @@ -483,6 +486,7 @@ CR_EXPORT int GetEntityAPI (gamefuncs_t *table, int interfaceVersion) { } } } + if (game.is (GameFlags::Metamod)) { RETURN_META (MRES_IGNORED); } diff --git a/src/manager.cpp b/src/manager.cpp index 9602939..2529a0f 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -186,9 +186,9 @@ BotCreateResult BotManager::create (StringRef name, int difficulty, int personal // try to set proffered personality static HashMap personalityMap { - {"normal", Personality::Normal }, - {"careful", Personality::Careful }, - {"rusher", Personality::Rusher }, + { "normal", Personality::Normal }, + { "careful", Personality::Careful }, + { "rusher", Personality::Rusher }, }; // 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 BotRequest request {}; - static StringRef any = "*"; + constexpr StringRef ANY = "*"; - request.name = (name.empty () || name == any) ? StringRef ("\0") : name; - request.difficulty = (difficulty.empty () || difficulty == any) ? -1 : difficulty.as (); - request.team = (team.empty () || team == any) ? -1 : team.as (); - request.skin = (skin.empty () || skin == any) ? -1 : skin.as (); - request.personality = (personality.empty () || personality == any) ? -1 : personality.as (); + auto handleParam = [&ANY] (StringRef value) { + return value.empty () || value == ANY ? -1 : value.as (); + }; + + 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; 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 ()); - constexpr char kTeams[6][12] = { "", {"Terrorists"}, {"CTs"}, "", "", {"Random"}, }; + constexpr char kTeams[6][12] = { "", { "Terrorists" }, { "CTs" }, "", "", { "Random" }, }; auto toAdd = numToAdd == -1 ? maxToAdd : numToAdd; // limit manually added count as well @@ -884,25 +888,25 @@ void BotManager::setWeaponMode (int selection) { selection--; 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, 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, -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, -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, -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, -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, -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, 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] = { - {-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, -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, -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, 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, -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, -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, -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, 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 auto tab = conf.getRawWeapons (); @@ -1080,7 +1084,7 @@ void BotManager::updateBotDifficulties () { // sets new difficulty for all bots for (const auto &bot : m_bots) { - bot->m_difficulty = difficulty; + bot->setNewDifficulty (difficulty); } m_lastDifficulty = difficulty; } @@ -1089,7 +1093,7 @@ void BotManager::updateBotDifficulties () { void BotManager::balanceBotDifficulties () { // difficulty changing once per round (time) auto updateDifficulty = [] (Bot *bot, int32_t offset) { - bot->m_difficulty = cr::clamp (static_cast (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert); + bot->setNewDifficulty (cr::clamp (static_cast (bot->m_difficulty + offset), Difficulty::Noob, Difficulty::Expert)); }; // 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_weaponBurstMode = BurstMode::Off; - m_difficulty = cr::clamp (static_cast (difficulty), Difficulty::Noob, Difficulty::Expert); + setNewDifficulty (cr::clamp (static_cast (difficulty), Difficulty::Noob, Difficulty::Expert)); auto minDifficulty = cv_difficulty_min.as (); auto maxDifficulty = cv_difficulty_max.as (); @@ -1202,7 +1206,7 @@ Bot::Bot (edict_t *bot, int difficulty, int personality, int team, int skin) { if (maxDifficulty > minDifficulty) { cr::swap (maxDifficulty, minDifficulty); } - m_difficulty = rg (minDifficulty, maxDifficulty); + setNewDifficulty (rg (minDifficulty, maxDifficulty)); } m_pingBase = fakeping.randomBase (); m_ping = fakeping.randomBase (); @@ -1515,14 +1519,13 @@ void Bot::newRound () { m_isLeader = false; m_hasProgressBar = false; - m_canChooseAimDirection = true; + m_canSetAimDirection = true; m_preventFlashing = 0.0f; m_timeTeamOrder = 0.0f; m_askCheckTime = rg (30.0f, 90.0f); m_minSpeed = 260.0f; m_prevSpeed = 0.0f; - m_prevVelocity = 0.0f; m_prevOrigin = Vector (kInfiniteDistance, kInfiniteDistance, kInfiniteDistance); m_prevTime = game.time (); m_lookUpdateTime = game.time (); @@ -1722,9 +1725,6 @@ void Bot::newRound () { m_enemyIgnoreTimer = 0.0f; } - // update refvec for blocked movement - updateRightRef (); - // and put buying into its message queue pushMsgQueue (BotMsg::Buy); startTask (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true); @@ -1828,6 +1828,16 @@ void Bot::markStale () { 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 () { // this function handles the selection of teams & class diff --git a/src/navigate.cpp b/src/navigate.cpp index ad536ec..80abc89 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -470,7 +470,7 @@ void Bot::resetCollision () { void Bot::ignoreCollision () { resetCollision (); - m_lastCollTime = game.time () + 0.5f; + m_lastCollTime = game.time () + 0.65f; m_checkTerrain = false; } @@ -602,11 +602,11 @@ void Bot::checkTerrain (const Vector &dirNormal) { const auto tid = getCurrentTaskId (); // 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); // 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 () && tid != Task::Attack && tid != Task::Camp) { @@ -617,7 +617,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { m_firstCollideTime = 0.0f; } // 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_isStuck = true; @@ -653,14 +653,16 @@ void Bot::checkTerrain (const Vector &dirNormal) { resetCollision (); // reset collision memory if not being stuck for 0.5 secs } else { + const auto state = m_collideMoves[m_collStateIndex]; + // 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; } - else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeLeft) { + else if (state == CollisionState::StrafeLeft) { setStrafeSpeed (dirNormal, -pev->maxspeed); } - else if (m_collideMoves[m_collStateIndex] == CollisionState::StrafeRight) { + else if (state == CollisionState::StrafeRight) { setStrafeSpeed (dirNormal, pev->maxspeed); } } @@ -683,6 +685,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { bits |= CollisionProbe::Duck; } + // collision check allowed if not flying through the air if (isOnFloor () || isOnLadder () || isInWater ()) { uint32_t state[kMaxCollideMoves * 2 + 1] {}; @@ -696,7 +699,6 @@ void Bot::checkTerrain (const Vector &dirNormal) { state[i++] = CollisionState::StrafeRight; state[i++] = CollisionState::Duck; - // now weight all possible states if (bits & CollisionProbe::Jump) { state[i] = 0; @@ -745,7 +747,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } ++i; - + // now weight all possible states if (bits & CollisionProbe::Strafe) { state[i] = 0; state[i + 1] = 0; @@ -813,6 +815,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } } + if (bits & CollisionProbe::Duck) { state[i] = 0; @@ -901,7 +904,7 @@ void Bot::checkTerrain (const Vector &dirNormal) { } void Bot::checkFall () { - if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder)) { + if (isPreviousLadder () || (m_pathFlags & NodeFlag::Ladder) || isOnLadder ()) { return; } @@ -945,26 +948,28 @@ void Bot::checkFall () { const float nowDistanceSq = pev->origin.distanceSq (m_checkFallPoint[1]); if (nowDistanceSq > baseDistanceSq - && (nowDistanceSq > baseDistanceSq * 1.2f || nowDistanceSq > baseDistanceSq + 200.0f) - && baseDistanceSq >= cr::sqrf (80.0f) && nowDistanceSq >= cr::sqrf (100.0f)) { + && (nowDistanceSq > baseDistanceSq * 1.8f || nowDistanceSq > baseDistanceSq + 260.0f) + && baseDistanceSq >= cr::sqrf (124.0f) && nowDistanceSq >= cr::sqrf (146.0f)) { fixFall = true; } - else if (m_checkFallPoint[1].z > pev->origin.z + 128.0f - && m_checkFallPoint[0].z > pev->origin.z + 128.0f) { + else if (m_checkFallPoint[1].z > pev->origin.z + 138.0f + || m_checkFallPoint[0].z > pev->origin.z + 138.0f) { fixFall = true; } else if (m_currentNodeIndex != kInvalidNodeIndex - && nowDistanceSq > cr::sqrf (16.0f) - && m_checkFallPoint[1].z > pev->origin.z + 62.0f) { + && nowDistanceSq > cr::sqrf (32.0f) + && m_checkFallPoint[1].z > pev->origin.z + 72.0f) { fixFall = true; } if (fixFall) { - m_currentNodeIndex = kInvalidNodeIndex; - findValidNode (); + if (graph.exists (m_currentNodeIndex) && !isReachableNode (m_currentNodeIndex)) { + 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 if (!(m_pathFlags & NodeFlag::Ladder) && isPreviousLadder () - && isOnFloor () && isOnLadder () && m_moveSpeed > 50.0f - && pev->velocity.lengthSq () < 50.0f) { + && pev->velocity.lengthSq () < 50.0f + && m_ladderDir == LadderDir::Down) { pev->button |= IN_JUMP; m_jumpTime = game.time () + 1.0f; } +#if 0 const auto distanceSq2d = m_destOrigin.distanceSq2d (pev->origin + pev->velocity * m_frameInterval); - if (distanceSq2d < cr::sqrf (m_moveSpeed) * m_frameInterval && getTask ()->data != kInvalidNodeIndex) { - m_moveSpeed = distanceSq2d; - } - if (m_moveSpeed > pev->maxspeed) { - m_moveSpeed = pev->maxspeed; + if (distanceSq2d < cr::sqrf (m_moveSpeed * m_frameInterval) && getTask ()->data != kInvalidNodeIndex) { + m_moveSpeed = distanceSq2d * 2.0f; } + m_moveSpeed = cr::clamp (m_moveSpeed, 4.0f, pev->maxspeed); +#endif m_lastUsedNodesTime = game.time (); // special movement for swimming here @@ -1135,10 +1140,11 @@ bool Bot::updateNavigation () { m_jumpFinished = true; m_checkTerrain = false; m_desiredVelocity.clear (); + m_currentTravelFlags &= ~PathFlag::Jump; // cool down a little if next path after current will be jump 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; } } @@ -1149,19 +1155,13 @@ bool Bot::updateNavigation () { } if (m_pathFlags & NodeFlag::Ladder) { - constexpr auto kLadderOffset = Vector { 0.0f, 0.0f, 36.0f }; - 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 - if (graph.exists (prevNodeIndex) && !(graph[prevNodeIndex].flags & NodeFlag::Ladder) && ladderDistance < 64.0f) { - if (m_pathOrigin.z >= pev->origin.z + 16.0f) { - m_pathOrigin = m_path->origin + kLadderOffset; - } - else if (m_pathOrigin.z < pev->origin.z - 16.0f) { - m_pathOrigin = m_path->origin - kLadderOffset; - } + if (graph.exists (prevNodeIndex) + && !(graph[prevNodeIndex].flags & NodeFlag::Ladder) + && ladderDistanceSq < cr::sqrf (64.0f)) { if (!isDucking ()) { m_moveSpeed = pev->maxspeed * 0.4f; @@ -1171,20 +1171,14 @@ bool Bot::updateNavigation () { if (!isOnLadder ()) { pev->button &= ~IN_DUCK; } - m_approachingLadderTimer.start (m_frameInterval * 6.0f); + m_approachingLadderTimer.start (2.0f * m_frameInterval); } if (!isOnLadder () && isOnFloor () && !isDucking ()) { if (!isPreviousLadder ()) { - m_moveSpeed = ladderDistance; - } - - if (m_moveSpeed < 150.0f) { - m_moveSpeed = 150.0f; - } - else if (m_moveSpeed > pev->maxspeed) { - m_moveSpeed = pev->maxspeed; + m_moveSpeed = cr::sqrtf (ladderDistanceSq); } + 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) @@ -1238,7 +1232,7 @@ bool Bot::updateNavigation () { ignoreCollision (); // don't consider being stuck // 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 if (game.is (GameFlags::Xash3D)) { pev->button |= IN_USE; @@ -1252,7 +1246,7 @@ bool Bot::updateNavigation () { // make sure we are always facing the door when going through it m_aimFlags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); - m_canChooseAimDirection = false; + m_canSetAimDirection = false; // delay task if (m_buttonPushTime < game.time ()) { @@ -1322,26 +1316,35 @@ bool Bot::updateNavigation () { } } - float desiredDistanceSq = cr::sqrf (32.0f); - const float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); + float desiredDistanceSq = cr::sqrf (48.0f); + float nodeDistanceSq = pev->origin.distanceSq (m_pathOrigin); // initialize the radius for a special node type, where the node is considered to be reached if (m_pathFlags & NodeFlag::Lift) { desiredDistanceSq = cr::sqrf (50.0f); } 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 - if (game.mapIs (MapFlags::HostageRescue) && (m_pathFlags & NodeFlag::Goal)) { + if (game.mapIs (MapFlags::HostageRescue) + && (m_pathFlags & NodeFlag::Goal)) { + desiredDistanceSq = cr::sqrf (96.0f); } } - else if (m_pathFlags & NodeFlag::Ladder) { - desiredDistanceSq = cr::sqrf (16.0f); + else if (isOnLadder () || (m_pathFlags & NodeFlag::Ladder)) { + desiredDistanceSq = cr::sqrf (15.0f); } else if (m_currentTravelFlags & PathFlag::Jump) { 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 ()) { desiredDistanceSq = 0.0f; @@ -1368,14 +1371,14 @@ bool Bot::updateNavigation () { // if just recalculated path, assume reached current node 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 if (desiredDistanceSq < cr::sqrf (16.0f) && nodeDistanceSq < cr::sqrf (30.0f)) { 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; } } @@ -1410,6 +1413,9 @@ bool Bot::updateNavigation () { // update the practice for team practice.setValue (m_team, m_chosenGoalIndex, m_currentNodeIndex, goalValue); + + // ignore collision + ignoreCollision (); } 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 - 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) { const auto &path = graph[i]; @@ -2460,7 +2466,7 @@ bool Bot::selectBestNextNode () { const auto currentNodeIndex = m_pathWalk.first (); const auto prevNodeIndex = m_currentNodeIndex; - if (!isOccupiedNode (currentNodeIndex)) { + if (isOnLadder () || !isOccupiedNode (currentNodeIndex)) { return false; } @@ -2590,10 +2596,11 @@ bool Bot::advanceMovement () { bool isCurrentJump = false; // find out about connection flags - if (destIndex != kInvalidNodeIndex && m_currentNodeIndex != kInvalidNodeIndex) { + if (graph.exists (destIndex) && m_path) { for (const auto &link : m_path->links) { if (link.index == destIndex) { m_currentTravelFlags = link.flags; + m_desiredVelocity = link.velocity; m_jumpFinished = false; @@ -2603,7 +2610,7 @@ bool Bot::advanceMovement () { } // 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) { if (link.index == destIndex) { 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 for (const auto &other : bots) { + // if another bot uses this ladder, wait 3 secs if (other.get () != this && other->m_isAlive && other->m_currentNodeIndex == destIndex && other->isOnLadder ()) { startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 3.0f, false); @@ -2725,21 +2733,30 @@ void Bot::setPathOrigin () { else if (radius > 0.0f) { setNonZeroPathOrigin (); } - if (isOnLadder ()) { - TraceResult tr {}; - game.testLine (Vector (pev->origin.x, pev->origin.y, pev->absmin.z), m_pathOrigin, TraceIgnore::Everything, ent (), &tr); + edict_t *ladder = nullptr; - if (tr.flFraction < 1.0f) { - m_pathOrigin = m_pathOrigin + (pev->origin - m_pathOrigin) * 0.5f + Vector (0.0f, 0.0f, 32.0f); + game.searchEntities (m_pathOrigin, 96.0f, [&] (edict_t *e) { + 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) { // 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); // right referential vector - updateRightRef (); + auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // bot's head is clear, check at shoulder level... // trace from the bot's shoulder left diagonal forward to the right shoulder... - src = getEyesPos () + kVec00N16 - m_rightRef * -16.0f; - forward = getEyesPos () + kVec00N16 + m_rightRef * 16.0f + normal * 24.0f; + src = getEyesPos () + kVec00N16 - right * -16.0f; + forward = getEyesPos () + kVec00N16 + right * 16.0f + normal * 24.0f; 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... // trace from the bot's shoulder right diagonal forward to the left shoulder... - src = getEyesPos () + kVec00N16 + m_rightRef * 16.0f; - forward = getEyesPos () + kVec00N16 - m_rightRef * -16.0f + normal * 24.0f; + src = getEyesPos () + kVec00N16 + right * 16.0f; + forward = getEyesPos () + kVec00N16 - right * -16.0f + normal * 24.0f; 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); // trace from the left waist to the right forward waist pos - src = pev->origin + kVec00N17 - m_rightRef * -16.0f; - forward = pev->origin + kVec00N17 + m_rightRef * 16.0f + normal * 24.0f; + src = pev->origin + kVec00N17 - right * -16.0f; + forward = pev->origin + kVec00N17 + right * 16.0f + normal * 24.0f; // trace from the bot's waist straight forward... 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 - src = pev->origin + kVec00N24 + m_rightRef * 16.0f; - forward = pev->origin + kVec00N24 - m_rightRef * -16.0f + normal * 24.0f; + src = pev->origin + kVec00N24 + right * 16.0f; + forward = pev->origin + kVec00N24 - right * -16.0f + normal * 24.0f; game.testLine (src, forward, TraceIgnore::Monsters, ent (), tr); @@ -2918,7 +2935,7 @@ bool Bot::canJumpUp (const Vector &normal) { if (!isOnFloor () && (isOnLadder () || !isInWater ())) { 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... 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); if (tr.flFraction < 1.0f) { - return doneCanJumpUp (normal, m_rightRef); + return doneCanJumpUp (normal, right); } else { // 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... - 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; // 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 (tr.flFraction < 1.0f) { - return doneCanJumpUp (normal, m_rightRef); + return doneCanJumpUp (normal, right); } // 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... - 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; // 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 (tr.flFraction < 1.0f) { - return doneCanJumpUp (normal, m_rightRef); + return doneCanJumpUp (normal, right); } // 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) { return false; } - updateRightRef (); + auto right = Vector { 0.0f, pev->angles.y, 0.0f }.right (); // 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; // 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... - src = baseHeight + (-m_rightRef * 16.0f); + src = baseHeight + (-right * 16.0f); dest = src + normal * 32.0f; // trace a line forward at duck height... @@ -3114,11 +3131,11 @@ bool Bot::isBlockedLeft () { if (m_moveSpeed < 0.0f) { direction = -48.0f; } - Vector right {}, forward {}; - pev->angles.angleVectors (&forward, &right, nullptr); + Vector left {}, forward {}; + pev->angles.angleVectors (&forward, &left, nullptr); // 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... if (game.mapIs (MapFlags::HasDoors) && tr.flFraction < 1.0f && !game.isDoorEntity (tr.pHit)) { @@ -3131,8 +3148,8 @@ bool Bot::isBlockedRight () { TraceResult tr {}; float direction = 48.0f; - if (m_moveSpeed > 0.0f) { - direction = 48.0f; + if (m_moveSpeed < 0.0f) { + direction = -48.0f; } Vector right {}, forward {}; pev->angles.angleVectors (&forward, &right, nullptr); diff --git a/src/planner.cpp b/src/planner.cpp index 4c7146f..64b1e13 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -184,7 +184,7 @@ bool AStarAlgo::cantSkipNode (const int a, const int b, bool 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) { return true; @@ -372,7 +372,7 @@ void FloydWarshallAlgo::syncRebuild () { for (int k = 0; k < m_length; ++k) { for (int i = 0; i < m_length; ++i) { 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) { *(matrix + (i * m_length) + j) = { (matrix + (i * m_length) + k)->index, distance }; diff --git a/src/storage.cpp b/src/storage.cpp index 9a2873c..56c8407 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -17,7 +17,7 @@ template bool BotStorage::load (SmallArray &data, ExtenHeader * // graphs can be downloaded... 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 data.clear (); @@ -85,11 +85,7 @@ template bool BotStorage::load (SmallArray &data, ExtenHeader * if (tryReload ()) { return true; } - - if (game.isDeveloperMode ()) { - return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename); - } - return false; + return error (isGraph, isDebug, file, "Unable to open %s file for reading (filename: '%s').", type.name, filename); } // erase the current graph just in case @@ -178,7 +174,7 @@ template bool BotStorage::load (SmallArray &data, ExtenHeader * if (isGraph) { resetRetries (); - ExtenHeader extenHeader; + ExtenHeader extenHeader {}; strings.copy (extenHeader.author, exten->author, cr::bufsize (exten->author)); if (extenSize <= actuallyRead) { diff --git a/src/support.cpp b/src/support.cpp index ad6e4e3..d2819df 100644 --- a/src/support.cpp +++ b/src/support.cpp @@ -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_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 () { m_needToSendWelcome = false; m_welcomeReceiveTime = 0.0f; @@ -369,7 +372,6 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) { if (!gameState.hasActiveGrenades ()) { return false; } - constexpr auto kSmokeGrenadeRadius = 115.0f; // distance along line of sight covered by smoke float totalSmokedLength = 0.0f; @@ -397,7 +399,7 @@ bool BotSupport::isLineBlockedBySmoke (const Vector &from, const Vector &to) { continue; } - const float smokeRadiusSq = cr::sqrf (kSmokeGrenadeRadius); + const float smokeRadiusSq = cr::sqrf (cv_smoke_greande_checks_radius.as ()); const Vector &smokeOrigin = game.getEntityOrigin (pent); 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 - const float maxSmokedLength = 0.7f * kSmokeGrenadeRadius; + const float maxSmokedLength = 0.7f * cv_smoke_greande_checks_radius.as (); // return true if the total length of smoke-covered line-of-sight is too much return totalSmokedLength > maxSmokedLength; diff --git a/src/tasks.cpp b/src/tasks.cpp index b264f94..3a8d1f8 100644 --- a/src/tasks.cpp +++ b/src/tasks.cpp @@ -21,7 +21,7 @@ void Bot::normal_ () { const int debugGoal = cv_debug_goal.as (); // user forced a node as a goal? - if (debugGoal != kInvalidNodeIndex) { + if (graph.exists (debugGoal)) { if (getTask ()->data != debugGoal) { clearSearchNodes (); @@ -105,7 +105,7 @@ void Bot::normal_ () { // spray logo sometimes if allowed to do so if (!(m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy)) && m_seeEnemyTime + 5.0f < game.time () - && m_reloadState == Reload::None + && m_reloadState == Reload::None && m_timeLogoSpray < game.time () && cv_spraypaints && pev->groundentity == game.getStartEntity () @@ -665,7 +665,7 @@ void Bot::camp_ () { predictNode = findAimingNode (m_lastEnemyOrigin, pathLength); if (isNodeValidForPredict (predictNode) && pathLength > 1 - && vistab.visible ( predictNode, m_currentNodeIndex)) { + && vistab.visible (predictNode, m_currentNodeIndex)) { m_lookAtSafe = graph[predictNode].origin + pev->view_ofs; } @@ -1477,7 +1477,7 @@ void Bot::shootBreakable_ () { } else { 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)) { m_ignoredBreakable.push (tr.pHit); @@ -1507,7 +1507,7 @@ void Bot::shootBreakable_ () { m_shootTime = game.time (); // 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)) { pev->button |= IN_ATTACK; } diff --git a/src/vision.cpp b/src/vision.cpp index 91b91bd..e10f9b8 100644 --- a/src/vision.cpp +++ b/src/vision.cpp @@ -230,7 +230,11 @@ void Bot::updateLookAngles () { float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); // 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 (); if (!cr::fzero (forward)) { @@ -436,7 +440,7 @@ void Bot::setAimDirection () { || (m_currentTravelFlags & PathFlag::Jump)) { flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); - m_canChooseAimDirection = false; + m_canSetAimDirection = false; } // 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); - if (distToPredictNodeSq >= cr::sqrf (2048.0f)) { + if (distToPredictNodeSq >= cr::sqrf (2048.0f) || + distToPredictNodeSq <= cr::sqrf (256.0f)) { return false; } @@ -542,7 +547,8 @@ void Bot::setAimDirection () { return false; } - return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as (); + return isNodeValidForPredict (predictNode) && pathLength < cv_max_nodes_for_predict.as () + && numEnemiesNear (graph[predictNode].origin, 1024.0f) > 0; }; if (changePredictedEnemy) { @@ -573,47 +579,21 @@ void Bot::setAimDirection () { const auto &destOrigin = m_destOrigin + pev->view_ofs; m_lookAt = destOrigin; - const bool horizontalMovement = (m_pathFlags & NodeFlag::Ladder) || isOnLadder (); - - 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; - } + const bool verticalMove = (m_pathFlags & NodeFlag::Ladder) || isOnLadder () || isPreviousLadder () || pev->velocity.z > 16.0f; if (m_numEnemiesLeft > 0 - && m_canChooseAimDirection + && m_canSetAimDirection && m_seeEnemyTime + 4.0f < game.time () - && m_currentNodeIndex != kInvalidNodeIndex - && !horizontalMovement) { + && graph.exists (m_currentNodeIndex) + && !(m_aimFlags & AimFlags::PredictPath)) { const auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); if (graph.exists (dangerIndex) - && vistab.visible (m_currentNodeIndex, dangerIndex) + && vistab.visibleBothSides (m_currentNodeIndex, dangerIndex) && !(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; } 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 - if (horizontalMovement - && m_pathWalk.hasNext () - && !(m_currentTravelFlags & PathFlag::Jump)) { - + if (verticalMove && m_pathWalk.hasNext ()) { const auto &nextPath = graph[m_pathWalk.next ()]; if ((nextPath.flags & NodeFlag::Ladder) - && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (64.0f) - && nextPath.origin.z > m_pathOrigin.z + 30.0f) { + && m_destOrigin.distanceSq (pev->origin) < cr::sqrf (96.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 - if (m_lookAt == destOrigin && !horizontalMovement) { + if (m_lookAt == destOrigin && !verticalMove) { m_lookAt.z = getEyesPos ().z; }