diff --git a/inc/yapb.h b/inc/yapb.h index e09f082..9e708bf 100644 --- a/inc/yapb.h +++ b/inc/yapb.h @@ -635,7 +635,7 @@ public: } bool hasNext () const { - return length () > m_cursor; + return length () - m_cursor > 1; } bool empty () const { @@ -749,7 +749,6 @@ private: float m_playServerTime {}; // time bot spent in the game float m_changeViewTime {}; // timestamp to change look at while at freezetime float m_breakableTime {}; // breakeble acquired time - float m_jumpDistance {}; // last jump distance bool m_moveToGoal {}; // bot currently moving to goal?? bool m_isStuck {}; // bot is stuck @@ -770,7 +769,6 @@ private: bool m_moveToC4 {}; // ct is moving to bomb bool m_grenadeRequested {}; // bot requested change to grenade bool m_needToSendWelcomeChat {}; // bot needs to greet people on server? - bool m_switchedToKnifeDuringJump {}; // bot needs to revert weapon after jump? bool m_isCreature {}; // bot is not a player, but something else ? zombie ? Pickup m_pickupType {}; // type of entity which needs to be used/picked up @@ -1302,6 +1300,7 @@ extern ConVar cv_graph_url; extern ConVar cv_graph_url_upload; extern ConVar cv_graph_auto_save_count; extern ConVar cv_graph_analyze_max_jump_height; +extern ConVar cv_spraypaints; extern ConVar mp_freezetime; extern ConVar mp_roundtime; diff --git a/meson.build b/meson.build index b6a71f0..fedbab4 100644 --- a/meson.build +++ b/meson.build @@ -240,6 +240,8 @@ sources = files( 'src/sounds.cpp', 'src/storage.cpp', 'src/support.cpp', + 'src/tasks.cpp', + 'src/vision.cpp', 'src/vistable.cpp' ) diff --git a/src/botlib.cpp b/src/botlib.cpp index 0700ce2..42789c5 100644 --- a/src/botlib.cpp +++ b/src/botlib.cpp @@ -18,13 +18,8 @@ ConVar cv_radio_mode ("yb_radio_mode", "2", "Allows bots to use radio or chatter ConVar cv_economics_rounds ("yb_economics_rounds", "1", "Specifies whether bots able to use team economics, like do not buy any weapons for whole team to keep money for better guns."); ConVar cv_economics_disrespect_percent ("yb_economics_disrespect_percent", "25", "Allows bots to ignore economics and buy weapons with disrespect of economics.", true, 0.0f, 100.0f); -ConVar cv_walking_allowed ("yb_walking_allowed", "1", "Specifies whether bots able to use 'shift' if they thinks that enemy is near."); -ConVar cv_camping_allowed ("yb_camping_allowed", "1", "Allows or disallows bots to camp. Doesn't affects bomb/hostage defending tasks."); -ConVar cv_avoid_grenades ("yb_avoid_grenades", "1", "Allows bots to partially avoid grenades."); ConVar cv_check_darkness ("yb_check_darkness", "1", "Allows or disallows bot to check environment for darkness, thus allows or not to use flashlights or NVG."); - -ConVar cv_camping_time_min ("yb_camping_time_min", "15.0", "Lower bound of time from which time for camping is calculated", true, 5.0f, 90.0f); -ConVar cv_camping_time_max ("yb_camping_time_max", "45.0", "Upper bound of time until which time for camping is calculated", true, 15.0f, 120.0f); +ConVar cv_avoid_grenades ("yb_avoid_grenades", "1", "Allows bots to partially avoid grenades."); ConVar cv_tkpunish ("yb_tkpunish", "1", "Allows or disallows bots to take revenge of teamkillers / team attacks."); ConVar cv_freeze_bots ("yb_freeze_bots", "0", "If enabled, the bots think function is disabled, so bots will not move anywhere from their spawn spots."); @@ -44,11 +39,8 @@ ConVar cv_pickup_best ("yb_pickup_best", "1", "Allows or disallows bots to picku ConVar cv_ignore_objectives ("yb_ignore_objectives", "0", "Allows or disallows bots to do map objectives, i.e. plant/defuse bombs, and saves hostages."); ConVar cv_random_knife_attacks ("yb_random_knife_attacks", "1", "Allows or disallows the ability for random knife attacks when bot is rushing and no enemy is nearby."); -ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "20", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f); - // game console variables ConVar mp_c4timer ("mp_c4timer", nullptr, Var::GameRef); -ConVar mp_flashlight ("mp_flashlight", nullptr, Var::GameRef); ConVar mp_buytime ("mp_buytime", nullptr, Var::GameRef, true, "1"); ConVar mp_startmoney ("mp_startmoney", nullptr, Var::GameRef, true, "800"); ConVar mp_footsteps ("mp_footsteps", nullptr, Var::GameRef); @@ -73,52 +65,6 @@ void Bot::pushMsgQueue (int message) { m_msgQueue.emplaceLast (message); } -float Bot::isInFOV (const Vector &destination) { - const float entityAngle = cr::wrapAngle360 (destination.yaw ()); // find yaw angle from source to destination... - const float viewAngle = cr::wrapAngle360 (pev->v_angle.y); // get bot's current view angle... - - // return the absolute value of angle to destination entity - // zero degrees means straight ahead, 45 degrees to the left or - // 45 degrees to the right is the limit of the normal view angle - float absoluteAngle = cr::abs (viewAngle - entityAngle); - - if (absoluteAngle > 180.0f) { - absoluteAngle = 360.0f - absoluteAngle; - } - return absoluteAngle; -} - -bool Bot::isInViewCone (const Vector &origin) { - // this function returns true if the spatial vector location origin is located inside - // the field of view cone of the bot entity, false otherwise. It is assumed that entities - // have a human-like field of view, that is, about 90 degrees. - - return util.isInViewCone (origin, ent ()); -} - -bool Bot::seesItem (const Vector &destination, const char *classname) { - TraceResult tr {}; - - // trace a line from bot's eyes to destination.. - game.testLine (getEyesPos (), destination, TraceIgnore::None, ent (), &tr); - - // check if line of sight to object is not blocked (i.e. visible) - if (tr.flFraction < 1.0f && tr.pHit && !tr.fStartSolid) { - return strcmp (tr.pHit->v.classname.chars (), classname) == 0; - } - return true; -} - -bool Bot::seesEntity (const Vector &dest, bool fromBody) { - TraceResult tr {}; - - // trace a line from bot's eyes to destination... - game.testLine (fromBody ? pev->origin : getEyesPos (), dest, TraceIgnore::Everything, ent (), &tr); - - // check if line of sight to object is not blocked (i.e. visible) - return tr.flFraction >= 1.0f; -} - void Bot::avoidGrenades () { // checks if bot 'sees' a grenade, and avoid it @@ -2630,208 +2576,6 @@ void Bot::tryHeadTowardRadioMessage () { } } -void Bot::updateAimDir () { - uint32_t flags = m_aimFlags; - - // don't allow bot to look at danger positions under certain circumstances - if (!(flags & (AimFlags::Grenade | AimFlags::Enemy | AimFlags::Entity))) { - - // check if narrow place and we're duck, do not predict enemies in that situation - const bool duckedInNarrowPlace = isInNarrowPlace () && ((m_pathFlags & NodeFlag::Crouch) || (pev->button & IN_DUCK)); - - if (duckedInNarrowPlace || isOnLadder () || isInWater () || (m_pathFlags & NodeFlag::Ladder) || (m_currentTravelFlags & PathFlag::Jump)) { - flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); - m_canChooseAimDirection = false; - } - } - - if (flags & AimFlags::Override) { - m_lookAt = m_lookAtSafe; - } - else if (flags & AimFlags::Grenade) { - m_lookAt = m_throw; - - float throwDistance = m_throw.distance (pev->origin); - float coordCorrection = 0.0f; - - if (throwDistance > 100.0f && throwDistance < 800.0f) { - coordCorrection = 0.25f * (m_throw.z - pev->origin.z); - } - else if (throwDistance >= 800.0f) { - float angleCorrection = 37.0f * (throwDistance - 800.0f) / 800.0f; - - if (angleCorrection > 45.0f) { - angleCorrection = 45.0f; - } - coordCorrection = throwDistance * cr::tanf (cr::deg2rad (angleCorrection)) + 0.25f * (m_throw.z - pev->origin.z); - } - m_lookAt.z += coordCorrection * 0.5f; - } - else if (flags & AimFlags::Enemy) { - focusEnemy (); - } - else if (flags & AimFlags::Entity) { - m_lookAt = m_entity; - - // do not look at hostages legs - if (m_pickupType == Pickup::Hostage) { - m_lookAt.z += 48.0f; - } - else if (m_pickupType == Pickup::Weapon) { - m_lookAt.z += 72.0; - } - } - else if (flags & AimFlags::LastEnemy) { - m_lookAt = m_lastEnemyOrigin; - - // did bot just see enemy and is quite aggressive? - if (m_seeEnemyTime + 2.0f - m_actualReactionTime + m_baseAgressionLevel > game.time ()) { - - // feel free to fire if shootable - if (!usesSniper () && lastEnemyShootable ()) { - m_wantsToFire = true; - } - } - } - else if (flags & AimFlags::PredictPath) { - bool changePredictedEnemy = true; - bool predictFailed = false; - - if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) { - changePredictedEnemy = false; - } - - auto doFailPredict = [this] () { - m_aimFlags &= ~AimFlags::PredictPath; - m_trackingEdict = nullptr; - }; - - if (changePredictedEnemy) { - int pathLength = m_lastPredictLength; - int predictNode = m_lastPredictIndex; - - if (predictNode != kInvalidNodeIndex) { - TraceResult tr; - game.testLine (getEyesPos (), graph[predictNode].origin, TraceIgnore::Everything, ent (), &tr); - - if (tr.flFraction < 0.2f) { - pathLength = kInfiniteDistanceLong; - } - } - - if (graph.exists (predictNode) && pathLength < cv_max_nodes_for_predict.int_ ()) { - m_lookAt = graph[predictNode].origin; - m_lookAtSafe = m_lookAt; - - m_timeNextTracking = game.time () + 0.25f; - m_trackingEdict = m_lastEnemy; - - // feel free to fire if shootable - if (!usesSniper () && lastEnemyShootable ()) { - m_wantsToFire = true; - } - } - else { - doFailPredict (); - predictFailed = true; - } - } - - if (predictFailed) { - doFailPredict (); - } - else { - m_lookAt = m_lookAtSafe; - } - } - else if (flags & AimFlags::Camp) { - m_lookAt = m_lookAtSafe; - } - else if (flags & AimFlags::Nav) { - m_lookAt = m_destOrigin; - - if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::sqrf (512.0f)) { - auto nextPathIndex = m_pathWalk.next (); - - if (vistab.visible (m_currentNodeIndex, nextPathIndex)) { - m_lookAt = graph[nextPathIndex].origin + pev->view_ofs; - } - else { - m_lookAt = m_destOrigin; - } - } - else if (m_seeEnemyTime + 3.0f > game.time () && !m_lastEnemyOrigin.empty ()) { - m_lookAt = m_lastEnemyOrigin; - } - else { - m_lookAt = m_destOrigin; - } - - if (m_canChooseAimDirection && m_seeEnemyTime + 4.0f < game.time () && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & NodeFlag::Ladder)) { - auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); - - if (graph.exists (dangerIndex) && vistab.visible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) { - if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (160.0f)) { - m_lookAt = m_destOrigin; - } - else { - m_lookAt = graph[dangerIndex].origin + pev->view_ofs; - - // add danger flags - m_aimFlags |= AimFlags::Danger; - } - } - } - - // don't look at bottom of node, if reached it - if (m_lookAt == m_destOrigin) { - m_lookAt.z = getEyesPos ().z; - } - } - - if (m_lookAt.empty ()) { - m_lookAt = m_destOrigin; - } -} - -void Bot::checkDarkness () { - - // do not check for darkness at the start of the round - if (m_spawnTime + 5.0f > game.time () || !graph.exists (m_currentNodeIndex)) { - return; - } - - // do not check every frame - if (m_checkDarkTime + 5.0f > game.time () || cr::fzero (m_path->light)) { - return; - } - auto skyColor = illum.getSkyColor (); - auto flashOn = (pev->effects & EF_DIMLIGHT); - - if (mp_flashlight.bool_ () && !m_hasNVG) { - auto task = Task (); - - if (!flashOn && task != Task::Camp && task != Task::Attack && m_heardSoundTime + 3.0f < game.time () && m_flashLevel > 30 && ((skyColor > 50.0f && m_path->light < 10.0f) || (skyColor <= 50.0f && m_path->light < 40.0f))) { - pev->impulse = 100; - } - else if (flashOn && (((m_path->light > 15.0f && skyColor > 50.0f) || (m_path->light > 45.0f && skyColor <= 50.0f)) || task == Task::Camp || task == Task::Attack || m_flashLevel <= 0 || m_heardSoundTime + 3.0f >= game.time ())) { - pev->impulse = 100; - } - } - else if (m_hasNVG) { - if (flashOn) { - pev->impulse = 100; - } - else if (!m_usesNVG && ((skyColor > 50.0f && m_path->light < 15.0f) || (skyColor <= 50.0f && m_path->light < 40.0f))) { - issueCommand ("nightvision"); - } - else if (m_usesNVG && ((m_path->light > 20.0f && skyColor > 50.0f) || (m_path->light > 45.0f && skyColor <= 50.0f))) { - issueCommand ("nightvision"); - } - } - m_checkDarkTime = game.time (); -} - void Bot::checkParachute () { static auto parachute = engfuncs.pfnCVarGetPointer (conf.fetchCustom ("AMXParachuteCvar").chars ()); @@ -3009,1672 +2753,6 @@ void Bot::logicDuringFreezetime () { m_changeViewTime = game.time () + rg.get (1.25f, 2.0f); } -void Bot::normal_ () { - m_aimFlags |= AimFlags::Nav; - - int debugGoal = cv_debug_goal.int_ (); - - // user forced a waypoint as a goal? - if (debugGoal != kInvalidNodeIndex && getTask ()->data != debugGoal) { - clearSearchNodes (); - - getTask ()->data = debugGoal; - m_chosenGoalIndex = debugGoal; - } - - // bots rushing with knife, when have no enemy (thanks for idea to nicebot project) - if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !m_hasHostage && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) { - if (rg.chance (40)) { - pev->button |= IN_ATTACK; - } - else { - pev->button |= IN_ATTACK2; - } - m_knifeAttackTime = game.time () + rg.get (2.5f, 6.0f); - } - const auto &prop = conf.getWeaponProp (m_currentWeapon); - - if (m_reloadState == Reload::None && getAmmo () != 0 && getAmmoInClip () < 5 && prop.ammo1 != -1) { - m_reloadState = Reload::Primary; - } - - // if bomb planted and it's a CT calculate new path to bomb point if he's not already heading for - if (!m_bombSearchOverridden && bots.isBombPlanted () && m_team == Team::CT && getTask ()->data != kInvalidNodeIndex && !(graph[getTask ()->data].flags & NodeFlag::Goal) && getCurrentTaskId () != Task::EscapeFromBomb) { - clearSearchNodes (); - getTask ()->data = kInvalidNodeIndex; - } - - // reached the destination (goal) waypoint? - if (updateNavigation ()) { - // if we're reached the goal, and there is not enemies, notify the team - if (!bots.isBombPlanted () && m_currentNodeIndex != kInvalidNodeIndex && (m_pathFlags & NodeFlag::Goal) && rg.chance (15) && numEnemiesNear (pev->origin, 650.0f) == 0) { - pushRadioMessage (Radio::SectorClear); - } - - completeTask (); - m_prevGoalIndex = kInvalidNodeIndex; - - // spray logo sometimes if allowed to do so - if (!(m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy)) && m_seeEnemyTime + 5.0f < game.time () && !m_reloadState && m_timeLogoSpray < game.time () && cv_spraypaints.bool_ () && rg.chance (50) && m_moveSpeed > getShiftSpeed () && game.isNullEntity (m_pickupItem)) { - if (!(game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_team == Team::CT)) { - startTask (Task::Spraypaint, TaskPri::Spraypaint, kInvalidNodeIndex, game.time () + 1.0f, false); - } - } - - // reached waypoint is a camp waypoint - if ((m_pathFlags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && cv_camping_allowed.bool_ () && !isKnifeMode ()) { - - // check if bot has got a primary weapon and hasn't camped before - if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !m_hasHostage) { - bool campingAllowed = true; - - // Check if it's not allowed for this team to camp here - if (m_team == Team::Terrorist) { - if (m_pathFlags & NodeFlag::CTOnly) { - campingAllowed = false; - } - } - else { - if (m_pathFlags & NodeFlag::TerroristOnly) { - campingAllowed = false; - } - } - - // don't allow vip on as_ maps to camp + don't allow terrorist carrying c4 to camp - if (campingAllowed && (m_isVIP || (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && !bots.isBombPlanted () && m_hasC4))) { - campingAllowed = false; - } - - // check if another bot is already camping here - if (campingAllowed && isOccupiedNode (m_currentNodeIndex)) { - campingAllowed = false; - } - - if (campingAllowed) { - // crouched camping here? - if (m_pathFlags & NodeFlag::Crouch) { - m_campButtons = IN_DUCK; - } - else { - m_campButtons = 0; - } - selectBestWeapon (); - - if (!(m_states & (Sense::SeeingEnemy | Sense::HearingEnemy)) && !m_reloadState) { - m_reloadState = Reload::Primary; - } - m_timeCamping = game.time () + rg.get (cv_camping_time_min.float_ (), cv_camping_time_max.float_ ()); - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, m_timeCamping, true); - - m_lookAtSafe = m_pathOrigin + m_path->start.forward () * 500.0f; - m_aimFlags |= AimFlags::Camp; - m_campDirection = 0; - - // tell the world we're camping - if (rg.chance (25)) { - pushRadioMessage (Radio::ImInPosition); - } - m_moveToGoal = false; - m_checkTerrain = false; - - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - } - } - } - else { - // some goal waypoints are map dependant so check it out... - if (game.mapIs (MapFlags::HostageRescue)) { - // CT Bot has some hostages following? - if (m_team == Team::CT && m_hasHostage) { - // and reached a rescue point? - if (m_pathFlags & NodeFlag::Rescue) { - m_hostages.clear (); - } - } - else if (m_team == Team::Terrorist && rg.chance (75)) { - int index = findDefendNode (m_path->origin); - - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.get (60.0f, 120.0f), true); // push camp task on to stack - startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.get (5.0f, 10.0f), true); // push move command - - // decide to duck or not to duck - selectCampButtons (index); - pushChatterMessage (Chatter::GoingToGuardVIPSafety); // play info about that - } - } - else if (game.mapIs (MapFlags::Demolition) && ((m_pathFlags & NodeFlag::Goal) || m_inBombZone)) { - // is it a terrorist carrying the bomb? - if (m_hasC4) { - if ((m_states & Sense::SeeingEnemy) && numFriendsNear (pev->origin, 768.0f) == 0) { - // request an help also - pushRadioMessage (Radio::NeedBackup); - pushChatterMessage (Chatter::ScaredEmotion); - - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.get (4.0f, 8.0f), true); - } - else { - startTask (Task::PlantBomb, TaskPri::PlantBomb, kInvalidNodeIndex, 0.0f, false); - } - } - else if (m_team == Team::CT) { - if (!bots.isBombPlanted () && numFriendsNear (pev->origin, 210.0f) < 4) { - int index = findDefendNode (m_path->origin); - float campTime = rg.get (25.0f, 40.f); - - // rusher bots don't like to camp too much - if (m_personality == Personality::Rusher) { - campTime *= 0.5f; - } - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + campTime, true); // push camp task on to stack - startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.get (5.0f, 11.0f), true); // push move command - - // decide to duck or not to duck - selectCampButtons (index); - - pushChatterMessage (Chatter::DefendingBombsite); // play info about that - } - } - } - } - } - // no more nodes to follow - search new ones (or we have a bomb) - else if (!hasActiveGoal ()) { - m_moveSpeed = pev->maxspeed; - - clearSearchNodes (); - ignoreCollision (); - - // did we already decide about a goal before? - auto currIndex = getTask ()->data; - auto destIndex = graph.exists (currIndex) && !isBannedNode (currIndex) && m_prevGoalIndex != currIndex ? currIndex : findBestGoal (); - - // check for existance (this is failover, for i.e. csdm, this should be not true with normal gameplay, only when spawned outside of waypointed area) - if (!graph.exists (destIndex)) { - destIndex = graph.getFarest (pev->origin, 512.0f); - } - - if (!graph.exists (cv_debug_goal.int_ ()) && graph.exists (currIndex) && m_prevGoalIndex == currIndex && !(graph[currIndex].flags & NodeFlag::Goal)) { - m_nodeHistory.push (currIndex); - } - m_prevGoalIndex = destIndex; - - // remember index - getTask ()->data = destIndex; - - auto pathSearchType = m_pathType; - - // override with fast path - if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { - pathSearchType = rg.chance (60) ? FindPath::Fast : FindPath::Optimal; - } - ensureCurrentNodeIndex (); - - // do pathfinding if it's not the current waypoint - if (destIndex != m_currentNodeIndex) { - findPath (m_currentNodeIndex, destIndex, pathSearchType); - } - } - else { - if (!isDucking () && !usesKnife () && !cr::fequal (m_minSpeed, pev->maxspeed) && m_minSpeed > 1.0f) { - m_moveSpeed = m_minSpeed; - } - } - float shiftSpeed = getShiftSpeed (); - - if (!m_isStuck && (!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) { - m_moveSpeed = shiftSpeed; - } - - // bot hasn't seen anything in a long time and is asking his teammates to report in - if (cv_radio_mode.int_ () > 1 && bots.getLastRadio (m_team) != Radio::ReportInTeam && bots.getRoundStartTime () + 20.0f < game.time () && m_askCheckTime < game.time () && rg.chance (15) && m_seeEnemyTime + rg.get (45.0f, 80.0f) < game.time () && numFriendsNear (pev->origin, 1024.0f) == 0) { - pushRadioMessage (Radio::ReportInTeam); - - m_askCheckTime = game.time () + rg.get (45.0f, 80.0f); - - // make sure everyone else will not ask next few moments - for (const auto &bot : bots) { - if (bot->m_notKilled) { - bot->m_askCheckTime = game.time () + rg.get (5.0f, 30.0f); - } - } - } -} - -void Bot::spraypaint_ () { - m_aimFlags |= AimFlags::Entity; - - // bot didn't spray this round? - if (m_timeLogoSpray < game.time () && getTask ()->time > game.time ()) { - const auto &forward = pev->v_angle.forward (); - Vector sprayOrigin = getEyesPos () + forward * 128.0f; - - TraceResult tr {}; - game.testLine (getEyesPos (), sprayOrigin, TraceIgnore::Monsters, ent (), &tr); - - // no wall in front? - if (tr.flFraction >= 1.0f) { - sprayOrigin.z -= 128.0f; - } - m_entity = sprayOrigin; - - if (getTask ()->time - 0.5f < game.time ()) { - // emit spraycan sound - engfuncs.pfnEmitSound (ent (), CHAN_VOICE, "player/sprayer.wav", 1.0f, ATTN_NORM, 0, 100); - game.testLine (getEyesPos (), getEyesPos () + forward * 128.0f, TraceIgnore::Monsters, ent (), &tr); - - // paint the actual logo decal - util.traceDecals (pev, &tr, m_logotypeIndex); - m_timeLogoSpray = game.time () + rg.get (60.0f, 90.0f); - } - } - else { - completeTask (); - } - m_moveToGoal = false; - m_checkTerrain = false; - - m_navTimeset = game.time (); - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - ignoreCollision (); -} - -void Bot::huntEnemy_ () { - m_aimFlags |= AimFlags::Nav; - - // if we've got new enemy... - if (!game.isNullEntity (m_enemy) || game.isNullEntity (m_lastEnemy)) { - - // forget about it... - clearTask (Task::Hunt); - m_prevGoalIndex = kInvalidNodeIndex; - } - else if (game.getTeam (m_lastEnemy) == m_team) { - - // don't hunt down our teammate... - clearTask (Task::Hunt); - - m_prevGoalIndex = kInvalidNodeIndex; - m_lastEnemy = nullptr; - } - else if (updateNavigation ()) // reached last enemy pos? - { - // forget about it... - completeTask (); - - m_prevGoalIndex = kInvalidNodeIndex; - m_lastEnemyOrigin = nullptr; - } - - // do we need to calculate a new path? - else if (!hasActiveGoal ()) { - clearSearchNodes (); - - int destIndex = kInvalidNodeIndex; - int goal = getTask ()->data; - - // is there a remembered index? - if (graph.exists (goal)) { - destIndex = goal; - } - - // find new one instead - else { - destIndex = graph.getNearest (m_lastEnemyOrigin); - } - - // remember index - m_prevGoalIndex = destIndex; - getTask ()->data = destIndex; - - if (destIndex != m_currentNodeIndex) { - findPath (m_currentNodeIndex, destIndex, FindPath::Fast); - } - } - - // bots skill higher than 60? - if (cv_walking_allowed.bool_ () && mp_footsteps.bool_ () && m_difficulty >= Difficulty::Normal && !isKnifeMode ()) { - // then make him move slow if near enemy - if (!(m_currentTravelFlags & PathFlag::Jump)) { - if (m_currentNodeIndex != kInvalidNodeIndex) { - if (m_path->radius < 32.0f && !isOnLadder () && !isInWater () && m_seeEnemyTime + 4.0f > game.time ()) { - m_moveSpeed = getShiftSpeed (); - } - } - } - } -} - -void Bot::seekCover_ () { - m_aimFlags |= AimFlags::Nav; - - if (!util.isAlive (m_lastEnemy)) { - completeTask (); - m_prevGoalIndex = kInvalidNodeIndex; - } - - // reached final waypoint? - else if (updateNavigation ()) { - // yep. activate hide behaviour - completeTask (); - m_prevGoalIndex = kInvalidNodeIndex; - - // start hide task - startTask (Task::Hide, TaskPri::Hide, kInvalidNodeIndex, game.time () + rg.get (3.0f, 12.0f), false); - - // get a valid look direction - const Vector &dest = getCampDirection (m_lastEnemyOrigin); - - m_aimFlags |= AimFlags::Camp; - m_lookAtSafe = dest; - m_campDirection = 0; - - // chosen waypoint is a camp waypoint? - if (m_pathFlags & NodeFlag::Camp) { - // use the existing camp node prefs - if (m_pathFlags & NodeFlag::Crouch) { - m_campButtons = IN_DUCK; - } - else { - m_campButtons = 0; - } - } - else { - // choose a crouch or stand pos - if (m_path->vis.crouch <= m_path->vis.stand) { - m_campButtons = IN_DUCK; - } - else { - m_campButtons = 0; - } - - // enter look direction from previously calculated positions - if (!dest.empty ()) { - m_lookAtSafe = dest; - } - } - - if (m_reloadState == Reload::None && getAmmoInClip () < 5 && getAmmo () != 0) { - m_reloadState = Reload::Primary; - } - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - m_moveToGoal = false; - m_checkTerrain = false; - } - else if (!hasActiveGoal ()) { - clearSearchNodes (); - int destIndex = kInvalidNodeIndex; - - if (getTask ()->data != kInvalidNodeIndex) { - destIndex = getTask ()->data; - } - else { - destIndex = findCoverNode (usesSniper () ? 256.0f : 512.0f); - - if (destIndex == kInvalidNodeIndex) { - m_retreatTime = game.time () + rg.get (5.0f, 10.0f); - m_prevGoalIndex = kInvalidNodeIndex; - - completeTask (); - return; - } - } - m_campDirection = 0; - - m_prevGoalIndex = destIndex; - getTask ()->data = destIndex; - - ensureCurrentNodeIndex (); - - if (destIndex != m_currentNodeIndex) { - findPath (m_currentNodeIndex, destIndex, FindPath::Fast); - } - } -} - -void Bot::attackEnemy_ () { - m_moveToGoal = false; - m_checkTerrain = false; - - if (!game.isNullEntity (m_enemy)) { - ignoreCollision (); - attackMovement (); - - if (usesKnife () && !m_enemyOrigin.empty ()) { - m_destOrigin = m_enemyOrigin; - } - } - else { - completeTask (); - m_destOrigin = m_lastEnemyOrigin; - } - m_navTimeset = game.time (); -} - -void Bot::pause_ () { - m_moveToGoal = false; - m_checkTerrain = false; - - m_navTimeset = game.time (); - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - m_aimFlags |= AimFlags::Nav; - - // is bot blinded and above average difficulty? - if (m_viewDistance < 500.0f && m_difficulty >= Difficulty::Normal) { - // go mad! - m_moveSpeed = -cr::abs ((m_viewDistance - 500.0f) * 0.5f); - - if (m_moveSpeed < -pev->maxspeed) { - m_moveSpeed = -pev->maxspeed; - } - m_lookAtSafe = getEyesPos () + pev->v_angle.forward () * 500.0f; - - m_aimFlags |= AimFlags::Override; - m_wantsToFire = true; - } - else { - pev->button |= m_campButtons; - } - - // stop camping if time over or gets hurt by something else than bullets - if (getTask ()->time < game.time () || m_lastDamageType > 0) { - completeTask (); - } -} - -void Bot::blind_ () { - m_moveToGoal = false; - m_checkTerrain = false; - m_navTimeset = game.time (); - - // if bot remembers last enemy position - if (m_difficulty >= Difficulty::Normal && !m_lastEnemyOrigin.empty () && util.isPlayer (m_lastEnemy) && !usesSniper ()) { - m_lookAt = m_lastEnemyOrigin; // face last enemy - m_wantsToFire = true; // and shoot it - } - - if (m_difficulty >= Difficulty::Normal && graph.exists (m_blindNodeIndex)) { - if (updateNavigation ()) { - if (m_blindTime >= game.time ()) { - completeTask (); - } - m_prevGoalIndex = kInvalidNodeIndex; - m_blindNodeIndex = kInvalidNodeIndex; - - m_blindMoveSpeed = 0.0f; - m_blindSidemoveSpeed = 0.0f; - m_blindButton = 0; - - m_states |= Sense::SuspectEnemy; - } - else if (!hasActiveGoal ()) { - clearSearchNodes (); - ensureCurrentNodeIndex (); - - m_prevGoalIndex = m_blindNodeIndex; - getTask ()->data = m_blindNodeIndex; - - findPath (m_currentNodeIndex, m_blindNodeIndex, FindPath::Fast); - } - } - else { - m_moveSpeed = m_blindMoveSpeed; - m_strafeSpeed = m_blindSidemoveSpeed; - pev->button |= m_blindButton; - - if (m_states & Sense::SuspectEnemy) { - m_states |= Sense::SuspectEnemy; - } - } - - if (m_blindTime < game.time ()) { - completeTask (); - } -} - -void Bot::camp_ () { - if (!cv_camping_allowed.bool_ () || m_isCreature) { - completeTask (); - return; - } - - m_aimFlags |= AimFlags::Camp; - m_checkTerrain = false; - m_moveToGoal = false; - - if (m_team == Team::CT && bots.isBombPlanted () && m_defendedBomb && !isBombDefusing (graph.getBombOrigin ()) && !isOutOfBombTimer ()) { - m_defendedBomb = false; - completeTask (); - } - ignoreCollision (); - - // half the reaction time if camping because you're more aware of enemies if camping - setIdealReactionTimers (); - m_idealReactionTime *= 0.5f; - - m_navTimeset = game.time (); - m_timeCamping = game.time (); - - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - findValidNode (); - - // random camp dir, or prediction - auto useRandomCampDirOrPredictEnemy = [&] () { - if (game.isNullEntity (m_lastEnemy) || !m_lastEnemyOrigin.empty ()) { - auto pathLength = 0; - auto lastEnemyNearestIndex = findAimingNode (m_lastEnemyOrigin, pathLength); - - if (pathLength > 1 && graph.exists (lastEnemyNearestIndex)) { - m_lookAtSafe = graph[lastEnemyNearestIndex].origin; - } - } - else { - m_lookAtSafe = graph[getRandomCampDir ()].origin + pev->view_ofs; - } - }; - - if (m_nextCampDirTime < game.time ()) { - m_nextCampDirTime = game.time () + rg.get (2.0f, 5.0f); - - if (m_pathFlags & NodeFlag::Camp) { - Vector dest; - - // switch from 1 direction to the other - if (m_campDirection < 1) { - dest = m_path->start; - m_campDirection ^= 1; - } - else { - dest = m_path->end; - m_campDirection ^= 1; - } - dest.z = 0.0f; - - // check if after the conversion camp start and camp end are broken, and bot will look into the wall - TraceResult tr {}; - - // and use real angles to check it - auto to = m_pathOrigin + dest.forward () * 500.0f; - - // let's check the destination - game.testLine (getEyesPos (), to, TraceIgnore::Monsters, ent (), &tr); - - // we're probably facing the wall, so ignore the flags provided by graph, and use our own - if (tr.flFraction < 0.5f) { - useRandomCampDirOrPredictEnemy (); - } - else { - m_lookAtSafe = to; - } - } - else { - useRandomCampDirOrPredictEnemy (); - } - } - // press remembered crouch button - pev->button |= m_campButtons; - - // stop camping if time over or gets hurt by something else than bullets - if (getTask ()->time < game.time () || m_lastDamageType > 0) { - completeTask (); - } -} - -void Bot::hide_ () { - if (m_isCreature) { - completeTask (); - return; - }; - - m_aimFlags |= AimFlags::Camp; - m_checkTerrain = false; - m_moveToGoal = false; - - // half the reaction time if camping - setIdealReactionTimers (); - m_idealReactionTime *= 0.5f; - - m_navTimeset = game.time (); - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - findValidNode (); - - if (hasShield () && !m_isReloading) { - if (!isShieldDrawn ()) { - pev->button |= IN_ATTACK2; // draw the shield! - } - else { - pev->button |= IN_DUCK; // duck under if the shield is already drawn - } - } - - // if we see an enemy and aren't at a good camping point leave the spot - if ((m_states & Sense::SeeingEnemy) || m_inBombZone) { - if (!(m_pathFlags & NodeFlag::Camp)) { - completeTask (); - - m_campButtons = 0; - m_prevGoalIndex = kInvalidNodeIndex; - - return; - } - } - - // if we don't have an enemy we're also free to leave - else if (m_lastEnemyOrigin.empty ()) { - completeTask (); - - m_campButtons = 0; - m_prevGoalIndex = kInvalidNodeIndex; - - if (getCurrentTaskId () == Task::Hide) { - completeTask (); - } - return; - } - - pev->button |= m_campButtons; - m_navTimeset = game.time (); - - if (!m_isReloading) { - checkReload (); - } - - // stop camping if time over or gets hurt by something else than bullets - if (getTask ()->time < game.time () || m_lastDamageType > 0) { - completeTask (); - } -} - -void Bot::moveToPos_ () { - m_aimFlags |= AimFlags::Nav; - - if (isShieldDrawn ()) { - pev->button |= IN_ATTACK2; - } - - auto ensureDestIndexOK = [&] (int &index) { - if (isOccupiedNode (index) || isBannedNode (index)) { - m_nodeHistory.push (index); - index = findDefendNode (m_position); - } - }; - - // reached destination? - if (updateNavigation ()) { - completeTask (); // we're done - - m_prevGoalIndex = kInvalidNodeIndex; - m_position = nullptr; - } - - // didn't choose goal waypoint yet? - else if (!hasActiveGoal ()) { - clearSearchNodes (); - - int destIndex = kInvalidNodeIndex; - int goal = getTask ()->data; - - if (graph.exists (goal)) { - destIndex = goal; - - // check if we're ok - ensureDestIndexOK (destIndex); - } - else { - destIndex = graph.getNearest (m_position); - - // check if we're ok - ensureDestIndexOK (destIndex); - } - - if (graph.exists (destIndex)) { - m_prevGoalIndex = destIndex; - getTask ()->data = destIndex; - - ensureCurrentNodeIndex (); - findPath (m_currentNodeIndex, destIndex, m_pathType); - } - else { - completeTask (); - } - } -} - -void Bot::plantBomb_ () { - m_aimFlags |= AimFlags::Camp; - - // we're still got the C4? - if (m_hasC4 && !isKnifeMode ()) { - if (m_currentWeapon != Weapon::C4) { - selectWeaponById (Weapon::C4); - } - - if (util.isAlive (m_enemy) || !m_inBombZone) { - completeTask (); - } - else { - m_moveToGoal = false; - m_checkTerrain = false; - m_navTimeset = game.time (); - - if (m_pathFlags & NodeFlag::Crouch) { - pev->button |= (IN_ATTACK | IN_DUCK); - } - else { - pev->button |= IN_ATTACK; - } - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - } - } - - // done with planting - else { - completeTask (); - - // tell teammates to move over here... - if (numFriendsNear (pev->origin, 1200.0f) != 0) { - pushRadioMessage (Radio::NeedBackup); - } - auto index = findDefendNode (pev->origin); - float guardTime = mp_c4timer.float_ () * 0.5f + mp_c4timer.float_ () * 0.25f; - - // push camp task on to stack - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + guardTime, true); - - // push move command - startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + guardTime, true); - - // decide to duck or not to duck - selectCampButtons (index); - } -} - -void Bot::defuseBomb_ () { - float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f; - float timeToBlowUp = getBombTimeleft (); - float defuseRemainingTime = fullDefuseTime; - - if (m_hasProgressBar /*&& isOnFloor ()*/) { - defuseRemainingTime = fullDefuseTime - game.time (); - } - - const Vector &bombPos = graph.getBombOrigin (); - bool defuseError = false; - - // exception: bomb has been defused - if (bombPos.empty ()) { - // fix for stupid behaviour of CT's when bot is defused - for (const auto &bot : bots) { - if (bot->m_team == m_team && bot->m_notKilled) { - auto defendPoint = graph.getFarest (bot->pev->origin); - - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.get (30.0f, 60.0f), true); // push camp task on to stack - startTask (Task::MoveToPosition, TaskPri::MoveToPosition, defendPoint, game.time () + rg.get (3.0f, 6.0f), true); // push move command - } - } - graph.setBombOrigin (true); - - if (m_numFriendsLeft != 0 && rg.chance (50)) { - if (timeToBlowUp <= 3.0f) { - if (cv_radio_mode.int_ () == 2) { - pushChatterMessage (Chatter::BarelyDefused); - } - else if (cv_radio_mode.int_ () == 1) { - pushRadioMessage (Radio::SectorClear); - } - } - else { - pushRadioMessage (Radio::SectorClear); - } - } - return; - } - else if (defuseRemainingTime > timeToBlowUp) { - defuseError = true; - } - else if (m_states & Sense::SeeingEnemy) { - int friends = numFriendsNear (pev->origin, 768.0f); - - if (friends < 2 && defuseRemainingTime < timeToBlowUp) { - defuseError = true; - - if (defuseRemainingTime + 2.0f > timeToBlowUp) { - defuseError = false; - } - - if (m_numFriendsLeft > friends) { - pushRadioMessage (Radio::NeedBackup); - } - } - } - - // one of exceptions is thrown. finish task. - if (defuseError) { - m_entity = nullptr; - - m_pickupItem = nullptr; - m_pickupType = Pickup::None; - - selectBestWeapon (); - resetCollision (); - - completeTask (); - - return; - } - - // to revert from pause after reload ting && just to be sure - m_moveToGoal = false; - m_checkTerrain = false; - - m_moveSpeed = pev->maxspeed; - m_strafeSpeed = 0.0f; - - // bot is reloading and we close enough to start defusing - if (m_isReloading && bombPos.distance2d (pev->origin) < 80.0f) { - if (m_numEnemiesLeft == 0 || timeToBlowUp < fullDefuseTime + 7.0f || ((getAmmoInClip () > 8 && m_reloadState == Reload::Primary) || (getAmmoInClip () > 5 && m_reloadState == Reload::Secondary))) { - int weaponIndex = bestWeaponCarried (); - - // just select knife and then select weapon - selectWeaponById (Weapon::Knife); - - if (weaponIndex > 0 && weaponIndex < kNumWeapons) { - selectWeaponByIndex (weaponIndex); - } - m_isReloading = false; - } - else { - m_moveToGoal = false; - m_checkTerrain = false; - - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - } - } - - // head to bomb and press use button - m_aimFlags |= AimFlags::Entity; - - m_destOrigin = bombPos; - m_entity = bombPos; - - pev->button |= IN_USE; - - // if defusing is not already started, maybe crouch before - if (!m_hasProgressBar && m_duckDefuseCheckTime < game.time ()) { - Vector botDuckOrigin, botStandOrigin; - - if (pev->button & IN_DUCK) { - botDuckOrigin = pev->origin; - botStandOrigin = pev->origin + Vector (0.0f, 0.0f, 18.0f); - } - else { - botDuckOrigin = pev->origin - Vector (0.0f, 0.0f, 18.0f); - botStandOrigin = pev->origin; - } - - float duckLength = m_entity.distanceSq (botDuckOrigin); - float standLength = m_entity.distanceSq (botStandOrigin); - - if (duckLength > 5625.0f || standLength > 5625.0f) { - if (standLength < duckLength) { - m_duckDefuse = false; // stand - } - else { - m_duckDefuse = m_difficulty >= Difficulty::Normal && m_numEnemiesLeft != 0; // duck - } - } - m_duckDefuseCheckTime = game.time () + 5.0f; - } - - // press duck button - if (m_duckDefuse || (m_oldButtons & IN_DUCK)) { - pev->button |= IN_DUCK; - } - else { - pev->button &= ~IN_DUCK; - } - - // we are defusing bomb - if (m_hasProgressBar || (m_oldButtons & IN_USE) || !game.isNullEntity (m_pickupItem)) { - pev->button |= IN_USE; - - if (!game.isNullEntity (m_pickupItem)) { - MDLL_Use (m_pickupItem, ent ()); - } - - m_reloadState = Reload::None; - m_navTimeset = game.time (); - - // don't move when defusing - m_moveToGoal = false; - m_checkTerrain = false; - - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - // notify team - if (m_numFriendsLeft != 0) { - pushChatterMessage (Chatter::DefusingBomb); - - if (numFriendsNear (pev->origin, 512.0f) < 2) { - pushRadioMessage (Radio::NeedBackup); - } - } - } - else { - completeTask (); - } -} - -void Bot::followUser_ () { - if (game.isNullEntity (m_targetEntity) || !util.isAlive (m_targetEntity)) { - m_targetEntity = nullptr; - completeTask (); - - return; - } - - if (m_targetEntity->v.button & IN_ATTACK) { - TraceResult tr {}; - game.testLine (m_targetEntity->v.origin + m_targetEntity->v.view_ofs, m_targetEntity->v.v_angle.forward () * 500.0f, TraceIgnore::Everything, ent (), &tr); - - if (!game.isNullEntity (tr.pHit) && util.isPlayer (tr.pHit) && game.getTeam (tr.pHit) != m_team) { - m_targetEntity = nullptr; - m_lastEnemy = tr.pHit; - m_lastEnemyOrigin = tr.pHit->v.origin; - - completeTask (); - return; - } - } - - if (!cr::fzero (m_targetEntity->v.maxspeed) && m_targetEntity->v.maxspeed < pev->maxspeed) { - m_moveSpeed = m_targetEntity->v.maxspeed; - } - - if (m_reloadState == Reload::None && getAmmo () != 0) { - m_reloadState = Reload::Primary; - } - - if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::sqrf (130.0f)) { - m_followWaitTime = 0.0f; - } - else { - m_moveSpeed = 0.0f; - - if (cr::fzero (m_followWaitTime)) { - m_followWaitTime = game.time (); - } - else { - if (m_followWaitTime + 3.0f < game.time ()) { - // stop following if we have been waiting too long - m_targetEntity = nullptr; - - pushRadioMessage (Radio::YouTakeThePoint); - completeTask (); - - return; - } - } - } - m_aimFlags |= AimFlags::Nav; - - if (cv_walking_allowed.bool_ () && m_targetEntity->v.maxspeed < m_moveSpeed && !isKnifeMode ()) { - m_moveSpeed = getShiftSpeed (); - } - - if (isShieldDrawn ()) { - pev->button |= IN_ATTACK2; - } - - // reached destination? - if (updateNavigation ()) { - getTask ()->data = kInvalidNodeIndex; - } - - // didn't choose goal waypoint yet? - if (!hasActiveGoal ()) { - clearSearchNodes (); - - int destIndex = graph.getNearest (m_targetEntity->v.origin); - auto points = graph.getNarestInRadius (200.0f, m_targetEntity->v.origin); - - for (auto &newIndex : points) { - // if waypoint not yet used, assign it as dest - if (newIndex != m_currentNodeIndex && !isOccupiedNode (newIndex)) { - destIndex = newIndex; - } - } - - if (graph.exists (destIndex) && graph.exists (m_currentNodeIndex)) { - m_prevGoalIndex = destIndex; - getTask ()->data = destIndex; - - // always take the shortest path - findPath (m_currentNodeIndex, destIndex, FindPath::Fast); - } - else { - m_targetEntity = nullptr; - completeTask (); - } - } -} - -void Bot::throwExplosive_ () { - Vector dest = m_throw; - - if (!(m_states & Sense::SeeingEnemy)) { - m_strafeSpeed = 0.0f; - m_moveSpeed = 0.0f; - m_moveToGoal = false; - } - else if (!(m_states & Sense::SuspectEnemy) && !game.isNullEntity (m_enemy)) { - dest = m_enemy->v.origin + m_enemy->v.velocity.get2d (); - } - m_isUsingGrenade = true; - m_checkTerrain = false; - - ignoreCollision (); - - if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) { - // heck, I don't wanna blow up myself - m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; - - selectBestWeapon (); - completeTask (); - - return; - } - m_grenade = calcThrow (getEyesPos (), dest); - - if (m_grenade.lengthSq () < 100.0f) { - m_grenade = calcToss (pev->origin, dest); - } - - if (m_grenade.lengthSq () <= 100.0f) { - m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; - - selectBestWeapon (); - completeTask (); - } - else { - m_aimFlags |= AimFlags::Grenade; - - auto grenade = setCorrectGrenadeVelocity ("hegrenade.mdl"); - - if (game.isNullEntity (grenade)) { - if (m_currentWeapon != Weapon::Explosive && !m_grenadeRequested) { - if (pev->weapons & cr::bit (Weapon::Explosive)) { - m_grenadeRequested = true; - selectWeaponById (Weapon::Explosive); - } - else { - m_grenadeRequested = false; - - selectBestWeapon (); - completeTask (); - - return; - } - } - else if (!(m_oldButtons & IN_ATTACK)) { - pev->button |= IN_ATTACK; - m_grenadeRequested = false; - } - } - } - pev->button |= m_campButtons; -} - -void Bot::throwFlashbang_ () { - Vector dest = m_throw; - - if (!(m_states & Sense::SeeingEnemy)) { - m_strafeSpeed = 0.0f; - m_moveSpeed = 0.0f; - m_moveToGoal = false; - } - else if (!(m_states & Sense::SuspectEnemy) && !game.isNullEntity (m_enemy)) { - dest = m_enemy->v.origin + m_enemy->v.velocity.get2d (); - } - - m_isUsingGrenade = true; - m_checkTerrain = false; - - ignoreCollision (); - - if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) { - m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; // heck, I don't wanna blow up myself - - selectBestWeapon (); - completeTask (); - - return; - } - m_grenade = calcThrow (getEyesPos (), dest); - - if (m_grenade.lengthSq () < 100.0f) { - m_grenade = calcToss (pev->origin, dest); - } - - if (m_grenade.lengthSq () <= 100.0f) { - m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; - - selectBestWeapon (); - completeTask (); - } - else { - m_aimFlags |= AimFlags::Grenade; - - auto grenade = setCorrectGrenadeVelocity ("flashbang.mdl"); - - if (game.isNullEntity (grenade)) { - if (m_currentWeapon != Weapon::Flashbang && !m_grenadeRequested) { - if (pev->weapons & cr::bit (Weapon::Flashbang)) { - m_grenadeRequested = true; - selectWeaponById (Weapon::Flashbang); - } - else { - m_grenadeRequested = false; - - selectBestWeapon (); - completeTask (); - - return; - } - } - else if (!(m_oldButtons & IN_ATTACK)) { - pev->button |= IN_ATTACK; - m_grenadeRequested = false; - } - } - } - pev->button |= m_campButtons; -} - -void Bot::throwSmoke_ () { - if (!(m_states & Sense::SeeingEnemy)) { - m_strafeSpeed = 0.0f; - m_moveSpeed = 0.0f; - m_moveToGoal = false; - } - - m_checkTerrain = false; - m_isUsingGrenade = true; - - ignoreCollision (); - - Vector src = m_lastEnemyOrigin - pev->velocity; - - // predict where the enemy is in secs - if (!game.isNullEntity (m_enemy)) { - src = src + m_enemy->v.velocity; - } - m_grenade = (src - getEyesPos ()).normalize (); - - if (getTask ()->time < game.time ()) { - completeTask (); - return; - } - - if (m_currentWeapon != Weapon::Smoke && !m_grenadeRequested) { - m_aimFlags |= AimFlags::Grenade; - - if (pev->weapons & cr::bit (Weapon::Smoke)) { - m_grenadeRequested = true; - - selectWeaponById (Weapon::Smoke); - getTask ()->time = game.time () + kGrenadeCheckTime * 2.0f; - } - else { - m_grenadeRequested = false; - - selectBestWeapon (); - completeTask (); - - return; - } - } - else if (!(m_oldButtons & IN_ATTACK)) { - pev->button |= IN_ATTACK; - m_grenadeRequested = false; - } - pev->button |= m_campButtons; -} - -void Bot::doublejump_ () { - if (!util.isAlive (m_doubleJumpEntity) || (m_aimFlags & AimFlags::Enemy) || (m_travelStartIndex != kInvalidNodeIndex && getTask ()->time + (graph.calculateTravelTime (pev->maxspeed, graph[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < game.time ())) { - resetDoubleJump (); - return; - } - m_aimFlags |= AimFlags::Nav; - - if (m_jumpReady) { - m_moveToGoal = false; - m_checkTerrain = false; - - m_navTimeset = game.time (); - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - bool inJump = (m_doubleJumpEntity->v.button & IN_JUMP) || (m_doubleJumpEntity->v.oldbuttons & IN_JUMP); - - if (m_duckForJump < game.time ()) { - pev->button |= IN_DUCK; - } - else if (inJump && !(m_oldButtons & IN_JUMP)) { - pev->button |= IN_JUMP; - } - - const auto &src = pev->origin + Vector (0.0f, 0.0f, 45.0f); - const auto &dest = src + Vector (0.0f, pev->angles.y, 0.0f).upward () * 256.0f; - - TraceResult tr {}; - game.testLine (src, dest, TraceIgnore::None, ent (), &tr); - - if (tr.flFraction < 1.0f && tr.pHit == m_doubleJumpEntity && inJump) { - m_duckForJump = game.time () + rg.get (3.0f, 5.0f); - getTask ()->time = game.time (); - } - return; - } - - if (m_currentNodeIndex == m_prevGoalIndex) { - m_pathOrigin = m_doubleJumpOrigin; - m_destOrigin = m_doubleJumpOrigin; - } - - if (updateNavigation ()) { - getTask ()->data = kInvalidNodeIndex; - } - - // didn't choose goal waypoint yet? - if (!hasActiveGoal ()) { - clearSearchNodes (); - - int destIndex = graph.getNearest (m_doubleJumpOrigin); - - if (graph.exists (destIndex)) { - m_prevGoalIndex = destIndex; - m_travelStartIndex = m_currentNodeIndex; - - getTask ()->data = destIndex; - - // always take the shortest path - findPath (m_currentNodeIndex, destIndex, FindPath::Fast); - - if (m_currentNodeIndex == destIndex) { - m_jumpReady = true; - } - } - else { - resetDoubleJump (); - } - } -} - -void Bot::escapeFromBomb_ () { - m_aimFlags |= AimFlags::Nav; - - if (!bots.isBombPlanted ()) { - completeTask (); - } - - if (isShieldDrawn ()) { - pev->button |= IN_ATTACK2; - } - - if (!usesKnife () && m_numEnemiesLeft == 0) { - selectWeaponById (Weapon::Knife); - } - - // reached destination? - if (updateNavigation ()) { - completeTask (); // we're done - - // press duck button if we still have some enemies - if (m_numEnemiesLeft > 0) { - m_campButtons = IN_DUCK; - } - - // we're reached destination point so just sit down and camp - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true); - } - - // didn't choose goal waypoint yet? - else if (!hasActiveGoal ()) { - clearSearchNodes (); - - int bestIndex = kInvalidNodeIndex; - - float safeRadius = rg.get (1513.0f, 2048.0f); - float closestDistance = kInfiniteDistance; - - for (const auto &path : graph) { - if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) { - continue; - } - float distance = pev->origin.distance (path.origin); - - if (closestDistance > distance) { - closestDistance = distance; - bestIndex = path.number; - } - } - - if (bestIndex < 0) { - bestIndex = graph.getFarest (pev->origin, safeRadius); - } - - // still no luck? - if (bestIndex < 0) { - completeTask (); // we're done - - // we have no destination point, so just sit down and camp - startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true); - return; - } - m_prevGoalIndex = bestIndex; - getTask ()->data = bestIndex; - - findPath (m_currentNodeIndex, bestIndex, FindPath::Fast); - } -} - -void Bot::shootBreakable_ () { - m_aimFlags |= AimFlags::Override; - - // breakable destroyed? - if (!game.isShootableBreakable (m_breakableEntity)) { - completeTask (); - return; - } - pev->button |= m_campButtons; - - m_checkTerrain = false; - m_moveToGoal = false; - m_navTimeset = game.time (); - m_lookAtSafe = m_breakableOrigin; - - // is bot facing the breakable? - if (util.getShootingCone (ent (), m_breakableOrigin) >= 0.90f) { - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - if (usesKnife ()) { - selectBestWeapon (); - } - m_wantsToFire = true; - m_shootTime = game.time (); - } - else { - m_checkTerrain = true; - m_moveToGoal = true; - - completeTask (); - } -} - -void Bot::pickupItem_ () { - if (game.isNullEntity (m_pickupItem)) { - m_pickupItem = nullptr; - completeTask (); - - return; - } - const Vector &dest = game.getEntityOrigin (m_pickupItem); - - m_destOrigin = dest; - m_entity = dest; - - // find the distance to the item - float itemDistance = dest.distance (pev->origin); - - switch (m_pickupType) { - case Pickup::DroppedC4: - case Pickup::None: - break; - - case Pickup::Weapon: - m_aimFlags |= AimFlags::Nav; - - // near to weapon? - if (itemDistance < 50.0f) { - int index = 0; - auto &info = conf.getWeapons (); - - for (index = 0; index < 7; ++index) { - if (strcmp (info[index].model, m_pickupItem->v.model.chars (9)) == 0) { - break; - } - } - - if (index < 7) { - // secondary weapon. i.e., pistol - int weaponIndex = 0; - - for (index = 0; index < 7; ++index) { - if (pev->weapons & cr::bit (info[index].id)) { - weaponIndex = index; - } - } - - if (weaponIndex > 0) { - selectWeaponByIndex (weaponIndex); - dropCurrentWeapon (); - - if (hasShield ()) { - dropCurrentWeapon (); // discard both shield and pistol - } - } - enteredBuyZone (BuyState::PrimaryWeapon); - } - else { - // primary weapon - int weaponIndex = bestWeaponCarried (); - bool niceWeapon = rateGroundWeapon (m_pickupItem); - - auto tab = conf.getRawWeapons (); - - if ((tab->id == Weapon::Shield || weaponIndex > 6 || hasShield ()) && niceWeapon) { - selectWeaponByIndex (weaponIndex); - dropCurrentWeapon (); - } - - if (!weaponIndex || !niceWeapon) { - m_itemIgnore = m_pickupItem; - m_pickupItem = nullptr; - m_pickupType = Pickup::None; - - break; - } - enteredBuyZone (BuyState::PrimaryWeapon); - } - checkSilencer (); // check the silencer - } - break; - - case Pickup::Shield: - m_aimFlags |= AimFlags::Nav; - - if (hasShield ()) { - m_pickupItem = nullptr; - break; - } - - // near to shield? - else if (itemDistance < 50.0f) { - // get current best weapon to check if it's a primary in need to be dropped - int weaponIndex = bestWeaponCarried (); - - if (weaponIndex > 6) { - selectWeaponByIndex (weaponIndex); - dropCurrentWeapon (); - } - } - break; - - case Pickup::PlantedC4: - m_aimFlags |= AimFlags::Entity; - - if (m_team == Team::CT && itemDistance < 80.0f) { - pushChatterMessage (Chatter::DefusingBomb); - - // notify team of defusing - if (m_numFriendsLeft < 3) { - pushRadioMessage (Radio::NeedBackup); - } - m_moveToGoal = false; - m_checkTerrain = false; - - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - - startTask (Task::DefuseBomb, TaskPri::DefuseBomb, kInvalidNodeIndex, 0.0f, false); - } - break; - - case Pickup::Hostage: - m_aimFlags |= AimFlags::Entity; - - if (!util.isAlive (m_pickupItem)) { - // don't pickup dead hostages - m_pickupItem = nullptr; - completeTask (); - - break; - } - - if (itemDistance < 50.0f) { - float angleToEntity = isInFOV (dest - getEyesPos ()); - - // bot faces hostage? - if (angleToEntity <= 10.0f) { - // use game dll function to make sure the hostage is correctly 'used' - MDLL_Use (m_pickupItem, ent ()); - - if (rg.chance (80)) { - pushChatterMessage (Chatter::UsingHostages); - } - m_hostages.push (m_pickupItem); - m_pickupItem = nullptr; - - completeTask (); - - float minDistance = kInfiniteDistance; - int nearestHostageNodeIndex = kInvalidNodeIndex; - - // find the nearest 'unused' hostage within the area - game.searchEntities (pev->origin, 768.0f, [&] (edict_t *ent) { - auto classname = ent->v.classname.chars (); - - if (strncmp ("hostage_entity", classname, 14) != 0 && strncmp ("monster_scientist", classname, 17) != 0) { - return EntitySearchResult::Continue; - } - - // check if hostage is dead - if (game.isNullEntity (ent) || ent->v.health <= 0) { - return EntitySearchResult::Continue; - } - - // check if hostage is with a bot - for (const auto &other : bots) { - if (other->m_notKilled) { - for (const auto &hostage : other->m_hostages) { - if (hostage == ent) { - return EntitySearchResult::Continue; - } - } - } - } - - // check if hostage is with a human teammate (hack) - for (auto &client : util.getClients ()) { - if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && - client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) { - return EntitySearchResult::Continue; - } - } - - int hostageNodeIndex = graph.getNearest (ent->v.origin); - - if (graph.exists (hostageNodeIndex)) { - float distance = graph[hostageNodeIndex].origin.distanceSq (pev->origin); - - if (distance < minDistance) { - minDistance = distance; - nearestHostageNodeIndex = hostageNodeIndex; - } - } - - return EntitySearchResult::Continue; - }); - - if (nearestHostageNodeIndex != kInvalidNodeIndex) { - clearTask (Task::MoveToPosition); // remove any move tasks - startTask (Task::MoveToPosition, TaskPri::MoveToPosition, nearestHostageNodeIndex, 0.0f, true); - } - } - ignoreCollision (); // also don't consider being stuck - } - break; - - case Pickup::DefusalKit: - m_aimFlags |= AimFlags::Nav; - - if (m_hasDefuser) { - m_pickupItem = nullptr; - m_pickupType = Pickup::None; - } - break; - - case Pickup::Button: - m_aimFlags |= AimFlags::Entity; - - if (game.isNullEntity (m_pickupItem) || m_buttonPushTime < game.time ()) { - completeTask (); - m_pickupType = Pickup::None; - - break; - } - - // find angles from bot origin to entity... - float angleToEntity = isInFOV (dest - getEyesPos ()); - - // near to the button? - if (itemDistance < 90.0f) { - m_moveSpeed = 0.0f; - m_strafeSpeed = 0.0f; - m_moveToGoal = false; - m_checkTerrain = false; - - // facing it directly? - if (angleToEntity <= 10.0f) { - MDLL_Use (m_pickupItem, ent ()); - - m_pickupItem = nullptr; - m_pickupType = Pickup::None; - m_buttonPushTime = game.time () + 3.0f; - - completeTask (); - } - } - break; - } -} - void Bot::executeTasks () { // this is core function that handle task execution @@ -4886,7 +2964,6 @@ void Bot::logic () { m_strafeSpeed = pev->maxspeed * static_cast (m_needAvoidGrenade); } - ensureEntitiesClear (); translateInput (); @@ -5070,29 +3147,6 @@ bool Bot::hasHostage () { return false; } -int Bot::getAmmo () { - return getAmmo (m_currentWeapon); -} - -int Bot::getAmmo (int id) { - const auto &prop = conf.getWeaponProp (id); - - if (prop.ammo1 == -1 || prop.ammo1 > kMaxWeapons - 1) { - return -1; - } - return m_ammo[prop.ammo1]; -} - -void Bot::selectWeaponByIndex (int index) { - auto tab = conf.getRawWeapons (); - issueCommand (tab[index].name); -} - -void Bot::selectWeaponById (int id) { - const auto &prop = conf.getWeaponProp (id); - issueCommand (prop.classname.chars ()); -} - void Bot::takeDamage (edict_t *inflictor, int damage, int armor, int bits) { // this function gets called from the network message handler, when bot's gets hurt from any // other player. @@ -5457,51 +3511,6 @@ void Bot::runMovement () { m_oldButtons = pev->button; } -void Bot::checkBurstMode (float distance) { - // this function checks burst mode, and switch it depending distance to to enemy. - - if (hasShield ()) { - return; // no checking when shield is active - } - - // if current weapon is glock, disable burstmode on long distances, enable it else - if (m_currentWeapon == Weapon::Glock18 && distance < 300.0f && m_weaponBurstMode == BurstMode::Off) { - pev->button |= IN_ATTACK2; - } - else if (m_currentWeapon == Weapon::Glock18 && distance >= 300.0f && m_weaponBurstMode == BurstMode::On) { - pev->button |= IN_ATTACK2; - } - - // if current weapon is famas, disable burstmode on short distances, enable it else - if (m_currentWeapon == Weapon::Famas && distance > 400.0f && m_weaponBurstMode == BurstMode::Off) { - pev->button |= IN_ATTACK2; - } - else if (m_currentWeapon == Weapon::Famas && distance <= 400.0f && m_weaponBurstMode == BurstMode::On) { - pev->button |= IN_ATTACK2; - } -} - -void Bot::checkSilencer () { - if ((m_currentWeapon == Weapon::USP || m_currentWeapon == Weapon::M4A1) && !hasShield () && game.isNullEntity (m_enemy)) { - int prob = (m_personality == Personality::Rusher ? 35 : 65); - - // aggressive bots don't like the silencer - if (rg.chance (m_currentWeapon == Weapon::USP ? prob / 2 : prob)) { - // is the silencer not attached... - if (pev->weaponanim > 6) { - pev->button |= IN_ATTACK2; // attach the silencer - } - } - else { - - // is the silencer attached... - if (pev->weaponanim <= 6) { - pev->button |= IN_ATTACK2; // detach the silencer - } - } - } -} - float Bot::getBombTimeleft () { if (!bots.isBombPlanted ()) { return 0.0f; @@ -5758,54 +3767,6 @@ float Bot::getShiftSpeed () { return pev->maxspeed * 0.4f; } -void Bot::calculateFrustum () { - // this function updates bot view frustum - - Vector forward, right, up; - pev->v_angle.angleVectors (&forward, &right, &up); - - static Vector fc, nc, fbl, fbr, ftl, ftr, nbl, nbr, ntl, ntr; - - fc = getEyesPos () + forward * frustum.MaxView; - nc = getEyesPos () + forward * frustum.MinView; - - fbl = fc + (up * frustum.farHeight * 0.5f) - (right * frustum.farWidth * 0.5f); - fbr = fc + (up * frustum.farHeight * 0.5f) + (right * frustum.farWidth * 0.5f); - ftl = fc - (up * frustum.farHeight * 0.5f) - (right * frustum.farWidth * 0.5f); - ftr = fc - (up * frustum.farHeight * 0.5f) + (right * frustum.farWidth * 0.5f); - nbl = nc + (up * frustum.nearHeight * 0.5f) - (right * frustum.nearWidth * 0.5f); - nbr = nc + (up * frustum.nearHeight * 0.5f) + (right * frustum.nearWidth * 0.5f); - ntl = nc - (up * frustum.nearHeight * 0.5f) - (right * frustum.nearWidth * 0.5f); - ntr = nc - (up * frustum.nearHeight * 0.5f) + (right * frustum.nearWidth * 0.5f); - - auto setPlane = [&] (FrustumSide side, const Vector &v1, const Vector &v2, const Vector &v3) { - auto &plane = m_frustum[side]; - - plane.normal = ((v2 - v1) ^ (v3 - v1)).normalize (); - plane.point = v2; - - plane.result = -(plane.normal | plane.point); - }; - - setPlane (FrustumSide::Top, ftl, ntl, ntr); - setPlane (FrustumSide::Bottom, fbr, nbr, nbl); - setPlane (FrustumSide::Left, fbl, nbl, ntl); - setPlane (FrustumSide::Right, ftr, ntr, nbr); - setPlane (FrustumSide::Near, nbr, ntr, ntl); - setPlane (FrustumSide::Far, fbl, ftl, ftr); -} - -bool Bot::isEnemyInFrustum (edict_t *enemy) { - const Vector &origin = enemy->v.origin - Vector (0.0f, 0.0f, 5.0f); - - for (auto &plane : m_frustum) { - if (!util.isObjectInsidePlane (plane, origin, 60.0f, 16.0f)) { - return false; - } - } - return true; -} - void Bot::refreshModelName (char *infobuffer) { if (infobuffer == nullptr) { infobuffer = engfuncs.pfnGetInfoKeyBuffer (ent ()); diff --git a/src/combat.cpp b/src/combat.cpp index 42f3064..dc7a0ba 100644 --- a/src/combat.cpp +++ b/src/combat.cpp @@ -2160,3 +2160,71 @@ bool Bot::isEnemyNoticeable (float range) { return rg.get (0.0f, 100.0f) < noticeChance; } + +int Bot::getAmmo () { + return getAmmo (m_currentWeapon); +} + +int Bot::getAmmo (int id) { + const auto &prop = conf.getWeaponProp (id); + + if (prop.ammo1 == -1 || prop.ammo1 > kMaxWeapons - 1) { + return -1; + } + return m_ammo[prop.ammo1]; +} + +void Bot::selectWeaponByIndex (int index) { + auto tab = conf.getRawWeapons (); + issueCommand (tab[index].name); +} + +void Bot::selectWeaponById (int id) { + const auto &prop = conf.getWeaponProp (id); + issueCommand (prop.classname.chars ()); +} + +void Bot::checkBurstMode (float distance) { + // this function checks burst mode, and switch it depending distance to to enemy. + + if (hasShield ()) { + return; // no checking when shield is active + } + + // if current weapon is glock, disable burstmode on long distances, enable it else + if (m_currentWeapon == Weapon::Glock18 && distance < 300.0f && m_weaponBurstMode == BurstMode::Off) { + pev->button |= IN_ATTACK2; + } + else if (m_currentWeapon == Weapon::Glock18 && distance >= 300.0f && m_weaponBurstMode == BurstMode::On) { + pev->button |= IN_ATTACK2; + } + + // if current weapon is famas, disable burstmode on short distances, enable it else + if (m_currentWeapon == Weapon::Famas && distance > 400.0f && m_weaponBurstMode == BurstMode::Off) { + pev->button |= IN_ATTACK2; + } + else if (m_currentWeapon == Weapon::Famas && distance <= 400.0f && m_weaponBurstMode == BurstMode::On) { + pev->button |= IN_ATTACK2; + } +} + +void Bot::checkSilencer () { + if ((m_currentWeapon == Weapon::USP || m_currentWeapon == Weapon::M4A1) && !hasShield () && game.isNullEntity (m_enemy)) { + int prob = (m_personality == Personality::Rusher ? 35 : 65); + + // aggressive bots don't like the silencer + if (rg.chance (m_currentWeapon == Weapon::USP ? prob / 2 : prob)) { + // is the silencer not attached... + if (pev->weaponanim > 6) { + pev->button |= IN_ATTACK2; // attach the silencer + } + } + else { + + // is the silencer attached... + if (pev->weaponanim <= 6) { + pev->button |= IN_ATTACK2; // detach the silencer + } + } + } +} diff --git a/src/manager.cpp b/src/manager.cpp index f73086c..5817331 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -1304,7 +1304,6 @@ void Bot::newRound () { m_isLeader = false; m_hasProgressBar = false; m_canChooseAimDirection = true; - m_switchedToKnifeDuringJump = false; m_preventFlashing = 0.0f; m_timeTeamOrder = 0.0f; diff --git a/src/navigate.cpp b/src/navigate.cpp index 65a0323..2e23d39 100644 --- a/src/navigate.cpp +++ b/src/navigate.cpp @@ -840,7 +840,34 @@ bool Bot::updateNavigation () { // pressing the jump button gives the illusion of the bot actual jumping. if (isOnFloor () || isOnLadder ()) { if (m_desiredVelocity.length2d () > 0.0f) { - pev->velocity = m_desiredVelocity + m_desiredVelocity * 0.05f; + pev->velocity = m_desiredVelocity; + } + else { + auto feet = pev->origin + pev->mins; + auto node = Vector { m_pathOrigin.x, m_pathOrigin.y, m_pathOrigin.z - ((m_pathFlags & NodeFlag::Crouch) ? 18.0f : 36.0f) }; + + if (feet.z > feet.z) { + feet = pev->origin + pev->maxs; + } + feet = { pev->origin.x, pev->origin.y, feet.z }; + + // calculate like we do with grenades + auto velocity = calcThrow (feet, node); + + if (velocity.lengthSq () < 100.0f) { + velocity = calcToss (feet, node); + } + velocity = velocity + velocity * 0.45f; + + // set the bot "grenade" velocity + if (velocity.length2d () > 0.0f) { + pev->velocity = velocity; + pev->velocity.z = 0.0f; + } + else { + pev->velocity = pev->velocity + pev->velocity * m_frameInterval * 2.0f; + pev->velocity.z = 0.0f; + } } pev->button |= IN_JUMP; @@ -849,18 +876,11 @@ bool Bot::updateNavigation () { m_desiredVelocity = nullptr; } } - } - else if (m_jumpDistance > 0.0f && !isKnifeMode () && m_switchedToKnifeDuringJump && isOnFloor ()) { - selectBestWeapon (); - - // if jump distance was big enough, cooldown a little - if (m_jumpDistance > 180.0f) { - startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + 0.45f, false); + else if (!cv_jasonmode.bool_ () && usesKnife () && isOnFloor ()) { + selectBestWeapon (); } - m_jumpDistance = 0.0f; - m_switchedToKnifeDuringJump = false; } - + if (m_pathFlags & NodeFlag::Ladder) { if (!m_pathWalk.empty ()) { if (m_pathWalk.hasNext ()) { @@ -2194,9 +2214,10 @@ bool Bot::advanceMovement () { break; } } + // check if bot is going to jump bool willJump = false; - m_jumpDistance = 0.0f; + float jumpDistance = 0.0f; Vector src; Vector dst; @@ -2213,7 +2234,7 @@ bool Bot::advanceMovement () { src = path.origin; dst = next.origin; - m_jumpDistance = path.origin.distance (next.origin); + jumpDistance = src.distance (dst); willJump = true; break; @@ -2222,11 +2243,8 @@ bool Bot::advanceMovement () { } // is there a jump node right ahead and do we need to draw out the light weapon ? - if (willJump && !usesKnife () && m_currentWeapon != Weapon::Scout && !m_isReloading && !usesPistol () && (m_jumpDistance > 175.0f || (dst.z - 32.0f > src.z && m_jumpDistance > 135.0f)) && !(m_states & Sense::SeeingEnemy)) { + if (willJump && !usesKnife () && m_currentWeapon != Weapon::Scout && !m_isReloading && !usesPistol () && (jumpDistance > 145.0f || (dst.z - 32.0f > src.z && jumpDistance > 125.0f)) && !(m_states & Sense::SeeingEnemy)) { selectWeaponById (Weapon::Knife); // draw out the knife if we needed - - // mark as switched - m_switchedToKnifeDuringJump = true; } // bot not already on ladder but will be soon? @@ -2875,152 +2893,6 @@ int Bot::getRandomCampDir () { return graph.random (); } -void Bot::updateBodyAngles () { - // set the body angles to point the gun correctly - pev->angles.x = -pev->v_angle.x * (1.0f / 3.0f); - pev->angles.y = pev->v_angle.y; - - pev->angles.clampAngles (); - - // calculate frustum plane data here, since lookangles update functions call this last one - calculateFrustum (); -} - -void Bot::updateLookAngles () { - - const float delta = cr::clamp (game.time () - m_lookUpdateTime, cr::kFloatEqualEpsilon, 1.0f / 30.0f); - m_lookUpdateTime = game.time (); - - // adjust all body and view angles to face an absolute vector - Vector direction = (m_lookAt - getEyesPos ()).angles (); - direction.x = -direction.x; // invert for engine - - direction.clampAngles (); - - // lower skilled bot's have lower aiming - if (m_difficulty == Difficulty::Noob) { - updateLookAnglesNewbie (direction, delta); - updateBodyAngles (); - - return; - } - - float accelerate = 3000.0f; - float stiffness = 200.0f; - float damping = 25.0f; - - if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_wantsToFire) && m_difficulty > Difficulty::Normal) { - if (m_difficulty == Difficulty::Expert) { - accelerate += 600.0f; - } - stiffness += 100.0f; - damping -= 5.0f; - } - m_idealAngles = pev->v_angle; - - const float angleDiffPitch = cr::anglesDifference (direction.x, m_idealAngles.x); - const float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); - - if (angleDiffYaw < 1.0f && angleDiffYaw > -1.0f) { - m_lookYawVel = 0.0f; - m_idealAngles.y = direction.y; - } - else { - float accel = cr::clamp (stiffness * angleDiffYaw - damping * m_lookYawVel, -accelerate, accelerate); - - m_lookYawVel += delta * accel; - m_idealAngles.y += delta * m_lookYawVel; - } - float accel = cr::clamp (2.0f * stiffness * angleDiffPitch - damping * m_lookPitchVel, -accelerate, accelerate); - - m_lookPitchVel += delta * accel; - m_idealAngles.x += cr::clamp (delta * m_lookPitchVel, -89.0f, 89.0f); - - pev->v_angle = m_idealAngles; - pev->v_angle.clampAngles (); - - updateBodyAngles (); -} - -void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) { - Vector spring { 13.0f, 13.0f, 0.0f }; - Vector damperCoefficient { 0.22f, 0.22f, 0.0f }; - - const float offset = cr::clamp (static_cast (m_difficulty), 1.0f, 4.0f) * 25.0f; - - Vector influence = Vector (0.25f, 0.17f, 0.0f) * (100.0f - offset) / 100.f; - Vector randomization = Vector (2.0f, 0.18f, 0.0f) * (100.0f - offset) / 100.f; - - const float noTargetRatio = 0.3f; - const float offsetDelay = 1.2f; - - Vector stiffness; - Vector randomize; - - m_idealAngles = direction.get2d (); - m_idealAngles.clampAngles (); - - if (m_aimFlags & (AimFlags::Enemy | AimFlags::Entity)) { - m_playerTargetTime = game.time (); - m_randomizedIdealAngles = m_idealAngles; - - stiffness = spring * (0.2f + offset / 125.0f); - } - else { - // is it time for bot to randomize the aim direction again (more often where moving) ? - if (m_randomizeAnglesTime < game.time () && ((pev->velocity.length () > 1.0f && m_angularDeviation.length () < 5.0f) || m_angularDeviation.length () < 1.0f)) { - // is the bot standing still ? - if (pev->velocity.length () < 1.0f) { - randomize = randomization * 0.2f; // randomize less - } - else { - randomize = randomization; - } - // randomize targeted location bit (slightly towards the ground) - m_randomizedIdealAngles = m_idealAngles + Vector (rg.get (-randomize.x * 0.5f, randomize.x * 1.5f), rg.get (-randomize.y, randomize.y), 0.0f); - - // set next time to do this - m_randomizeAnglesTime = game.time () + rg.get (0.4f, offsetDelay); - } - float stiffnessMultiplier = noTargetRatio; - - // take in account whether the bot was targeting someone in the last N seconds - if (game.time () - (m_playerTargetTime + offsetDelay) < noTargetRatio * 10.0f) { - stiffnessMultiplier = 1.0f - (game.time () - m_timeLastFired) * 0.1f; - - // don't allow that stiffness multiplier less than zero - if (stiffnessMultiplier < 0.0f) { - stiffnessMultiplier = 0.5f; - } - } - - // also take in account the remaining deviation (slow down the aiming in the last 10°) - stiffnessMultiplier *= m_angularDeviation.length () * 0.1f * 0.5f; - - // but don't allow getting below a certain value - if (stiffnessMultiplier < 0.35f) { - stiffnessMultiplier = 0.35f; - } - stiffness = spring * stiffnessMultiplier; // increasingly slow aim - } - // compute randomized angle deviation this time - m_angularDeviation = m_randomizedIdealAngles - pev->v_angle; - m_angularDeviation.clampAngles (); - - // spring/damper model aiming - m_aimSpeed.x = stiffness.x * m_angularDeviation.x - damperCoefficient.x * m_aimSpeed.x; - m_aimSpeed.y = stiffness.y * m_angularDeviation.y - damperCoefficient.y * m_aimSpeed.y; - - // influence of y movement on x axis and vice versa (less influence than x on y since it's - // easier and more natural for the bot to "move its mouse" horizontally than vertically) - m_aimSpeed.x += cr::clamp (m_aimSpeed.y * influence.y, -50.0f, 50.0f); - m_aimSpeed.y += cr::clamp (m_aimSpeed.x * influence.x, -200.0f, 200.0f); - - // move the aim cursor - pev->v_angle = pev->v_angle + delta * Vector (m_aimSpeed.x, m_aimSpeed.y, 0.0f); - pev->v_angle.clampAngles (); -} - void Bot::setStrafeSpeed (const Vector &moveDir, float strafeSpeed) { const Vector &los = (moveDir - pev->origin).normalize2d (); float dot = los | pev->angles.forward ().get2d (); diff --git a/src/tasks.cpp b/src/tasks.cpp new file mode 100644 index 0000000..c48ecae --- /dev/null +++ b/src/tasks.cpp @@ -0,0 +1,1680 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#include + +ConVar cv_walking_allowed ("yb_walking_allowed", "1", "Specifies whether bots able to use 'shift' if they thinks that enemy is near."); +ConVar cv_camping_allowed ("yb_camping_allowed", "1", "Allows or disallows bots to camp. Doesn't affects bomb/hostage defending tasks."); + +ConVar cv_camping_time_min ("yb_camping_time_min", "15.0", "Lower bound of time from which time for camping is calculated", true, 5.0f, 90.0f); +ConVar cv_camping_time_max ("yb_camping_time_max", "45.0", "Upper bound of time until which time for camping is calculated", true, 15.0f, 120.0f); + +void Bot::normal_ () { + m_aimFlags |= AimFlags::Nav; + + int debugGoal = cv_debug_goal.int_ (); + + // user forced a waypoint as a goal? + if (debugGoal != kInvalidNodeIndex && getTask ()->data != debugGoal) { + clearSearchNodes (); + + getTask ()->data = debugGoal; + m_chosenGoalIndex = debugGoal; + } + + // bots rushing with knife, when have no enemy (thanks for idea to nicebot project) + if (cv_random_knife_attacks.bool_ () && usesKnife () && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !m_hasHostage && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) { + if (rg.chance (40)) { + pev->button |= IN_ATTACK; + } + else { + pev->button |= IN_ATTACK2; + } + m_knifeAttackTime = game.time () + rg.get (2.5f, 6.0f); + } + const auto &prop = conf.getWeaponProp (m_currentWeapon); + + if (m_reloadState == Reload::None && getAmmo () != 0 && getAmmoInClip () < 5 && prop.ammo1 != -1) { + m_reloadState = Reload::Primary; + } + + // if bomb planted and it's a CT calculate new path to bomb point if he's not already heading for + if (!m_bombSearchOverridden && bots.isBombPlanted () && m_team == Team::CT && getTask ()->data != kInvalidNodeIndex && !(graph[getTask ()->data].flags & NodeFlag::Goal) && getCurrentTaskId () != Task::EscapeFromBomb) { + clearSearchNodes (); + getTask ()->data = kInvalidNodeIndex; + } + + // reached the destination (goal) waypoint? + if (updateNavigation ()) { + // if we're reached the goal, and there is not enemies, notify the team + if (!bots.isBombPlanted () && m_currentNodeIndex != kInvalidNodeIndex && (m_pathFlags & NodeFlag::Goal) && rg.chance (15) && numEnemiesNear (pev->origin, 650.0f) == 0) { + pushRadioMessage (Radio::SectorClear); + } + + completeTask (); + m_prevGoalIndex = kInvalidNodeIndex; + + // spray logo sometimes if allowed to do so + if (!(m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy)) && m_seeEnemyTime + 5.0f < game.time () && !m_reloadState && m_timeLogoSpray < game.time () && cv_spraypaints.bool_ () && rg.chance (50) && m_moveSpeed > getShiftSpeed () && game.isNullEntity (m_pickupItem)) { + if (!(game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_team == Team::CT)) { + startTask (Task::Spraypaint, TaskPri::Spraypaint, kInvalidNodeIndex, game.time () + 1.0f, false); + } + } + + // reached waypoint is a camp waypoint + if ((m_pathFlags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && cv_camping_allowed.bool_ () && !isKnifeMode ()) { + + // check if bot has got a primary weapon and hasn't camped before + if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !m_hasHostage) { + bool campingAllowed = true; + + // Check if it's not allowed for this team to camp here + if (m_team == Team::Terrorist) { + if (m_pathFlags & NodeFlag::CTOnly) { + campingAllowed = false; + } + } + else { + if (m_pathFlags & NodeFlag::TerroristOnly) { + campingAllowed = false; + } + } + + // don't allow vip on as_ maps to camp + don't allow terrorist carrying c4 to camp + if (campingAllowed && (m_isVIP || (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && !bots.isBombPlanted () && m_hasC4))) { + campingAllowed = false; + } + + // check if another bot is already camping here + if (campingAllowed && isOccupiedNode (m_currentNodeIndex)) { + campingAllowed = false; + } + + if (campingAllowed) { + // crouched camping here? + if (m_pathFlags & NodeFlag::Crouch) { + m_campButtons = IN_DUCK; + } + else { + m_campButtons = 0; + } + selectBestWeapon (); + + if (!(m_states & (Sense::SeeingEnemy | Sense::HearingEnemy)) && !m_reloadState) { + m_reloadState = Reload::Primary; + } + m_timeCamping = game.time () + rg.get (cv_camping_time_min.float_ (), cv_camping_time_max.float_ ()); + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, m_timeCamping, true); + + m_lookAtSafe = m_pathOrigin + m_path->start.forward () * 500.0f; + m_aimFlags |= AimFlags::Camp; + m_campDirection = 0; + + // tell the world we're camping + if (rg.chance (25)) { + pushRadioMessage (Radio::ImInPosition); + } + m_moveToGoal = false; + m_checkTerrain = false; + + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + } + } + } + else { + // some goal waypoints are map dependant so check it out... + if (game.mapIs (MapFlags::HostageRescue)) { + // CT Bot has some hostages following? + if (m_team == Team::CT && m_hasHostage) { + // and reached a rescue point? + if (m_pathFlags & NodeFlag::Rescue) { + m_hostages.clear (); + } + } + else if (m_team == Team::Terrorist && rg.chance (75)) { + int index = findDefendNode (m_path->origin); + + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.get (60.0f, 120.0f), true); // push camp task on to stack + startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.get (5.0f, 10.0f), true); // push move command + + // decide to duck or not to duck + selectCampButtons (index); + pushChatterMessage (Chatter::GoingToGuardVIPSafety); // play info about that + } + } + else if (game.mapIs (MapFlags::Demolition) && ((m_pathFlags & NodeFlag::Goal) || m_inBombZone)) { + // is it a terrorist carrying the bomb? + if (m_hasC4) { + if ((m_states & Sense::SeeingEnemy) && numFriendsNear (pev->origin, 768.0f) == 0) { + // request an help also + pushRadioMessage (Radio::NeedBackup); + pushChatterMessage (Chatter::ScaredEmotion); + + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.get (4.0f, 8.0f), true); + } + else { + startTask (Task::PlantBomb, TaskPri::PlantBomb, kInvalidNodeIndex, 0.0f, false); + } + } + else if (m_team == Team::CT) { + if (!bots.isBombPlanted () && numFriendsNear (pev->origin, 210.0f) < 4) { + int index = findDefendNode (m_path->origin); + float campTime = rg.get (25.0f, 40.f); + + // rusher bots don't like to camp too much + if (m_personality == Personality::Rusher) { + campTime *= 0.5f; + } + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + campTime, true); // push camp task on to stack + startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.get (5.0f, 11.0f), true); // push move command + + // decide to duck or not to duck + selectCampButtons (index); + + pushChatterMessage (Chatter::DefendingBombsite); // play info about that + } + } + } + } + } + // no more nodes to follow - search new ones (or we have a bomb) + else if (!hasActiveGoal ()) { + m_moveSpeed = pev->maxspeed; + + clearSearchNodes (); + ignoreCollision (); + + // did we already decide about a goal before? + auto currIndex = getTask ()->data; + auto destIndex = graph.exists (currIndex) && !isBannedNode (currIndex) && m_prevGoalIndex != currIndex ? currIndex : findBestGoal (); + + // check for existance (this is failover, for i.e. csdm, this should be not true with normal gameplay, only when spawned outside of waypointed area) + if (!graph.exists (destIndex)) { + destIndex = graph.getFarest (pev->origin, 512.0f); + } + + if (!graph.exists (cv_debug_goal.int_ ()) && graph.exists (currIndex) && m_prevGoalIndex == currIndex && !(graph[currIndex].flags & NodeFlag::Goal)) { + m_nodeHistory.push (currIndex); + } + m_prevGoalIndex = destIndex; + + // remember index + getTask ()->data = destIndex; + + auto pathSearchType = m_pathType; + + // override with fast path + if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) { + pathSearchType = rg.chance (60) ? FindPath::Fast : FindPath::Optimal; + } + ensureCurrentNodeIndex (); + + // do pathfinding if it's not the current waypoint + if (destIndex != m_currentNodeIndex) { + findPath (m_currentNodeIndex, destIndex, pathSearchType); + } + } + else { + if (!isDucking () && !usesKnife () && !cr::fequal (m_minSpeed, pev->maxspeed) && m_minSpeed > 1.0f) { + m_moveSpeed = m_minSpeed; + } + } + float shiftSpeed = getShiftSpeed (); + + if (!m_isStuck && (!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (cv_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty >= Difficulty::Normal && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !isKnifeMode () && !bots.isBombPlanted ()) { + m_moveSpeed = shiftSpeed; + } + + // bot hasn't seen anything in a long time and is asking his teammates to report in + if (cv_radio_mode.int_ () > 1 && bots.getLastRadio (m_team) != Radio::ReportInTeam && bots.getRoundStartTime () + 20.0f < game.time () && m_askCheckTime < game.time () && rg.chance (15) && m_seeEnemyTime + rg.get (45.0f, 80.0f) < game.time () && numFriendsNear (pev->origin, 1024.0f) == 0) { + pushRadioMessage (Radio::ReportInTeam); + + m_askCheckTime = game.time () + rg.get (45.0f, 80.0f); + + // make sure everyone else will not ask next few moments + for (const auto &bot : bots) { + if (bot->m_notKilled) { + bot->m_askCheckTime = game.time () + rg.get (5.0f, 30.0f); + } + } + } +} + +void Bot::spraypaint_ () { + m_aimFlags |= AimFlags::Entity; + + // bot didn't spray this round? + if (m_timeLogoSpray < game.time () && getTask ()->time > game.time ()) { + const auto &forward = pev->v_angle.forward (); + Vector sprayOrigin = getEyesPos () + forward * 128.0f; + + TraceResult tr {}; + game.testLine (getEyesPos (), sprayOrigin, TraceIgnore::Monsters, ent (), &tr); + + // no wall in front? + if (tr.flFraction >= 1.0f) { + sprayOrigin.z -= 128.0f; + } + m_entity = sprayOrigin; + + if (getTask ()->time - 0.5f < game.time ()) { + // emit spraycan sound + engfuncs.pfnEmitSound (ent (), CHAN_VOICE, "player/sprayer.wav", 1.0f, ATTN_NORM, 0, 100); + game.testLine (getEyesPos (), getEyesPos () + forward * 128.0f, TraceIgnore::Monsters, ent (), &tr); + + // paint the actual logo decal + util.traceDecals (pev, &tr, m_logotypeIndex); + m_timeLogoSpray = game.time () + rg.get (60.0f, 90.0f); + } + } + else { + completeTask (); + } + m_moveToGoal = false; + m_checkTerrain = false; + + m_navTimeset = game.time (); + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + ignoreCollision (); +} + +void Bot::huntEnemy_ () { + m_aimFlags |= AimFlags::Nav; + + // if we've got new enemy... + if (!game.isNullEntity (m_enemy) || game.isNullEntity (m_lastEnemy)) { + + // forget about it... + clearTask (Task::Hunt); + m_prevGoalIndex = kInvalidNodeIndex; + } + else if (game.getTeam (m_lastEnemy) == m_team) { + + // don't hunt down our teammate... + clearTask (Task::Hunt); + + m_prevGoalIndex = kInvalidNodeIndex; + m_lastEnemy = nullptr; + } + else if (updateNavigation ()) // reached last enemy pos? + { + // forget about it... + completeTask (); + + m_prevGoalIndex = kInvalidNodeIndex; + m_lastEnemyOrigin = nullptr; + } + + // do we need to calculate a new path? + else if (!hasActiveGoal ()) { + clearSearchNodes (); + + int destIndex = kInvalidNodeIndex; + int goal = getTask ()->data; + + // is there a remembered index? + if (graph.exists (goal)) { + destIndex = goal; + } + + // find new one instead + else { + destIndex = graph.getNearest (m_lastEnemyOrigin); + } + + // remember index + m_prevGoalIndex = destIndex; + getTask ()->data = destIndex; + + if (destIndex != m_currentNodeIndex) { + findPath (m_currentNodeIndex, destIndex, FindPath::Fast); + } + } + + // bots skill higher than 60? + if (cv_walking_allowed.bool_ () && mp_footsteps.bool_ () && m_difficulty >= Difficulty::Normal && !isKnifeMode ()) { + // then make him move slow if near enemy + if (!(m_currentTravelFlags & PathFlag::Jump)) { + if (m_currentNodeIndex != kInvalidNodeIndex) { + if (m_path->radius < 32.0f && !isOnLadder () && !isInWater () && m_seeEnemyTime + 4.0f > game.time ()) { + m_moveSpeed = getShiftSpeed (); + } + } + } + } +} + +void Bot::seekCover_ () { + m_aimFlags |= AimFlags::Nav; + + if (!util.isAlive (m_lastEnemy)) { + completeTask (); + m_prevGoalIndex = kInvalidNodeIndex; + } + + // reached final waypoint? + else if (updateNavigation ()) { + // yep. activate hide behaviour + completeTask (); + m_prevGoalIndex = kInvalidNodeIndex; + + // start hide task + startTask (Task::Hide, TaskPri::Hide, kInvalidNodeIndex, game.time () + rg.get (3.0f, 12.0f), false); + + // get a valid look direction + const Vector &dest = getCampDirection (m_lastEnemyOrigin); + + m_aimFlags |= AimFlags::Camp; + m_lookAtSafe = dest; + m_campDirection = 0; + + // chosen waypoint is a camp waypoint? + if (m_pathFlags & NodeFlag::Camp) { + // use the existing camp node prefs + if (m_pathFlags & NodeFlag::Crouch) { + m_campButtons = IN_DUCK; + } + else { + m_campButtons = 0; + } + } + else { + // choose a crouch or stand pos + if (m_path->vis.crouch <= m_path->vis.stand) { + m_campButtons = IN_DUCK; + } + else { + m_campButtons = 0; + } + + // enter look direction from previously calculated positions + if (!dest.empty ()) { + m_lookAtSafe = dest; + } + } + + if (m_reloadState == Reload::None && getAmmoInClip () < 5 && getAmmo () != 0) { + m_reloadState = Reload::Primary; + } + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + m_moveToGoal = false; + m_checkTerrain = false; + } + else if (!hasActiveGoal ()) { + clearSearchNodes (); + int destIndex = kInvalidNodeIndex; + + if (getTask ()->data != kInvalidNodeIndex) { + destIndex = getTask ()->data; + } + else { + destIndex = findCoverNode (usesSniper () ? 256.0f : 512.0f); + + if (destIndex == kInvalidNodeIndex) { + m_retreatTime = game.time () + rg.get (5.0f, 10.0f); + m_prevGoalIndex = kInvalidNodeIndex; + + completeTask (); + return; + } + } + m_campDirection = 0; + + m_prevGoalIndex = destIndex; + getTask ()->data = destIndex; + + ensureCurrentNodeIndex (); + + if (destIndex != m_currentNodeIndex) { + findPath (m_currentNodeIndex, destIndex, FindPath::Fast); + } + } +} + +void Bot::attackEnemy_ () { + m_moveToGoal = false; + m_checkTerrain = false; + + if (!game.isNullEntity (m_enemy)) { + ignoreCollision (); + attackMovement (); + + if (usesKnife () && !m_enemyOrigin.empty ()) { + m_destOrigin = m_enemyOrigin; + } + } + else { + completeTask (); + m_destOrigin = m_lastEnemyOrigin; + } + m_navTimeset = game.time (); +} + +void Bot::pause_ () { + m_moveToGoal = false; + m_checkTerrain = false; + + m_navTimeset = game.time (); + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + m_aimFlags |= AimFlags::Nav; + + // is bot blinded and above average difficulty? + if (m_viewDistance < 500.0f && m_difficulty >= Difficulty::Normal) { + // go mad! + m_moveSpeed = -cr::abs ((m_viewDistance - 500.0f) * 0.5f); + + if (m_moveSpeed < -pev->maxspeed) { + m_moveSpeed = -pev->maxspeed; + } + m_lookAtSafe = getEyesPos () + pev->v_angle.forward () * 500.0f; + + m_aimFlags |= AimFlags::Override; + m_wantsToFire = true; + } + else { + pev->button |= m_campButtons; + } + + // stop camping if time over or gets hurt by something else than bullets + if (getTask ()->time < game.time () || m_lastDamageType > 0) { + completeTask (); + } +} + +void Bot::blind_ () { + m_moveToGoal = false; + m_checkTerrain = false; + m_navTimeset = game.time (); + + // if bot remembers last enemy position + if (m_difficulty >= Difficulty::Normal && !m_lastEnemyOrigin.empty () && util.isPlayer (m_lastEnemy) && !usesSniper ()) { + m_lookAt = m_lastEnemyOrigin; // face last enemy + m_wantsToFire = true; // and shoot it + } + + if (m_difficulty >= Difficulty::Normal && graph.exists (m_blindNodeIndex)) { + if (updateNavigation ()) { + if (m_blindTime >= game.time ()) { + completeTask (); + } + m_prevGoalIndex = kInvalidNodeIndex; + m_blindNodeIndex = kInvalidNodeIndex; + + m_blindMoveSpeed = 0.0f; + m_blindSidemoveSpeed = 0.0f; + m_blindButton = 0; + + m_states |= Sense::SuspectEnemy; + } + else if (!hasActiveGoal ()) { + clearSearchNodes (); + ensureCurrentNodeIndex (); + + m_prevGoalIndex = m_blindNodeIndex; + getTask ()->data = m_blindNodeIndex; + + findPath (m_currentNodeIndex, m_blindNodeIndex, FindPath::Fast); + } + } + else { + m_moveSpeed = m_blindMoveSpeed; + m_strafeSpeed = m_blindSidemoveSpeed; + pev->button |= m_blindButton; + + if (m_states & Sense::SuspectEnemy) { + m_states |= Sense::SuspectEnemy; + } + } + + if (m_blindTime < game.time ()) { + completeTask (); + } +} + +void Bot::camp_ () { + if (!cv_camping_allowed.bool_ () || m_isCreature) { + completeTask (); + return; + } + + m_aimFlags |= AimFlags::Camp; + m_checkTerrain = false; + m_moveToGoal = false; + + if (m_team == Team::CT && bots.isBombPlanted () && m_defendedBomb && !isBombDefusing (graph.getBombOrigin ()) && !isOutOfBombTimer ()) { + m_defendedBomb = false; + completeTask (); + } + ignoreCollision (); + + // half the reaction time if camping because you're more aware of enemies if camping + setIdealReactionTimers (); + m_idealReactionTime *= 0.5f; + + m_navTimeset = game.time (); + m_timeCamping = game.time (); + + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + findValidNode (); + + // random camp dir, or prediction + auto useRandomCampDirOrPredictEnemy = [&] () { + if (game.isNullEntity (m_lastEnemy) || !m_lastEnemyOrigin.empty ()) { + auto pathLength = 0; + auto lastEnemyNearestIndex = findAimingNode (m_lastEnemyOrigin, pathLength); + + if (pathLength > 1 && graph.exists (lastEnemyNearestIndex)) { + m_lookAtSafe = graph[lastEnemyNearestIndex].origin; + } + } + else { + m_lookAtSafe = graph[getRandomCampDir ()].origin + pev->view_ofs; + } + }; + + if (m_nextCampDirTime < game.time ()) { + m_nextCampDirTime = game.time () + rg.get (2.0f, 5.0f); + + if (m_pathFlags & NodeFlag::Camp) { + Vector dest; + + // switch from 1 direction to the other + if (m_campDirection < 1) { + dest = m_path->start; + m_campDirection ^= 1; + } + else { + dest = m_path->end; + m_campDirection ^= 1; + } + dest.z = 0.0f; + + // check if after the conversion camp start and camp end are broken, and bot will look into the wall + TraceResult tr {}; + + // and use real angles to check it + auto to = m_pathOrigin + dest.forward () * 500.0f; + + // let's check the destination + game.testLine (getEyesPos (), to, TraceIgnore::Monsters, ent (), &tr); + + // we're probably facing the wall, so ignore the flags provided by graph, and use our own + if (tr.flFraction < 0.5f) { + useRandomCampDirOrPredictEnemy (); + } + else { + m_lookAtSafe = to; + } + } + else { + useRandomCampDirOrPredictEnemy (); + } + } + // press remembered crouch button + pev->button |= m_campButtons; + + // stop camping if time over or gets hurt by something else than bullets + if (getTask ()->time < game.time () || m_lastDamageType > 0) { + completeTask (); + } +} + +void Bot::hide_ () { + if (m_isCreature) { + completeTask (); + return; + }; + + m_aimFlags |= AimFlags::Camp; + m_checkTerrain = false; + m_moveToGoal = false; + + // half the reaction time if camping + setIdealReactionTimers (); + m_idealReactionTime *= 0.5f; + + m_navTimeset = game.time (); + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + findValidNode (); + + if (hasShield () && !m_isReloading) { + if (!isShieldDrawn ()) { + pev->button |= IN_ATTACK2; // draw the shield! + } + else { + pev->button |= IN_DUCK; // duck under if the shield is already drawn + } + } + + // if we see an enemy and aren't at a good camping point leave the spot + if ((m_states & Sense::SeeingEnemy) || m_inBombZone) { + if (!(m_pathFlags & NodeFlag::Camp)) { + completeTask (); + + m_campButtons = 0; + m_prevGoalIndex = kInvalidNodeIndex; + + return; + } + } + + // if we don't have an enemy we're also free to leave + else if (m_lastEnemyOrigin.empty ()) { + completeTask (); + + m_campButtons = 0; + m_prevGoalIndex = kInvalidNodeIndex; + + if (getCurrentTaskId () == Task::Hide) { + completeTask (); + } + return; + } + + pev->button |= m_campButtons; + m_navTimeset = game.time (); + + if (!m_isReloading) { + checkReload (); + } + + // stop camping if time over or gets hurt by something else than bullets + if (getTask ()->time < game.time () || m_lastDamageType > 0) { + completeTask (); + } +} + +void Bot::moveToPos_ () { + m_aimFlags |= AimFlags::Nav; + + if (isShieldDrawn ()) { + pev->button |= IN_ATTACK2; + } + + auto ensureDestIndexOK = [&] (int &index) { + if (isOccupiedNode (index) || isBannedNode (index)) { + m_nodeHistory.push (index); + index = findDefendNode (m_position); + } + }; + + // reached destination? + if (updateNavigation ()) { + completeTask (); // we're done + + m_prevGoalIndex = kInvalidNodeIndex; + m_position = nullptr; + } + + // didn't choose goal waypoint yet? + else if (!hasActiveGoal ()) { + clearSearchNodes (); + + int destIndex = kInvalidNodeIndex; + int goal = getTask ()->data; + + if (graph.exists (goal)) { + destIndex = goal; + + // check if we're ok + ensureDestIndexOK (destIndex); + } + else { + destIndex = graph.getNearest (m_position); + + // check if we're ok + ensureDestIndexOK (destIndex); + } + + if (graph.exists (destIndex)) { + m_prevGoalIndex = destIndex; + getTask ()->data = destIndex; + + ensureCurrentNodeIndex (); + findPath (m_currentNodeIndex, destIndex, m_pathType); + } + else { + completeTask (); + } + } +} + +void Bot::plantBomb_ () { + m_aimFlags |= AimFlags::Camp; + + // we're still got the C4? + if (m_hasC4 && !isKnifeMode ()) { + if (m_currentWeapon != Weapon::C4) { + selectWeaponById (Weapon::C4); + } + + if (util.isAlive (m_enemy) || !m_inBombZone) { + completeTask (); + } + else { + m_moveToGoal = false; + m_checkTerrain = false; + m_navTimeset = game.time (); + + if (m_pathFlags & NodeFlag::Crouch) { + pev->button |= (IN_ATTACK | IN_DUCK); + } + else { + pev->button |= IN_ATTACK; + } + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + } + } + + // done with planting + else { + completeTask (); + + // tell teammates to move over here... + if (numFriendsNear (pev->origin, 1200.0f) != 0) { + pushRadioMessage (Radio::NeedBackup); + } + auto index = findDefendNode (pev->origin); + float guardTime = mp_c4timer.float_ () * 0.5f + mp_c4timer.float_ () * 0.25f; + + // push camp task on to stack + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + guardTime, true); + + // push move command + startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + guardTime, true); + + // decide to duck or not to duck + selectCampButtons (index); + } +} + +void Bot::defuseBomb_ () { + float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f; + float timeToBlowUp = getBombTimeleft (); + float defuseRemainingTime = fullDefuseTime; + + if (m_hasProgressBar /*&& isOnFloor ()*/) { + defuseRemainingTime = fullDefuseTime - game.time (); + } + + const Vector &bombPos = graph.getBombOrigin (); + bool defuseError = false; + + // exception: bomb has been defused + if (bombPos.empty ()) { + // fix for stupid behaviour of CT's when bot is defused + for (const auto &bot : bots) { + if (bot->m_team == m_team && bot->m_notKilled) { + auto defendPoint = graph.getFarest (bot->pev->origin); + + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.get (30.0f, 60.0f), true); // push camp task on to stack + startTask (Task::MoveToPosition, TaskPri::MoveToPosition, defendPoint, game.time () + rg.get (3.0f, 6.0f), true); // push move command + } + } + graph.setBombOrigin (true); + + if (m_numFriendsLeft != 0 && rg.chance (50)) { + if (timeToBlowUp <= 3.0f) { + if (cv_radio_mode.int_ () == 2) { + pushChatterMessage (Chatter::BarelyDefused); + } + else if (cv_radio_mode.int_ () == 1) { + pushRadioMessage (Radio::SectorClear); + } + } + else { + pushRadioMessage (Radio::SectorClear); + } + } + return; + } + else if (defuseRemainingTime > timeToBlowUp) { + defuseError = true; + } + else if (m_states & Sense::SeeingEnemy) { + int friends = numFriendsNear (pev->origin, 768.0f); + + if (friends < 2 && defuseRemainingTime < timeToBlowUp) { + defuseError = true; + + if (defuseRemainingTime + 2.0f > timeToBlowUp) { + defuseError = false; + } + + if (m_numFriendsLeft > friends) { + pushRadioMessage (Radio::NeedBackup); + } + } + } + + // one of exceptions is thrown. finish task. + if (defuseError) { + m_entity = nullptr; + + m_pickupItem = nullptr; + m_pickupType = Pickup::None; + + selectBestWeapon (); + resetCollision (); + + completeTask (); + + return; + } + + // to revert from pause after reload ting && just to be sure + m_moveToGoal = false; + m_checkTerrain = false; + + m_moveSpeed = pev->maxspeed; + m_strafeSpeed = 0.0f; + + // bot is reloading and we close enough to start defusing + if (m_isReloading && bombPos.distance2d (pev->origin) < 80.0f) { + if (m_numEnemiesLeft == 0 || timeToBlowUp < fullDefuseTime + 7.0f || ((getAmmoInClip () > 8 && m_reloadState == Reload::Primary) || (getAmmoInClip () > 5 && m_reloadState == Reload::Secondary))) { + int weaponIndex = bestWeaponCarried (); + + // just select knife and then select weapon + selectWeaponById (Weapon::Knife); + + if (weaponIndex > 0 && weaponIndex < kNumWeapons) { + selectWeaponByIndex (weaponIndex); + } + m_isReloading = false; + } + else { + m_moveToGoal = false; + m_checkTerrain = false; + + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + } + } + + // head to bomb and press use button + m_aimFlags |= AimFlags::Entity; + + m_destOrigin = bombPos; + m_entity = bombPos; + + pev->button |= IN_USE; + + // if defusing is not already started, maybe crouch before + if (!m_hasProgressBar && m_duckDefuseCheckTime < game.time ()) { + Vector botDuckOrigin, botStandOrigin; + + if (pev->button & IN_DUCK) { + botDuckOrigin = pev->origin; + botStandOrigin = pev->origin + Vector (0.0f, 0.0f, 18.0f); + } + else { + botDuckOrigin = pev->origin - Vector (0.0f, 0.0f, 18.0f); + botStandOrigin = pev->origin; + } + + float duckLength = m_entity.distanceSq (botDuckOrigin); + float standLength = m_entity.distanceSq (botStandOrigin); + + if (duckLength > 5625.0f || standLength > 5625.0f) { + if (standLength < duckLength) { + m_duckDefuse = false; // stand + } + else { + m_duckDefuse = m_difficulty >= Difficulty::Normal && m_numEnemiesLeft != 0; // duck + } + } + m_duckDefuseCheckTime = game.time () + 5.0f; + } + + // press duck button + if (m_duckDefuse || (m_oldButtons & IN_DUCK)) { + pev->button |= IN_DUCK; + } + else { + pev->button &= ~IN_DUCK; + } + + // we are defusing bomb + if (m_hasProgressBar || (m_oldButtons & IN_USE) || !game.isNullEntity (m_pickupItem)) { + pev->button |= IN_USE; + + if (!game.isNullEntity (m_pickupItem)) { + MDLL_Use (m_pickupItem, ent ()); + } + + m_reloadState = Reload::None; + m_navTimeset = game.time (); + + // don't move when defusing + m_moveToGoal = false; + m_checkTerrain = false; + + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + // notify team + if (m_numFriendsLeft != 0) { + pushChatterMessage (Chatter::DefusingBomb); + + if (numFriendsNear (pev->origin, 512.0f) < 2) { + pushRadioMessage (Radio::NeedBackup); + } + } + } + else { + completeTask (); + } +} + +void Bot::followUser_ () { + if (game.isNullEntity (m_targetEntity) || !util.isAlive (m_targetEntity)) { + m_targetEntity = nullptr; + completeTask (); + + return; + } + + if (m_targetEntity->v.button & IN_ATTACK) { + TraceResult tr {}; + game.testLine (m_targetEntity->v.origin + m_targetEntity->v.view_ofs, m_targetEntity->v.v_angle.forward () * 500.0f, TraceIgnore::Everything, ent (), &tr); + + if (!game.isNullEntity (tr.pHit) && util.isPlayer (tr.pHit) && game.getTeam (tr.pHit) != m_team) { + m_targetEntity = nullptr; + m_lastEnemy = tr.pHit; + m_lastEnemyOrigin = tr.pHit->v.origin; + + completeTask (); + return; + } + } + + if (!cr::fzero (m_targetEntity->v.maxspeed) && m_targetEntity->v.maxspeed < pev->maxspeed) { + m_moveSpeed = m_targetEntity->v.maxspeed; + } + + if (m_reloadState == Reload::None && getAmmo () != 0) { + m_reloadState = Reload::Primary; + } + + if (m_targetEntity->v.origin.distanceSq (pev->origin) > cr::sqrf (130.0f)) { + m_followWaitTime = 0.0f; + } + else { + m_moveSpeed = 0.0f; + + if (cr::fzero (m_followWaitTime)) { + m_followWaitTime = game.time (); + } + else { + if (m_followWaitTime + 3.0f < game.time ()) { + // stop following if we have been waiting too long + m_targetEntity = nullptr; + + pushRadioMessage (Radio::YouTakeThePoint); + completeTask (); + + return; + } + } + } + m_aimFlags |= AimFlags::Nav; + + if (cv_walking_allowed.bool_ () && m_targetEntity->v.maxspeed < m_moveSpeed && !isKnifeMode ()) { + m_moveSpeed = getShiftSpeed (); + } + + if (isShieldDrawn ()) { + pev->button |= IN_ATTACK2; + } + + // reached destination? + if (updateNavigation ()) { + getTask ()->data = kInvalidNodeIndex; + } + + // didn't choose goal waypoint yet? + if (!hasActiveGoal ()) { + clearSearchNodes (); + + int destIndex = graph.getNearest (m_targetEntity->v.origin); + auto points = graph.getNarestInRadius (200.0f, m_targetEntity->v.origin); + + for (auto &newIndex : points) { + // if waypoint not yet used, assign it as dest + if (newIndex != m_currentNodeIndex && !isOccupiedNode (newIndex)) { + destIndex = newIndex; + } + } + + if (graph.exists (destIndex) && graph.exists (m_currentNodeIndex)) { + m_prevGoalIndex = destIndex; + getTask ()->data = destIndex; + + // always take the shortest path + findPath (m_currentNodeIndex, destIndex, FindPath::Fast); + } + else { + m_targetEntity = nullptr; + completeTask (); + } + } +} + +void Bot::throwExplosive_ () { + Vector dest = m_throw; + + if (!(m_states & Sense::SeeingEnemy)) { + m_strafeSpeed = 0.0f; + m_moveSpeed = 0.0f; + m_moveToGoal = false; + } + else if (!(m_states & Sense::SuspectEnemy) && !game.isNullEntity (m_enemy)) { + dest = m_enemy->v.origin + m_enemy->v.velocity.get2d (); + } + m_isUsingGrenade = true; + m_checkTerrain = false; + + ignoreCollision (); + + if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) { + // heck, I don't wanna blow up myself + m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; + + selectBestWeapon (); + completeTask (); + + return; + } + m_grenade = calcThrow (getEyesPos (), dest); + + if (m_grenade.lengthSq () < 100.0f) { + m_grenade = calcToss (pev->origin, dest); + } + + if (m_grenade.lengthSq () <= 100.0f) { + m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; + + selectBestWeapon (); + completeTask (); + } + else { + m_aimFlags |= AimFlags::Grenade; + + auto grenade = setCorrectGrenadeVelocity ("hegrenade.mdl"); + + if (game.isNullEntity (grenade)) { + if (m_currentWeapon != Weapon::Explosive && !m_grenadeRequested) { + if (pev->weapons & cr::bit (Weapon::Explosive)) { + m_grenadeRequested = true; + selectWeaponById (Weapon::Explosive); + } + else { + m_grenadeRequested = false; + + selectBestWeapon (); + completeTask (); + + return; + } + } + else if (!(m_oldButtons & IN_ATTACK)) { + pev->button |= IN_ATTACK; + m_grenadeRequested = false; + } + } + } + pev->button |= m_campButtons; +} + +void Bot::throwFlashbang_ () { + Vector dest = m_throw; + + if (!(m_states & Sense::SeeingEnemy)) { + m_strafeSpeed = 0.0f; + m_moveSpeed = 0.0f; + m_moveToGoal = false; + } + else if (!(m_states & Sense::SuspectEnemy) && !game.isNullEntity (m_enemy)) { + dest = m_enemy->v.origin + m_enemy->v.velocity.get2d (); + } + + m_isUsingGrenade = true; + m_checkTerrain = false; + + ignoreCollision (); + + if (pev->origin.distanceSq (dest) < cr::sqrf (450.0f)) { + m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; // heck, I don't wanna blow up myself + + selectBestWeapon (); + completeTask (); + + return; + } + m_grenade = calcThrow (getEyesPos (), dest); + + if (m_grenade.lengthSq () < 100.0f) { + m_grenade = calcToss (pev->origin, dest); + } + + if (m_grenade.lengthSq () <= 100.0f) { + m_grenadeCheckTime = game.time () + kGrenadeCheckTime * 2.0f; + + selectBestWeapon (); + completeTask (); + } + else { + m_aimFlags |= AimFlags::Grenade; + + auto grenade = setCorrectGrenadeVelocity ("flashbang.mdl"); + + if (game.isNullEntity (grenade)) { + if (m_currentWeapon != Weapon::Flashbang && !m_grenadeRequested) { + if (pev->weapons & cr::bit (Weapon::Flashbang)) { + m_grenadeRequested = true; + selectWeaponById (Weapon::Flashbang); + } + else { + m_grenadeRequested = false; + + selectBestWeapon (); + completeTask (); + + return; + } + } + else if (!(m_oldButtons & IN_ATTACK)) { + pev->button |= IN_ATTACK; + m_grenadeRequested = false; + } + } + } + pev->button |= m_campButtons; +} + +void Bot::throwSmoke_ () { + if (!(m_states & Sense::SeeingEnemy)) { + m_strafeSpeed = 0.0f; + m_moveSpeed = 0.0f; + m_moveToGoal = false; + } + + m_checkTerrain = false; + m_isUsingGrenade = true; + + ignoreCollision (); + + Vector src = m_lastEnemyOrigin - pev->velocity; + + // predict where the enemy is in secs + if (!game.isNullEntity (m_enemy)) { + src = src + m_enemy->v.velocity; + } + m_grenade = (src - getEyesPos ()).normalize (); + + if (getTask ()->time < game.time ()) { + completeTask (); + return; + } + + if (m_currentWeapon != Weapon::Smoke && !m_grenadeRequested) { + m_aimFlags |= AimFlags::Grenade; + + if (pev->weapons & cr::bit (Weapon::Smoke)) { + m_grenadeRequested = true; + + selectWeaponById (Weapon::Smoke); + getTask ()->time = game.time () + kGrenadeCheckTime * 2.0f; + } + else { + m_grenadeRequested = false; + + selectBestWeapon (); + completeTask (); + + return; + } + } + else if (!(m_oldButtons & IN_ATTACK)) { + pev->button |= IN_ATTACK; + m_grenadeRequested = false; + } + pev->button |= m_campButtons; +} + +void Bot::doublejump_ () { + if (!util.isAlive (m_doubleJumpEntity) || (m_aimFlags & AimFlags::Enemy) || (m_travelStartIndex != kInvalidNodeIndex && getTask ()->time + (graph.calculateTravelTime (pev->maxspeed, graph[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < game.time ())) { + resetDoubleJump (); + return; + } + m_aimFlags |= AimFlags::Nav; + + if (m_jumpReady) { + m_moveToGoal = false; + m_checkTerrain = false; + + m_navTimeset = game.time (); + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + bool inJump = (m_doubleJumpEntity->v.button & IN_JUMP) || (m_doubleJumpEntity->v.oldbuttons & IN_JUMP); + + if (m_duckForJump < game.time ()) { + pev->button |= IN_DUCK; + } + else if (inJump && !(m_oldButtons & IN_JUMP)) { + pev->button |= IN_JUMP; + } + + const auto &src = pev->origin + Vector (0.0f, 0.0f, 45.0f); + const auto &dest = src + Vector (0.0f, pev->angles.y, 0.0f).upward () * 256.0f; + + TraceResult tr {}; + game.testLine (src, dest, TraceIgnore::None, ent (), &tr); + + if (tr.flFraction < 1.0f && tr.pHit == m_doubleJumpEntity && inJump) { + m_duckForJump = game.time () + rg.get (3.0f, 5.0f); + getTask ()->time = game.time (); + } + return; + } + + if (m_currentNodeIndex == m_prevGoalIndex) { + m_pathOrigin = m_doubleJumpOrigin; + m_destOrigin = m_doubleJumpOrigin; + } + + if (updateNavigation ()) { + getTask ()->data = kInvalidNodeIndex; + } + + // didn't choose goal waypoint yet? + if (!hasActiveGoal ()) { + clearSearchNodes (); + + int destIndex = graph.getNearest (m_doubleJumpOrigin); + + if (graph.exists (destIndex)) { + m_prevGoalIndex = destIndex; + m_travelStartIndex = m_currentNodeIndex; + + getTask ()->data = destIndex; + + // always take the shortest path + findPath (m_currentNodeIndex, destIndex, FindPath::Fast); + + if (m_currentNodeIndex == destIndex) { + m_jumpReady = true; + } + } + else { + resetDoubleJump (); + } + } +} + +void Bot::escapeFromBomb_ () { + m_aimFlags |= AimFlags::Nav; + + if (!bots.isBombPlanted ()) { + completeTask (); + } + + if (isShieldDrawn ()) { + pev->button |= IN_ATTACK2; + } + + if (!usesKnife () && m_numEnemiesLeft == 0) { + selectWeaponById (Weapon::Knife); + } + + // reached destination? + if (updateNavigation ()) { + completeTask (); // we're done + + // press duck button if we still have some enemies + if (m_numEnemiesLeft > 0) { + m_campButtons = IN_DUCK; + } + + // we're reached destination point so just sit down and camp + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true); + } + + // didn't choose goal waypoint yet? + else if (!hasActiveGoal ()) { + clearSearchNodes (); + + int bestIndex = kInvalidNodeIndex; + + float safeRadius = rg.get (1513.0f, 2048.0f); + float closestDistance = kInfiniteDistance; + + for (const auto &path : graph) { + if (path.origin.distance (graph.getBombOrigin ()) < safeRadius || isOccupiedNode (path.number)) { + continue; + } + float distance = pev->origin.distance (path.origin); + + if (closestDistance > distance) { + closestDistance = distance; + bestIndex = path.number; + } + } + + if (bestIndex < 0) { + bestIndex = graph.getFarest (pev->origin, safeRadius); + } + + // still no luck? + if (bestIndex < 0) { + completeTask (); // we're done + + // we have no destination point, so just sit down and camp + startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true); + return; + } + m_prevGoalIndex = bestIndex; + getTask ()->data = bestIndex; + + findPath (m_currentNodeIndex, bestIndex, FindPath::Fast); + } +} + +void Bot::shootBreakable_ () { + m_aimFlags |= AimFlags::Override; + + // breakable destroyed? + if (!game.isShootableBreakable (m_breakableEntity)) { + completeTask (); + return; + } + pev->button |= m_campButtons; + + m_checkTerrain = false; + m_moveToGoal = false; + m_navTimeset = game.time (); + m_lookAtSafe = m_breakableOrigin; + + // is bot facing the breakable? + if (util.getShootingCone (ent (), m_breakableOrigin) >= 0.90f) { + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + if (usesKnife ()) { + selectBestWeapon (); + } + m_wantsToFire = true; + m_shootTime = game.time (); + } + else { + m_checkTerrain = true; + m_moveToGoal = true; + + completeTask (); + } +} + +void Bot::pickupItem_ () { + if (game.isNullEntity (m_pickupItem)) { + m_pickupItem = nullptr; + completeTask (); + + return; + } + const Vector &dest = game.getEntityOrigin (m_pickupItem); + + m_destOrigin = dest; + m_entity = dest; + + // find the distance to the item + float itemDistance = dest.distance (pev->origin); + + switch (m_pickupType) { + case Pickup::DroppedC4: + case Pickup::None: + break; + + case Pickup::Weapon: + m_aimFlags |= AimFlags::Nav; + + // near to weapon? + if (itemDistance < 50.0f) { + int index = 0; + auto &info = conf.getWeapons (); + + for (index = 0; index < 7; ++index) { + if (strcmp (info[index].model, m_pickupItem->v.model.chars (9)) == 0) { + break; + } + } + + if (index < 7) { + // secondary weapon. i.e., pistol + int weaponIndex = 0; + + for (index = 0; index < 7; ++index) { + if (pev->weapons & cr::bit (info[index].id)) { + weaponIndex = index; + } + } + + if (weaponIndex > 0) { + selectWeaponByIndex (weaponIndex); + dropCurrentWeapon (); + + if (hasShield ()) { + dropCurrentWeapon (); // discard both shield and pistol + } + } + enteredBuyZone (BuyState::PrimaryWeapon); + } + else { + // primary weapon + int weaponIndex = bestWeaponCarried (); + bool niceWeapon = rateGroundWeapon (m_pickupItem); + + auto tab = conf.getRawWeapons (); + + if ((tab->id == Weapon::Shield || weaponIndex > 6 || hasShield ()) && niceWeapon) { + selectWeaponByIndex (weaponIndex); + dropCurrentWeapon (); + } + + if (!weaponIndex || !niceWeapon) { + m_itemIgnore = m_pickupItem; + m_pickupItem = nullptr; + m_pickupType = Pickup::None; + + break; + } + enteredBuyZone (BuyState::PrimaryWeapon); + } + checkSilencer (); // check the silencer + } + break; + + case Pickup::Shield: + m_aimFlags |= AimFlags::Nav; + + if (hasShield ()) { + m_pickupItem = nullptr; + break; + } + + // near to shield? + else if (itemDistance < 50.0f) { + // get current best weapon to check if it's a primary in need to be dropped + int weaponIndex = bestWeaponCarried (); + + if (weaponIndex > 6) { + selectWeaponByIndex (weaponIndex); + dropCurrentWeapon (); + } + } + break; + + case Pickup::PlantedC4: + m_aimFlags |= AimFlags::Entity; + + if (m_team == Team::CT && itemDistance < 80.0f) { + pushChatterMessage (Chatter::DefusingBomb); + + // notify team of defusing + if (m_numFriendsLeft < 3) { + pushRadioMessage (Radio::NeedBackup); + } + m_moveToGoal = false; + m_checkTerrain = false; + + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + + startTask (Task::DefuseBomb, TaskPri::DefuseBomb, kInvalidNodeIndex, 0.0f, false); + } + break; + + case Pickup::Hostage: + m_aimFlags |= AimFlags::Entity; + + if (!util.isAlive (m_pickupItem)) { + // don't pickup dead hostages + m_pickupItem = nullptr; + completeTask (); + + break; + } + + if (itemDistance < 50.0f) { + float angleToEntity = isInFOV (dest - getEyesPos ()); + + // bot faces hostage? + if (angleToEntity <= 10.0f) { + // use game dll function to make sure the hostage is correctly 'used' + MDLL_Use (m_pickupItem, ent ()); + + if (rg.chance (80)) { + pushChatterMessage (Chatter::UsingHostages); + } + m_hostages.push (m_pickupItem); + m_pickupItem = nullptr; + + completeTask (); + + float minDistance = kInfiniteDistance; + int nearestHostageNodeIndex = kInvalidNodeIndex; + + // find the nearest 'unused' hostage within the area + game.searchEntities (pev->origin, 768.0f, [&] (edict_t *ent) { + auto classname = ent->v.classname.chars (); + + if (strncmp ("hostage_entity", classname, 14) != 0 && strncmp ("monster_scientist", classname, 17) != 0) { + return EntitySearchResult::Continue; + } + + // check if hostage is dead + if (game.isNullEntity (ent) || ent->v.health <= 0) { + return EntitySearchResult::Continue; + } + + // check if hostage is with a bot + for (const auto &other : bots) { + if (other->m_notKilled) { + for (const auto &hostage : other->m_hostages) { + if (hostage == ent) { + return EntitySearchResult::Continue; + } + } + } + } + + // check if hostage is with a human teammate (hack) + for (auto &client : util.getClients ()) { + if ((client.flags & ClientFlags::Used) && !(client.ent->v.flags & FL_FAKECLIENT) && (client.flags & ClientFlags::Alive) && + client.team == m_team && client.ent->v.origin.distanceSq (ent->v.origin) <= cr::sqrf (240.0f)) { + return EntitySearchResult::Continue; + } + } + + int hostageNodeIndex = graph.getNearest (ent->v.origin); + + if (graph.exists (hostageNodeIndex)) { + float distance = graph[hostageNodeIndex].origin.distanceSq (pev->origin); + + if (distance < minDistance) { + minDistance = distance; + nearestHostageNodeIndex = hostageNodeIndex; + } + } + + return EntitySearchResult::Continue; + }); + + if (nearestHostageNodeIndex != kInvalidNodeIndex) { + clearTask (Task::MoveToPosition); // remove any move tasks + startTask (Task::MoveToPosition, TaskPri::MoveToPosition, nearestHostageNodeIndex, 0.0f, true); + } + } + ignoreCollision (); // also don't consider being stuck + } + break; + + case Pickup::DefusalKit: + m_aimFlags |= AimFlags::Nav; + + if (m_hasDefuser) { + m_pickupItem = nullptr; + m_pickupType = Pickup::None; + } + break; + + case Pickup::Button: + m_aimFlags |= AimFlags::Entity; + + if (game.isNullEntity (m_pickupItem) || m_buttonPushTime < game.time ()) { + completeTask (); + m_pickupType = Pickup::None; + + break; + } + + // find angles from bot origin to entity... + float angleToEntity = isInFOV (dest - getEyesPos ()); + + // near to the button? + if (itemDistance < 90.0f) { + m_moveSpeed = 0.0f; + m_strafeSpeed = 0.0f; + m_moveToGoal = false; + m_checkTerrain = false; + + // facing it directly? + if (angleToEntity <= 10.0f) { + MDLL_Use (m_pickupItem, ent ()); + + m_pickupItem = nullptr; + m_pickupType = Pickup::None; + m_buttonPushTime = game.time () + 3.0f; + + completeTask (); + } + } + break; + } +} diff --git a/src/vision.cpp b/src/vision.cpp new file mode 100644 index 0000000..f25dfae --- /dev/null +++ b/src/vision.cpp @@ -0,0 +1,455 @@ +// +// YaPB - Counter-Strike Bot based on PODBot by Markus Klinge. +// Copyright © 2004-2023 YaPB Project . +// +// SPDX-License-Identifier: MIT +// + +#include + +ConVar cv_max_nodes_for_predict ("yb_max_nodes_for_predict", "20", "Maximum number for path length, to predict the enemy.", true, 15.0f, 256.0f); + +// game console variables +ConVar mp_flashlight ("mp_flashlight", nullptr, Var::GameRef); + +float Bot::isInFOV (const Vector &destination) { + const float entityAngle = cr::wrapAngle360 (destination.yaw ()); // find yaw angle from source to destination... + const float viewAngle = cr::wrapAngle360 (pev->v_angle.y); // get bot's current view angle... + + // return the absolute value of angle to destination entity + // zero degrees means straight ahead, 45 degrees to the left or + // 45 degrees to the right is the limit of the normal view angle + float absoluteAngle = cr::abs (viewAngle - entityAngle); + + if (absoluteAngle > 180.0f) { + absoluteAngle = 360.0f - absoluteAngle; + } + return absoluteAngle; +} + +bool Bot::isInViewCone (const Vector &origin) { + // this function returns true if the spatial vector location origin is located inside + // the field of view cone of the bot entity, false otherwise. It is assumed that entities + // have a human-like field of view, that is, about 90 degrees. + + return util.isInViewCone (origin, ent ()); +} + +bool Bot::seesItem (const Vector &destination, const char *classname) { + TraceResult tr {}; + + // trace a line from bot's eyes to destination.. + game.testLine (getEyesPos (), destination, TraceIgnore::None, ent (), &tr); + + // check if line of sight to object is not blocked (i.e. visible) + if (tr.flFraction < 1.0f && tr.pHit && !tr.fStartSolid) { + return strcmp (tr.pHit->v.classname.chars (), classname) == 0; + } + return true; +} + +bool Bot::seesEntity (const Vector &dest, bool fromBody) { + TraceResult tr {}; + + // trace a line from bot's eyes to destination... + game.testLine (fromBody ? pev->origin : getEyesPos (), dest, TraceIgnore::Everything, ent (), &tr); + + // check if line of sight to object is not blocked (i.e. visible) + return tr.flFraction >= 1.0f; +} + +void Bot::updateAimDir () { + uint32_t flags = m_aimFlags; + + // don't allow bot to look at danger positions under certain circumstances + if (!(flags & (AimFlags::Grenade | AimFlags::Enemy | AimFlags::Entity))) { + + // check if narrow place and we're duck, do not predict enemies in that situation + const bool duckedInNarrowPlace = isInNarrowPlace () && ((m_pathFlags & NodeFlag::Crouch) || (pev->button & IN_DUCK)); + + if (duckedInNarrowPlace || isOnLadder () || isInWater () || (m_pathFlags & NodeFlag::Ladder) || (m_currentTravelFlags & PathFlag::Jump)) { + flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath); + m_canChooseAimDirection = false; + } + } + + if (flags & AimFlags::Override) { + m_lookAt = m_lookAtSafe; + } + else if (flags & AimFlags::Grenade) { + m_lookAt = m_throw; + + float throwDistance = m_throw.distance (pev->origin); + float coordCorrection = 0.0f; + + if (throwDistance > 100.0f && throwDistance < 800.0f) { + coordCorrection = 0.25f * (m_throw.z - pev->origin.z); + } + else if (throwDistance >= 800.0f) { + float angleCorrection = 37.0f * (throwDistance - 800.0f) / 800.0f; + + if (angleCorrection > 45.0f) { + angleCorrection = 45.0f; + } + coordCorrection = throwDistance * cr::tanf (cr::deg2rad (angleCorrection)) + 0.25f * (m_throw.z - pev->origin.z); + } + m_lookAt.z += coordCorrection * 0.5f; + } + else if (flags & AimFlags::Enemy) { + focusEnemy (); + } + else if (flags & AimFlags::Entity) { + m_lookAt = m_entity; + + // do not look at hostages legs + if (m_pickupType == Pickup::Hostage) { + m_lookAt.z += 48.0f; + } + else if (m_pickupType == Pickup::Weapon) { + m_lookAt.z += 72.0; + } + } + else if (flags & AimFlags::LastEnemy) { + m_lookAt = m_lastEnemyOrigin; + + // did bot just see enemy and is quite aggressive? + if (m_seeEnemyTime + 2.0f - m_actualReactionTime + m_baseAgressionLevel > game.time ()) { + + // feel free to fire if shootable + if (!usesSniper () && lastEnemyShootable ()) { + m_wantsToFire = true; + } + } + } + else if (flags & AimFlags::PredictPath) { + bool changePredictedEnemy = true; + bool predictFailed = false; + + if (m_timeNextTracking < game.time () && m_trackingEdict == m_lastEnemy) { + changePredictedEnemy = false; + } + + auto doFailPredict = [this] () { + m_aimFlags &= ~AimFlags::PredictPath; + m_trackingEdict = nullptr; + }; + + if (changePredictedEnemy) { + int pathLength = m_lastPredictLength; + int predictNode = m_lastPredictIndex; + + if (predictNode != kInvalidNodeIndex) { + TraceResult tr; + game.testLine (getEyesPos (), graph[predictNode].origin, TraceIgnore::Everything, ent (), &tr); + + if (tr.flFraction < 0.2f) { + pathLength = kInfiniteDistanceLong; + } + } + + if (graph.exists (predictNode) && pathLength < cv_max_nodes_for_predict.int_ ()) { + m_lookAt = graph[predictNode].origin; + m_lookAtSafe = m_lookAt; + + m_timeNextTracking = game.time () + 0.25f; + m_trackingEdict = m_lastEnemy; + + // feel free to fire if shootable + if (!usesSniper () && lastEnemyShootable ()) { + m_wantsToFire = true; + } + } + else { + doFailPredict (); + predictFailed = true; + } + } + + if (predictFailed) { + doFailPredict (); + } + else { + m_lookAt = m_lookAtSafe; + } + } + else if (flags & AimFlags::Camp) { + m_lookAt = m_lookAtSafe; + } + else if (flags & AimFlags::Nav) { + m_lookAt = m_destOrigin; + + if (m_moveToGoal && m_seeEnemyTime + 4.0f < game.time () && !m_isStuck && m_moveSpeed > getShiftSpeed () && !(pev->button & IN_DUCK) && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & (NodeFlag::Ladder | NodeFlag::Crouch)) && m_pathWalk.hasNext () && pev->origin.distanceSq (m_destOrigin) < cr::sqrf (512.0f)) { + auto nextPathIndex = m_pathWalk.next (); + + if (vistab.visible (m_currentNodeIndex, nextPathIndex)) { + m_lookAt = graph[nextPathIndex].origin + pev->view_ofs; + } + else { + m_lookAt = m_destOrigin; + } + } + else if (m_seeEnemyTime + 3.0f > game.time () && !m_lastEnemyOrigin.empty ()) { + m_lookAt = m_lastEnemyOrigin; + } + else { + m_lookAt = m_destOrigin; + } + + if (m_canChooseAimDirection && m_seeEnemyTime + 4.0f < game.time () && m_currentNodeIndex != kInvalidNodeIndex && !(m_pathFlags & NodeFlag::Ladder)) { + auto dangerIndex = practice.getIndex (m_team, m_currentNodeIndex, m_currentNodeIndex); + + if (graph.exists (dangerIndex) && vistab.visible (m_currentNodeIndex, dangerIndex) && !(graph[dangerIndex].flags & NodeFlag::Crouch)) { + if (pev->origin.distanceSq (graph[dangerIndex].origin) < cr::sqrf (160.0f)) { + m_lookAt = m_destOrigin; + } + else { + m_lookAt = graph[dangerIndex].origin + pev->view_ofs; + + // add danger flags + m_aimFlags |= AimFlags::Danger; + } + } + } + + // don't look at bottom of node, if reached it + if (m_lookAt == m_destOrigin) { + m_lookAt.z = getEyesPos ().z; + } + } + + if (m_lookAt.empty ()) { + m_lookAt = m_destOrigin; + } +} + +void Bot::checkDarkness () { + + // do not check for darkness at the start of the round + if (m_spawnTime + 5.0f > game.time () || !graph.exists (m_currentNodeIndex)) { + return; + } + + // do not check every frame + if (m_checkDarkTime + 5.0f > game.time () || cr::fzero (m_path->light)) { + return; + } + auto skyColor = illum.getSkyColor (); + auto flashOn = (pev->effects & EF_DIMLIGHT); + + if (mp_flashlight.bool_ () && !m_hasNVG) { + auto task = Task (); + + if (!flashOn && task != Task::Camp && task != Task::Attack && m_heardSoundTime + 3.0f < game.time () && m_flashLevel > 30 && ((skyColor > 50.0f && m_path->light < 10.0f) || (skyColor <= 50.0f && m_path->light < 40.0f))) { + pev->impulse = 100; + } + else if (flashOn && (((m_path->light > 15.0f && skyColor > 50.0f) || (m_path->light > 45.0f && skyColor <= 50.0f)) || task == Task::Camp || task == Task::Attack || m_flashLevel <= 0 || m_heardSoundTime + 3.0f >= game.time ())) { + pev->impulse = 100; + } + } + else if (m_hasNVG) { + if (flashOn) { + pev->impulse = 100; + } + else if (!m_usesNVG && ((skyColor > 50.0f && m_path->light < 15.0f) || (skyColor <= 50.0f && m_path->light < 40.0f))) { + issueCommand ("nightvision"); + } + else if (m_usesNVG && ((m_path->light > 20.0f && skyColor > 50.0f) || (m_path->light > 45.0f && skyColor <= 50.0f))) { + issueCommand ("nightvision"); + } + } + m_checkDarkTime = game.time (); +} + + +void Bot::calculateFrustum () { + // this function updates bot view frustum + + Vector forward, right, up; + pev->v_angle.angleVectors (&forward, &right, &up); + + static Vector fc, nc, fbl, fbr, ftl, ftr, nbl, nbr, ntl, ntr; + + fc = getEyesPos () + forward * frustum.MaxView; + nc = getEyesPos () + forward * frustum.MinView; + + fbl = fc + (up * frustum.farHeight * 0.5f) - (right * frustum.farWidth * 0.5f); + fbr = fc + (up * frustum.farHeight * 0.5f) + (right * frustum.farWidth * 0.5f); + ftl = fc - (up * frustum.farHeight * 0.5f) - (right * frustum.farWidth * 0.5f); + ftr = fc - (up * frustum.farHeight * 0.5f) + (right * frustum.farWidth * 0.5f); + nbl = nc + (up * frustum.nearHeight * 0.5f) - (right * frustum.nearWidth * 0.5f); + nbr = nc + (up * frustum.nearHeight * 0.5f) + (right * frustum.nearWidth * 0.5f); + ntl = nc - (up * frustum.nearHeight * 0.5f) - (right * frustum.nearWidth * 0.5f); + ntr = nc - (up * frustum.nearHeight * 0.5f) + (right * frustum.nearWidth * 0.5f); + + auto setPlane = [&] (FrustumSide side, const Vector &v1, const Vector &v2, const Vector &v3) { + auto &plane = m_frustum[side]; + + plane.normal = ((v2 - v1) ^ (v3 - v1)).normalize (); + plane.point = v2; + + plane.result = -(plane.normal | plane.point); + }; + + setPlane (FrustumSide::Top, ftl, ntl, ntr); + setPlane (FrustumSide::Bottom, fbr, nbr, nbl); + setPlane (FrustumSide::Left, fbl, nbl, ntl); + setPlane (FrustumSide::Right, ftr, ntr, nbr); + setPlane (FrustumSide::Near, nbr, ntr, ntl); + setPlane (FrustumSide::Far, fbl, ftl, ftr); +} + +bool Bot::isEnemyInFrustum (edict_t *enemy) { + const Vector &origin = enemy->v.origin - Vector (0.0f, 0.0f, 5.0f); + + for (auto &plane : m_frustum) { + if (!util.isObjectInsidePlane (plane, origin, 60.0f, 16.0f)) { + return false; + } + } + return true; +} + +void Bot::updateBodyAngles () { + // set the body angles to point the gun correctly + pev->angles.x = -pev->v_angle.x * (1.0f / 3.0f); + pev->angles.y = pev->v_angle.y; + + pev->angles.clampAngles (); + + // calculate frustum plane data here, since look angles update functions call this last one + calculateFrustum (); +} + +void Bot::updateLookAngles () { + const float delta = cr::clamp (game.time () - m_lookUpdateTime, cr::kFloatEqualEpsilon, 1.0f / 30.0f); + m_lookUpdateTime = game.time (); + + // adjust all body and view angles to face an absolute vector + Vector direction = (m_lookAt - getEyesPos ()).angles (); + direction.x = -direction.x; // invert for engine + + direction.clampAngles (); + + // lower skilled bot's have lower aiming + if (m_difficulty == Difficulty::Noob) { + updateLookAnglesNewbie (direction, delta); + updateBodyAngles (); + + return; + } + + float accelerate = 3000.0f; + float stiffness = 200.0f; + float damping = 25.0f; + + if (((m_aimFlags & (AimFlags::Enemy | AimFlags::Entity | AimFlags::Grenade)) || m_wantsToFire) && m_difficulty > Difficulty::Normal) { + if (m_difficulty == Difficulty::Expert) { + accelerate += 600.0f; + } + stiffness += 100.0f; + damping -= 5.0f; + } + m_idealAngles = pev->v_angle; + + const float angleDiffPitch = cr::anglesDifference (direction.x, m_idealAngles.x); + const float angleDiffYaw = cr::anglesDifference (direction.y, m_idealAngles.y); + + if (angleDiffYaw < 1.0f && angleDiffYaw > -1.0f) { + m_lookYawVel = 0.0f; + m_idealAngles.y = direction.y; + } + else { + float accel = cr::clamp (stiffness * angleDiffYaw - damping * m_lookYawVel, -accelerate, accelerate); + + m_lookYawVel += delta * accel; + m_idealAngles.y += delta * m_lookYawVel; + } + float accel = cr::clamp (2.0f * stiffness * angleDiffPitch - damping * m_lookPitchVel, -accelerate, accelerate); + + m_lookPitchVel += delta * accel; + m_idealAngles.x += cr::clamp (delta * m_lookPitchVel, -89.0f, 89.0f); + + pev->v_angle = m_idealAngles; + pev->v_angle.clampAngles (); + + updateBodyAngles (); +} + +void Bot::updateLookAnglesNewbie (const Vector &direction, float delta) { + Vector spring { 13.0f, 13.0f, 0.0f }; + Vector damperCoefficient { 0.22f, 0.22f, 0.0f }; + + const float offset = cr::clamp (static_cast (m_difficulty), 1.0f, 4.0f) * 25.0f; + + Vector influence = Vector (0.25f, 0.17f, 0.0f) * (100.0f - offset) / 100.f; + Vector randomization = Vector (2.0f, 0.18f, 0.0f) * (100.0f - offset) / 100.f; + + const float noTargetRatio = 0.3f; + const float offsetDelay = 1.2f; + + Vector stiffness; + Vector randomize; + + m_idealAngles = direction.get2d (); + m_idealAngles.clampAngles (); + + if (m_aimFlags & (AimFlags::Enemy | AimFlags::Entity)) { + m_playerTargetTime = game.time (); + m_randomizedIdealAngles = m_idealAngles; + + stiffness = spring * (0.2f + offset / 125.0f); + } + else { + // is it time for bot to randomize the aim direction again (more often where moving) ? + if (m_randomizeAnglesTime < game.time () && ((pev->velocity.length () > 1.0f && m_angularDeviation.length () < 5.0f) || m_angularDeviation.length () < 1.0f)) { + // is the bot standing still ? + if (pev->velocity.length () < 1.0f) { + randomize = randomization * 0.2f; // randomize less + } + else { + randomize = randomization; + } + // randomize targeted location bit (slightly towards the ground) + m_randomizedIdealAngles = m_idealAngles + Vector (rg.get (-randomize.x * 0.5f, randomize.x * 1.5f), rg.get (-randomize.y, randomize.y), 0.0f); + + // set next time to do this + m_randomizeAnglesTime = game.time () + rg.get (0.4f, offsetDelay); + } + float stiffnessMultiplier = noTargetRatio; + + // take in account whether the bot was targeting someone in the last N seconds + if (game.time () - (m_playerTargetTime + offsetDelay) < noTargetRatio * 10.0f) { + stiffnessMultiplier = 1.0f - (game.time () - m_timeLastFired) * 0.1f; + + // don't allow that stiffness multiplier less than zero + if (stiffnessMultiplier < 0.0f) { + stiffnessMultiplier = 0.5f; + } + } + + // also take in account the remaining deviation (slow down the aiming in the last 10°) + stiffnessMultiplier *= m_angularDeviation.length () * 0.1f * 0.5f; + + // but don't allow getting below a certain value + if (stiffnessMultiplier < 0.35f) { + stiffnessMultiplier = 0.35f; + } + stiffness = spring * stiffnessMultiplier; // increasingly slow aim + } + // compute randomized angle deviation this time + m_angularDeviation = m_randomizedIdealAngles - pev->v_angle; + m_angularDeviation.clampAngles (); + + // spring/damper model aiming + m_aimSpeed.x = stiffness.x * m_angularDeviation.x - damperCoefficient.x * m_aimSpeed.x; + m_aimSpeed.y = stiffness.y * m_angularDeviation.y - damperCoefficient.y * m_aimSpeed.y; + + // influence of y movement on x axis and vice versa (less influence than x on y since it's + // easier and more natural for the bot to "move its mouse" horizontally than vertically) + m_aimSpeed.x += cr::clamp (m_aimSpeed.y * influence.y, -50.0f, 50.0f); + m_aimSpeed.y += cr::clamp (m_aimSpeed.x * influence.x, -200.0f, 200.0f); + + // move the aim cursor + pev->v_angle = pev->v_angle + delta * Vector (m_aimSpeed.x, m_aimSpeed.y, 0.0f); + pev->v_angle.clampAngles (); +} diff --git a/src/vistable.cpp b/src/vistable.cpp index 2de5726..04cd88f 100644 --- a/src/vistable.cpp +++ b/src/vistable.cpp @@ -7,7 +7,6 @@ #include - void GraphVistable::rebuild () { if (!m_rebuild) { return; diff --git a/vc/yapb.vcxproj b/vc/yapb.vcxproj index 9c9664a..a80ed76 100644 --- a/vc/yapb.vcxproj +++ b/vc/yapb.vcxproj @@ -87,6 +87,8 @@ + +