You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
752 lines
18 KiB
752 lines
18 KiB
#include "bot_common.h" |
|
|
|
// Lightweight maintenance, invoked frequently |
|
|
|
void CCSBot::Upkeep() |
|
{ |
|
CCSBotManager *ctrl = TheCSBots(); |
|
|
|
if (ctrl->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 != NULL) |
|
{ |
|
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); |
|
} |
|
|
|
// view angles can change quickly |
|
UpdateLookAngles(); |
|
} |
|
|
|
// Heavyweight processing, invoked less often |
|
|
|
void CCSBot::Update() |
|
{ |
|
CCSBotManager *ctrl = TheCSBots(); |
|
|
|
if (ctrl->IsAnalysisRequested() && m_processMode == PROCESS_NORMAL) |
|
{ |
|
ctrl->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.0f && IsLocalPlayerWatchingMe()) || cv_bot_traceview.value == 101.0f) |
|
{ |
|
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, 224, 0); |
|
//else |
|
// UTIL_DrawBeamPoints(vecSrc, vecSrc + 2000.0f * vecAiming, 1, 0, 50, 255); |
|
} |
|
} |
|
|
|
// Debug beam rendering |
|
if( ( cv_bot_traceview.value == 2.0f && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.value == 3.0f ) |
|
DrawApproachPoints(); |
|
|
|
if( ( cv_bot_traceview.value == 4.0f && IsLocalPlayerWatchingMe() ) || cv_bot_traceview.value == 5.0f ) |
|
{ |
|
// ... |
|
} |
|
|
|
if (cv_bot_traceview.value == 1.0f && IsLocalPlayerWatchingMe()) |
|
{ |
|
// ... |
|
} |
|
|
|
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 != NULL && 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 != NULL) |
|
{ |
|
m_lastKnownArea->DrawConnectedAreas(); |
|
} |
|
|
|
#if 0 |
|
// 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; |
|
} |
|
#endif |
|
|
|
// 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 != NULL) |
|
{ |
|
// adjust our personal "safe" time |
|
AdjustSafeTime(); |
|
|
|
// Decide if we should attack |
|
bool doAttack = false; |
|
if (!IsUsingGrenade()) |
|
{ |
|
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; |
|
} |
|
} |
|
} |
|
else |
|
ThrowGrenade(&threat->pev->origin); |
|
|
|
// if we aren't attacking but we are being attacked, retaliate |
|
if (!doAttack && !IsAttacking() && GetDisposition() != IGNORE_ENEMIES) |
|
{ |
|
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) |
|
doAttack = true; |
|
PrintIfWatched("Ouch! Retaliating!\n"); |
|
} |
|
} |
|
|
|
if (doAttack) |
|
{ |
|
if (GetEnemy() == NULL || !IsAttacking() || threat != GetEnemy()) |
|
{ |
|
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; |
|
} |
|
|
|
ctrl->SetLastSeenEnemyTimestamp(); |
|
} |
|
|
|
// Validate existing enemy, if any |
|
if (m_enemy != NULL) |
|
{ |
|
if (IsAwareOfEnemyDeath()) |
|
{ |
|
// we have noticed that our enemy has died |
|
m_enemy = NULL; |
|
m_isEnemyVisible = false; |
|
} |
|
else |
|
{ |
|
// 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 != NULL && 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; |
|
} |
|
|
|
|
|
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 != NULL) |
|
{ |
|
StrafeAwayFromPosition(&m_avoid->pev->origin); |
|
} |
|
else |
|
{ |
|
m_avoid = NULL; |
|
} |
|
|
|
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 != NULL) |
|
GetChatter()->SpottedBomber(GetBomber()); |
|
|
|
if (CanSeeLooseBomb()) |
|
GetChatter()->SpottedLooseBomb(ctrl->GetLooseBomb()); |
|
|
|
// Scenario interrupts |
|
switch (ctrl->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 |
|
&& ctrl->IsBombPlanted() // is the bomb planted |
|
&& GetGameState()->IsPlantedBombLocationKnown() // we know where the bomb is |
|
&& ctrl->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 0 |
|
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; |
|
#endif |
|
} |
|
} |
|
|
|
// 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 (ctrl->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 *leader = GetClosestVisibleHumanFriend(); |
|
#if 0 |
|
if (leader != NULL && leader->IsAutoFollowAllowed()) |
|
{ |
|
// count how many bots are already following this player |
|
const float maxFollowCount = 2; |
|
if (GetBotFollowCount(leader) < maxFollowCount) |
|
{ |
|
const float autoFollowRange = 300.0f; |
|
if ((leader->pev->origin - pev->origin).IsLengthLessThan(autoFollowRange)) |
|
{ |
|
CNavArea *leaderArea = TheNavAreaGrid.GetNavArea(&leader->pev->origin); |
|
if (leaderArea != NULL) |
|
{ |
|
PathCost cost(this, FASTEST_ROUTE); |
|
float travelRange = NavAreaTravelDistance(GetLastKnownArea(), leaderArea, cost); |
|
if (/*travelRange >= 0.0f &&*/ travelRange < autoFollowRange) |
|
{ |
|
// follow this human |
|
Follow(leader); |
|
PrintIfWatched("Auto-Following %s\n", STRING(leader->pev->netname)); |
|
|
|
if (g_pGameRules->IsCareer()) |
|
{ |
|
GetChatter()->Say("FollowingCommander", 10.0f); |
|
} |
|
else |
|
{ |
|
GetChatter()->Say("FollowingSir", 10.0f); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
#endif |
|
} |
|
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 *leader = m_leader; |
|
if (leader == NULL || !leader->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 (ctrl->IsOnOffense(this) || !ctrl->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(); |
|
} |
|
#if 0 |
|
// don't move while reloading unless we see an enemy |
|
if (IsReloading() && !m_isEnemyVisible) |
|
{ |
|
ResetStuckMonitor(); |
|
ClearMovement(); |
|
} |
|
#endif |
|
|
|
// 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(); |
|
}
|
|
|