/* * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at * your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * In addition, as a special exception, the author gives permission to * link the code of this program with the Half-Life Game Engine ("HL * Engine") and Modified Game Libraries ("MODs") developed by Valve, * L.L.C ("Valve"). You must obey the GNU General Public License in all * respects for all of the code used other than the HL Engine and MODs * from Valve. If you modify this file, you may extend this exception * to your version of the file, but you are not obligated to do so. If * you do not wish to do so, delete this exception statement from your * version. * */ #include "precompiled.h" // Lightweight maintenance, invoked frequently void CCSBot::Upkeep() { if (TheCSBots()->IsLearningMap() || !IsAlive()) return; if (m_isRapidFiring) TogglePrimaryAttack(); // aiming must be smooth - update often if (IsAimingAtEnemy()) { UpdateAimOffset(); // aim at enemy, if he's still alive if (m_enemy.IsValid()) { float feetOffset = pev->origin.z - GetFeetZ(); if (IsEnemyVisible()) { if (GetProfile()->GetSkill() > 0.5f) { const float k = 3.0f; m_aimSpot = (m_enemy->pev->velocity - pev->velocity) * g_flBotCommandInterval * k + m_enemy->pev->origin; } else m_aimSpot = m_enemy->pev->origin; bool aimBlocked = false; const float sharpshooter = 0.8f; if (IsUsingAWP() || IsUsingShotgun() || IsUsingMachinegun() || GetProfile()->GetSkill() < sharpshooter || (IsActiveWeaponRecoilHigh() && !IsUsingPistol() && !IsUsingSniperRifle())) { if (IsEnemyPartVisible(CHEST)) { // No headshots, go for the chest. aimBlocked = true; } } if (aimBlocked) m_aimSpot.z -= feetOffset * 0.25f; else if (!IsEnemyPartVisible(HEAD)) { if (IsEnemyPartVisible(CHEST)) { m_aimSpot.z -= feetOffset * 0.5f; } else if (IsEnemyPartVisible(LEFT_SIDE)) { Vector2D to = (m_enemy->pev->origin - pev->origin).Make2D(); to.NormalizeInPlace(); m_aimSpot.x -= to.y * 16.0f; m_aimSpot.y += to.x * 16.0f; m_aimSpot.z -= feetOffset * 0.5f; } else if (IsEnemyPartVisible(RIGHT_SIDE)) { Vector2D to = (m_enemy->pev->origin - pev->origin).Make2D(); to.NormalizeInPlace(); m_aimSpot.x += to.y * 16.0f; m_aimSpot.y -= to.x * 16.0f; m_aimSpot.z -= feetOffset * 0.5f; } else // FEET { m_aimSpot.z -= (feetOffset + feetOffset); } } } else { m_aimSpot = m_lastEnemyPosition; } // add in aim error m_aimSpot.x += m_aimOffset.x; m_aimSpot.y += m_aimOffset.y; m_aimSpot.z += m_aimOffset.z; Vector toEnemy = m_aimSpot - pev->origin; Vector idealAngle = UTIL_VecToAngles(toEnemy); idealAngle.x = 360.0 - idealAngle.x; SetLookAngles(idealAngle.y, idealAngle.x); } } else { if (m_lookAtSpotClearIfClose) { // dont look at spots just in front of our face - it causes erratic view rotation const float tooCloseRange = 100.0f; if ((m_lookAtSpot - pev->origin).IsLengthLessThan(tooCloseRange)) m_lookAtSpotState = NOT_LOOKING_AT_SPOT; } switch (m_lookAtSpotState) { case NOT_LOOKING_AT_SPOT: { // look ahead SetLookAngles(m_lookAheadAngle, 0); break; } case LOOK_TOWARDS_SPOT: { UpdateLookAt(); if (IsLookingAtPosition(&m_lookAtSpot, m_lookAtSpotAngleTolerance)) { m_lookAtSpotState = LOOK_AT_SPOT; m_lookAtSpotTimestamp = gpGlobals->time; } break; } case LOOK_AT_SPOT: { UpdateLookAt(); if (m_lookAtSpotDuration >= 0.0f && gpGlobals->time - m_lookAtSpotTimestamp > m_lookAtSpotDuration) { m_lookAtSpotState = NOT_LOOKING_AT_SPOT; m_lookAtSpotDuration = 0.0f; } break; } } float driftAmplitude = 2.0f; // have view "drift" very slowly, so view looks "alive" if (IsUsingSniperRifle() && IsUsingScope()) { driftAmplitude = 0.5f; } m_lookYaw += driftAmplitude * BotCOS(33.0f * gpGlobals->time); m_lookPitch += driftAmplitude * BotSIN(13.0f * gpGlobals->time); } #ifdef REGAMEDLL_FIXES // Don't update view angles at frozen state if (!(pev->flags & FL_FROZEN)) #endif { // view angles can change quickly UpdateLookAngles(); } } // Heavyweight processing, invoked less often void CCSBot::Update() { if (TheCSBots()->IsAnalysisRequested() && m_processMode == PROCESS_NORMAL) { TheCSBots()->AckAnalysisRequest(); StartAnalyzeAlphaProcess(); } switch (m_processMode) { case PROCESS_LEARN: UpdateLearnProcess(); return; case PROCESS_ANALYZE_ALPHA: UpdateAnalyzeAlphaProcess(); return; case PROCESS_ANALYZE_BETA: UpdateAnalyzeBetaProcess(); return; case PROCESS_SAVE: UpdateSaveProcess(); return; } // update our radio chatter // need to allow bots to finish their chatter even if they are dead GetChatter()->Update(); if (m_voiceFeedbackEndTimestamp != 0.0f && (m_voiceFeedbackEndTimestamp <= gpGlobals->time || gpGlobals->time < m_voiceFeedbackStartTimestamp)) { EndVoiceFeedback(NO_FORCE); } // check if we are dead if (!IsAlive()) { // remember that we died m_diedLastRound = true; BotDeathThink(); return; } // show line of fire if ((cv_bot_traceview.value == 100.0 && IsLocalPlayerWatchingMe()) || cv_bot_traceview.value == 101.0) { UTIL_MakeVectors(pev->punchangle + pev->v_angle); if (!IsFriendInLineOfFire()) { Vector vecAiming = gpGlobals->v_forward; Vector vecSrc = GetGunPosition(); if (m_iTeam == TERRORIST) UTIL_DrawBeamPoints(vecSrc, vecSrc + 2000.0f * vecAiming, 1, 255, 0, 0); else UTIL_DrawBeamPoints(vecSrc, vecSrc + 2000.0f * vecAiming, 1, 0, 50, 255); } } // // Debug beam rendering // // show approach points if ((cv_bot_traceview.value == 2.0f && IsLocalPlayerWatchingMe()) || cv_bot_traceview.value == 3.0f) DrawApproachPoints(); // show encounter spot data if ((cv_bot_traceview.value == 4.0f && IsLocalPlayerWatchingMe()) || cv_bot_traceview.value == 5.0f) { if (m_spotEncounter) { UTIL_DrawBeamPoints(m_spotEncounter->path.from, m_spotEncounter->path.to, 3, 0, 0, 255); Vector dir = m_spotEncounter->path.to - m_spotEncounter->path.from; float length = dir.NormalizeInPlace(); for (auto &order : m_spotEncounter->spotList) { UTIL_DrawBeamPoints(m_spotEncounter->path.from + order.t * length * dir, *order.spot->GetPosition(), 3, 0, 255, 255); } } } // show path navigation data if (cv_bot_traceview.value == 1.0f && IsLocalPlayerWatchingMe()) { Vector from = GetEyePosition(); const float size = 50.0f; Vector arrow(size * float(Q_cos(m_lookAheadAngle * M_PI / 180.0f)), size * float(Q_sin(m_lookAheadAngle * M_PI / 180.0f)), 0.0f); UTIL_DrawBeamPoints(from, from + arrow, 1, 0, 255, 255); } if (cv_bot_stop.value != 0.0f) return; // check if we are stuck StuckCheck(); // if our current 'noise' was heard a long time ago, forget it const float rememberNoiseDuration = 20.0f; if (m_noiseTimestamp > 0.0f && gpGlobals->time - m_noiseTimestamp > rememberNoiseDuration) { ForgetNoise(); } // where are we if (!m_currentArea || !m_currentArea->Contains(&pev->origin)) { m_currentArea = TheNavAreaGrid.GetNavArea(&pev->origin); } // track the last known area we were in if (m_currentArea && m_currentArea != m_lastKnownArea) { m_lastKnownArea = m_currentArea; // assume that we "clear" an area of enemies when we enter it m_currentArea->SetClearedTimestamp(m_iTeam - 1); } // update approach points const float recomputeApproachPointTolerance = 50.0f; if ((m_approachPointViewPosition - pev->origin).IsLengthGreaterThan(recomputeApproachPointTolerance)) { ComputeApproachPoints(); m_approachPointViewPosition = pev->origin; } if (cv_bot_show_nav.value > 0.0f && m_lastKnownArea) { m_lastKnownArea->DrawConnectedAreas(); } // if we're blind, retreat! if (IsBlind()) { if (!IsAtHidingSpot()) { switch (m_blindMoveDir) { case FORWARD: MoveForward(); break; case RIGHT: StrafeRight(); break; case BACKWARD: MoveBackward(); break; case LEFT: StrafeLeft(); break; default: Crouch(); break; } } if (m_blindFire) { PrimaryAttack(); } return; } // Enemy acquisition and attack initiation // take a snapshot and update our reaction time queue UpdateReactionQueue(); // "threat" may be the same as our current enemy CBasePlayer *threat = GetRecognizedEnemy(); if (threat) { // adjust our personal "safe" time AdjustSafeTime(); if (IsUsingGrenade()) { ThrowGrenade(&threat->pev->origin); } else { // Decide if we should attack bool doAttack = false; switch (GetDisposition()) { case IGNORE_ENEMIES: { // never attack doAttack = false; break; } case SELF_DEFENSE: { // attack if fired on doAttack = IsPlayerLookingAtMe(threat); // attack if enemy very close if (!doAttack) { const float selfDefenseRange = 750.0f; doAttack = (pev->origin - threat->pev->origin).IsLengthLessThan(selfDefenseRange); } break; } case ENGAGE_AND_INVESTIGATE: case OPPORTUNITY_FIRE: { // normal combat range doAttack = true; break; } } if (doAttack) { if (!GetEnemy() || threat != GetEnemy() || !IsAttacking()) { if (IsUsingKnife() && IsHiding()) { // if hiding with a knife, wait until threat is close const float knifeAttackRange = 250.0f; if ((pev->origin - threat->pev->origin).IsLengthLessThan(knifeAttackRange)) { Attack(threat); } } else { Attack(threat); } } } else { // dont attack, but keep track of nearby enemies SetEnemy(threat); m_isEnemyVisible = true; } } // if we aren't attacking but we are being attacked, retaliate if (GetDisposition() != IGNORE_ENEMIES && !IsAttacking()) { const float recentAttackDuration = 1.0f; if (GetTimeSinceAttacked() < recentAttackDuration) { // we may not be attacking our attacker, but at least we're not just taking it // (since m_attacker isn't reaction-time delayed, we can't directly use it) Attack(threat); PrintIfWatched("Ouch! Retaliating!\n"); } } TheCSBots()->SetLastSeenEnemyTimestamp(); } // Validate existing enemy, if any if (m_enemy.IsValid()) { if (IsAwareOfEnemyDeath()) { // we have noticed that our enemy has died m_enemy = nullptr; m_isEnemyVisible = false; } else { const int dada = offsetof(CCSBot, m_visibleEnemyParts); // check LOS to current enemy (chest & head), in case he's dead (GetNearestEnemy() only returns live players) // note we're not checking FOV - once we've acquired an enemy (which does check FOV), assume we know roughly where he is if (IsVisible(m_enemy, false, &m_visibleEnemyParts)) { m_isEnemyVisible = true; m_lastSawEnemyTimestamp = gpGlobals->time; m_lastEnemyPosition = m_enemy->pev->origin; } else { m_isEnemyVisible = false; } // check if enemy died if (m_enemy->IsAlive()) { m_enemyDeathTimestamp = 0.0f; m_isLastEnemyDead = false; } else if (m_enemyDeathTimestamp == 0.0f) { // note time of death (to allow bots to overshoot for a time) m_enemyDeathTimestamp = gpGlobals->time; m_isLastEnemyDead = true; } } } else { m_isEnemyVisible = false; } // if we have seen an enemy recently, keep an eye on him if we can const float seenRecentTime = 3.0f; if (m_enemy.IsValid() && GetTimeSinceLastSawEnemy() < seenRecentTime) { AimAtEnemy(); } else { StopAiming(); } // Hack to fire while retreating // TODO: Encapsulate aiming and firing on enemies separately from current task if (GetDisposition() == IGNORE_ENEMIES) { FireWeaponAtEnemy(); } if (IsEndOfSafeTime() && IsUsingGrenade() && (IsWellPastSafe() || !IsUsingHEGrenade()) && !m_isWaitingToTossGrenade) { Vector target; if (FindGrenadeTossPathTarget(&target)) { ThrowGrenade(&target); } } if (IsUsingGrenade()) { bool doToss = (m_isWaitingToTossGrenade && (m_tossGrenadeTimer.IsElapsed() || m_lookAtSpotState == LOOK_AT_SPOT)); if (doToss) { ClearPrimaryAttack(); m_isWaitingToTossGrenade = false; } else { PrimaryAttack(); } } else { m_isWaitingToTossGrenade = false; } #ifdef REGAMEDLL_FIXES // bots with low skill cannot switch weapons underwater if (GetProfile()->GetSkill() > 0.4f && pev->waterlevel == 3 && !IsActiveWeaponCanShootUnderwater()) { EquipBestWeapon(MUST_EQUIP); } #endif if (IsHunting() && IsWellPastSafe() && IsUsingGrenade()) { EquipBestWeapon(MUST_EQUIP); } // check if our weapon is totally out of ammo // or if we no longer feel "safe", equip our weapon if (!IsSafe() && !IsUsingGrenade() && IsActiveWeaponOutOfAmmo()) { EquipBestWeapon(); } // TODO: This doesn't work if we are restricted to just knives and sniper rifles because we cant use the rifle at close range if (!IsSafe() && !IsUsingGrenade() && IsUsingKnife() && !IsEscapingFromBomb()) { EquipBestWeapon(); } // if we haven't seen an enemy in awhile, and we switched to our pistol during combat, // switch back to our primary weapon (if it still has ammo left) const float safeRearmTime = 5.0f; if (!IsActiveWeaponReloading() && IsUsingPistol() && !IsPrimaryWeaponEmpty() && GetTimeSinceLastSawEnemy() > safeRearmTime) { EquipBestWeapon(); } // reload our weapon if we must ReloadCheck(); // equip silencer SilencerCheck(); // listen to the radio RespondToRadioCommands(); // make way const float avoidTime = 0.33f; if (gpGlobals->time - m_avoidTimestamp < avoidTime && m_avoid) { StrafeAwayFromPosition(&m_avoid->pev->origin); } else { m_avoid = nullptr; } if (m_isJumpCrouching) { const float duration = 0.75f; const float crouchDelayTime = 0.05f; const float standUpTime = 0.6f; float elapsed = gpGlobals->time - m_jumpCrouchTimestamp; if (elapsed > crouchDelayTime && elapsed < standUpTime) Crouch(); if (elapsed >= standUpTime) StandUp(); if (elapsed > duration) m_isJumpCrouching = false; } // if we're using a sniper rifle and are no longer attacking, stop looking thru scope if (!IsAtHidingSpot() && !IsAttacking() && IsUsingSniperRifle() && IsUsingScope()) { SecondaryAttack(); } // check encounter spots UpdatePeripheralVision(); // Update gamestate if (m_bomber) { GetChatter()->SpottedBomber(GetBomber()); } if (CanSeeLooseBomb()) { GetChatter()->SpottedLooseBomb(TheCSBots()->GetLooseBomb()); } // Scenario interrupts switch (TheCSBots()->GetScenario()) { case CCSBotManager::SCENARIO_DEFUSE_BOMB: { // flee if the bomb is ready to blow and we aren't defusing it or attacking and we know where the bomb is // (aggressive players wait until its almost too late) float gonnaBlowTime = 8.0f - (2.0f * GetProfile()->GetAggression()); // if we have a defuse kit, can wait longer if (m_bHasDefuser) gonnaBlowTime *= 0.66f; if (!IsEscapingFromBomb() // we aren't already escaping the bomb && TheCSBots()->IsBombPlanted() // is the bomb planted && GetGameState()->IsPlantedBombLocationKnown() // we know where the bomb is && TheCSBots()->GetBombTimeLeft() < gonnaBlowTime // is the bomb about to explode && !IsDefusingBomb() // we aren't defusing the bomb && !IsAttacking()) // we aren't in the midst of a firefight { EscapeFromBomb(); break; } break; } case CCSBotManager::SCENARIO_RESCUE_HOSTAGES: { if (m_iTeam == CT) { UpdateHostageEscortCount(); } else { // Terrorists have imperfect information on status of hostages CSGameState::ValidateStatusType status = GetGameState()->ValidateHostagePositions(); if (status & CSGameState::HOSTAGES_ALL_GONE) { GetChatter()->HostagesTaken(); Idle(); } else if (status & CSGameState::HOSTAGE_GONE) { GetGameState()->HostageWasTaken(); Idle(); } } break; } } // Follow nearby humans if our co-op is high and we have nothing else to do // If we were just following someone, don't auto-follow again for a short while to // give us a chance to do something else. const float earliestAutoFollowTime = 5.0f; const float minAutoFollowTeamwork = 0.4f; if (TheCSBots()->GetElapsedRoundTime() > earliestAutoFollowTime && GetProfile()->GetTeamwork() > minAutoFollowTeamwork && CanAutoFollow() && !IsBusy() && !IsFollowing() && !GetGameState()->IsAtPlantedBombsite()) { // chance of following is proportional to teamwork attribute if (GetProfile()->GetTeamwork() > RANDOM_FLOAT(0.0f, 1.0f)) { CBasePlayer *pLeader = GetClosestVisibleHumanFriend(); if (pLeader && pLeader->IsAutoFollowAllowed()) { // count how many bots are already following this player const float maxFollowCount = 2; if (GetBotFollowCount(pLeader) < maxFollowCount) { const float autoFollowRange = 300.0f; if ((pLeader->pev->origin - pev->origin).IsLengthLessThan(autoFollowRange)) { CNavArea *leaderArea = TheNavAreaGrid.GetNavArea(&pLeader->pev->origin); if (leaderArea) { PathCost cost(this, FASTEST_ROUTE); float travelRange = NavAreaTravelDistance(GetLastKnownArea(), leaderArea, cost); if (/*travelRange >= 0.0f &&*/ travelRange < autoFollowRange) { // follow this human Follow(pLeader); PrintIfWatched("Auto-Following %s\n", STRING(pLeader->pev->netname)); if (CSGameRules()->IsCareer()) { GetChatter()->Say("FollowingCommander", 10.0f); } else { GetChatter()->Say("FollowingSir", 10.0f); } } } } } } } else { // we decided not to follow, don't re-check for a duration m_allowAutoFollowTime = gpGlobals->time + 15.0f + (1.0f - GetProfile()->GetTeamwork()) * 30.0f; } } if (IsFollowing()) { // if we are following someone, make sure they are still alive CBaseEntity *pLeader = m_leader; if (!pLeader || !pLeader->IsAlive()) { StopFollowing(); } // decide whether to continue following them const float highTeamwork = 0.85f; if (GetProfile()->GetTeamwork() < highTeamwork) { float minFollowDuration = 15.0f; if (GetFollowDuration() > minFollowDuration + 40.0f * GetProfile()->GetTeamwork()) { // we are bored of following our leader StopFollowing(); PrintIfWatched("Stopping following - bored\n"); } } } else { if (GetMorale() < NEUTRAL && IsSafe() && GetSafeTimeRemaining() < 2.0f && IsHunting()) { if (GetMorale() * -40.0 > RANDOM_FLOAT(0.0f, 100.0f)) { if (TheCSBots()->IsOnOffense(this) || !TheCSBots()->IsDefenseRushing()) { SetDisposition(OPPORTUNITY_FIRE); Hide(m_lastKnownArea, RANDOM_FLOAT(3.0f, 15.0f)); GetChatter()->Say("WaitingHere"); } } } } // Execute state machine if (m_isAttacking) { m_attackState.OnUpdate(this); } else { m_state->OnUpdate(this); } if (m_isWaitingToTossGrenade) { ResetStuckMonitor(); ClearMovement(); } // don't move while reloading unless we see an enemy if (IsReloading() && !m_isEnemyVisible) { ResetStuckMonitor(); ClearMovement(); } // if we get too far ahead of the hostages we are escorting, wait for them if (!IsAttacking() && m_inhibitWaitingForHostageTimer.IsElapsed()) { const float waitForHostageRange = 500.0f; if (GetTask() == RESCUE_HOSTAGES && GetRangeToFarthestEscortedHostage() > waitForHostageRange) { if (!m_isWaitingForHostage) { // just started waiting m_isWaitingForHostage = true; m_waitForHostageTimer.Start(10.0f); } else { // we've been waiting if (m_waitForHostageTimer.IsElapsed()) { // give up waiting for awhile m_isWaitingForHostage = false; m_inhibitWaitingForHostageTimer.Start(3.0f); } else { // keep waiting ResetStuckMonitor(); ClearMovement(); } } } } // remember our prior safe time status m_wasSafe = IsSafe(); }