diff --git a/main/source/mod/AIPlayers/AvHAIConstants.h b/main/source/mod/AIPlayers/AvHAIConstants.h index 95b85a9f..b878b55d 100644 --- a/main/source/mod/AIPlayers/AvHAIConstants.h +++ b/main/source/mod/AIPlayers/AvHAIConstants.h @@ -201,6 +201,15 @@ typedef enum _AVHAIBOTROLE BOT_ROLE_HARASS // Focuses on taking down enemy resource nodes and hunting the enemy } AvHAIBotRole; +typedef enum _AVHAICOMBATSTRATEGY +{ + COMBAT_STRATEGY_IGNORE = 0, // Don't engage this enemy + COMBAT_STRATEGY_AMBUSH, // Set up an ambush for this enemy + COMBAT_STRATEGY_RETREAT, // Retreat and find health + COMBAT_STRATEGY_SKIRMISH, // Maintain distance, whittle down their health from range and generally be a pain the arse + COMBAT_STRATEGY_ATTACK // Attack the enemy +} AvHAICombatStrategy; + typedef struct _OFF_MESH_CONN { unsigned int ConnectionRefs[2]; @@ -406,6 +415,7 @@ typedef struct _BOT_PATH_NODE // Represents a bot's current understanding of an enemy player's status typedef struct _ENEMY_STATUS { + AvHPlayer* EnemyPlayer = nullptr; edict_t* EnemyEdict = nullptr; // Reference to the enemy player edict Vector LastSeenLocation = g_vecZero; // The last visibly-confirmed location of the player Vector LastFloorPosition = g_vecZero; // Nearest point on the floor where the enemy was (for moving towards it) @@ -634,6 +644,7 @@ typedef struct AVH_AI_PLAYER enemy_status TrackedEnemies[32]; int CurrentEnemy = -1; + AvHAICombatStrategy CurrentCombatStrategy = COMBAT_STRATEGY_ATTACK; edict_t* CurrentEnemyRef = nullptr; AvHAIPlayerTask* CurrentTask = nullptr; // Bot's current task they're performing diff --git a/main/source/mod/AIPlayers/AvHAINavigation.cpp b/main/source/mod/AIPlayers/AvHAINavigation.cpp index d0160df9..f1d74f87 100644 --- a/main/source/mod/AIPlayers/AvHAINavigation.cpp +++ b/main/source/mod/AIPlayers/AvHAINavigation.cpp @@ -1995,7 +1995,7 @@ bool HasBotReachedPathPoint(const AvHAIPlayer* pBot) if (DirectionDot >= -0.5f) { - return bAtOrPastDestination && UTIL_PointIsDirectlyReachable(pBot, pBot->CurrentFloorPosition, NextPathPoint->Location) && UTIL_QuickHullTrace(pBot->Edict, pBot->Edict->v.origin, NextPathPoint->Location + Vector(0.0f, 0.0f, 10.0f), head_hull); + return bAtOrPastDestination && UTIL_PointIsDirectlyReachable(pBot, pBot->CurrentFloorPosition, NextPathPoint->Location) && UTIL_QuickTrace(pBot->Edict, pBot->Edict->v.origin, NextPathPoint->Location); } else { @@ -2007,7 +2007,7 @@ bool HasBotReachedPathPoint(const AvHAIPlayer* pBot) return (vDist2D(pEdict->v.origin, MoveTo) <= playerRadius && (pEdict->v.origin.z - MoveTo.z) < 50.0f && pBot->BotNavInfo.IsOnGround); } case SAMPLE_POLYFLAGS_WALLCLIMB: - return (bAtOrPastDestination && pBot->CollisionHullTopLocation.z > MoveTo.z); + return (bAtOrPastDestination && fabs(pBot->CollisionHullTopLocation.z - MoveTo.z) < fabs(MoveFrom.z - MoveTo.z)); case SAMPLE_POLYFLAGS_LADDER: if (MoveTo.z > MoveFrom.z) { @@ -3836,7 +3836,7 @@ bool IsBotOffPath(const AvHAIPlayer* pBot) { // Can't be off the path if we don't have one... - if (pBot->BotNavInfo.CurrentPath.size() == 0) { return false; } + if (pBot->BotNavInfo.CurrentPath.size() == 0 || pBot->BotNavInfo.CurrentPathPoint == pBot->BotNavInfo.CurrentPath.end()) { return false; } if (pBot->BotNavInfo.CurrentPathPoint->flag == SAMPLE_POLYFLAGS_LIFT) { return false; } @@ -3847,15 +3847,16 @@ bool IsBotOffPath(const AvHAIPlayer* pBot) PGFilter.DeployableTypes = STRUCTURE_MARINE_PHASEGATE; PGFilter.MaxSearchRadius = UTIL_MetresToGoldSrcUnits(2.0f); PGFilter.IncludeStatusFlags = STRUCTURE_STATUS_COMPLETED; + PGFilter.ExcludeStatusFlags = STRUCTURE_STATUS_RECYCLING; PGFilter.DeployableTeam = (AvHTeamNumber)pBot->Edict->v.team; - + // The phase gate we're meant to be using isn't here any more! if (!AITAC_DeployableExistsAtLocation(pBot->Edict->v.origin, &PGFilter)) { return true; } - // This checks to ensure the target phase gate hasn't been destroyed since the bot initially calculated its path. If so, then this will force it to calculate a new path + // The phase gate we're meant to be warping to isn't there any more! if (!AITAC_DeployableExistsAtLocation(pBot->BotNavInfo.CurrentPathPoint->Location, &PGFilter)) { return true; @@ -3867,10 +3868,8 @@ bool IsBotOffPath(const AvHAIPlayer* pBot) edict_t* pEdict = pBot->Edict; Vector MoveTo = pBot->BotNavInfo.CurrentPathPoint->Location; - Vector MoveFrom = pBot->CurrentFloorPosition; - float PlayerRadiusSq = sqrf(GetPlayerRadius(pBot->Player)); float PlayerHeight = GetPlayerHeight(pBot->Edict, false); @@ -3891,7 +3890,13 @@ bool IsBotOffPath(const AvHAIPlayer* pBot) if (!UTIL_PointIsDirectlyReachable(pBot->CurrentFloorPosition, MoveTo)) { - return true; + Vector TraceEnd = MoveTo; + TraceEnd.z = pBot->Edict->v.origin.z; + + if (!UTIL_QuickHullTrace(pBot->Edict, pBot->Edict->v.origin, TraceEnd, GetPlayerHullIndex(pBot->Edict))) + { + return true; + } } bool bAtMoveStart = vEquals(PointOnPath, MoveFrom, 2.0f); @@ -4158,6 +4163,8 @@ void MoveToWithoutNav(AvHAIPlayer* pBot, const Vector Destination) void MoveDirectlyTo(AvHAIPlayer* pBot, const Vector Destination) { + if (vIsZero(Destination)) { return; } + Vector CurrentPos = (pBot->BotNavInfo.IsOnGround) ? pBot->Edict->v.origin : pBot->CurrentFloorPosition; const Vector vForward = UTIL_GetVectorNormal2D(Destination - CurrentPos); @@ -5203,10 +5210,10 @@ void SkulkUpdateBotMoveProfile(AvHAIPlayer* pBot, BotMoveStyle MoveStyle) { if (MoveStyle == pBot->BotNavInfo.PreviousMoveStyle) { return; } + pBot->BotNavInfo.MoveStyle = MoveStyle; pBot->BotNavInfo.PreviousMoveStyle = MoveStyle; - pBot->BotNavInfo.bNavProfileChanged = true; - pBot->BotNavInfo.MoveStyle = MoveStyle; + pBot->BotNavInfo.bNavProfileChanged = true; nav_profile* NavProfile = &pBot->BotNavInfo.NavProfile; @@ -5313,6 +5320,135 @@ void OnosUpdateBotMoveProfile(AvHAIPlayer* pBot, BotMoveStyle MoveStyle) } bool MoveTo(AvHAIPlayer* pBot, const Vector Destination, const BotMoveStyle MoveStyle, const float MaxAcceptableDist) +{ + if (vIsZero(Destination) || (vDist2D(pBot->Edict->v.origin, Destination) <= 8.0f && (fabs(pBot->CollisionHullBottomLocation.z - Destination.z) < 50.0f))) + { + ClearBotMovement(pBot); + return true; + } + + nav_status* BotNavInfo = &pBot->BotNavInfo; + + pBot->BotNavInfo.MoveStyle = MoveStyle; + UTIL_UpdateBotMovementStatus(pBot); + + UpdateBotMoveProfile(pBot, MoveStyle); + + bool bIsFlyingProfile = pBot->BotNavInfo.NavProfile.bFlyingProfile; + bool bNavProfileChanged = pBot->BotNavInfo.bNavProfileChanged; + bool bForceRecalculation = (pBot->BotNavInfo.NextForceRecalc > 0.0f && gpGlobals->time >= pBot->BotNavInfo.NextForceRecalc); + + // Only recalculate the path if there isn't a path, or something has changed and enough time has elapsed since the last path calculation + bool bShouldCalculatePath = (bNavProfileChanged || bForceRecalculation || BotNavInfo->CurrentPath.size() == 0 || !vEquals(Destination, BotNavInfo->TargetDestination, GetPlayerRadius(pBot->Player))); + + if (bShouldCalculatePath) + { + if (!AbortCurrentMove(pBot, Destination)) { return true; } + + if (!bIsFlyingProfile && !pBot->BotNavInfo.IsOnGround) { return true; } + + dtStatus PathFindingStatus = DT_FAILURE; + + if (bIsFlyingProfile) + { + PathFindingStatus = FindFlightPathToPoint(pBot->BotNavInfo.NavProfile, pBot->Edict->v.origin, Destination, BotNavInfo->CurrentPath, MaxAcceptableDist); + } + else + { + Vector NavAdjustedDestination = AdjustPointForPathfinding(Destination); + if (vIsZero(NavAdjustedDestination)) { return false; } + + PathFindingStatus = FindPathClosestToPoint(pBot, pBot->BotNavInfo.MoveStyle, pBot->CollisionHullBottomLocation, NavAdjustedDestination, BotNavInfo->CurrentPath, MaxAcceptableDist); + } + + pBot->BotNavInfo.NextForceRecalc = 0.0f; + pBot->BotNavInfo.bNavProfileChanged = false; + + if (dtStatusSucceed(PathFindingStatus)) + { + BotNavInfo->PathDestination = BotNavInfo->CurrentPath.back().Location; + ClearBotStuckMovement(pBot); + pBot->BotNavInfo.TotalStuckTime = 0.0f; + BotNavInfo->TargetDestination = Destination; + BotNavInfo->CurrentPathPoint = BotNavInfo->CurrentPath.begin(); + } + else + { + if (!vIsZero(BotNavInfo->LastNavMeshPosition)) + { + MoveDirectlyTo(pBot, BotNavInfo->LastNavMeshPosition); + + if (vDist2DSq(pBot->CurrentFloorPosition, BotNavInfo->LastNavMeshPosition) < sqrf(8.0f)) + { + BotNavInfo->LastNavMeshPosition = g_vecZero; + } + + return true; + } + else + { + if (vIsZero(BotNavInfo->UnstuckMoveLocation)) + { + BotNavInfo->UnstuckMoveLocation = FindClosestPointBackOnPath(pBot); + } + + if (!vIsZero(BotNavInfo->UnstuckMoveLocation)) + { + MoveDirectlyTo(pBot, BotNavInfo->UnstuckMoveLocation); + return true; + } + } + + if (IsBotPermaStuck(pBot)) + { + BotSuicide(pBot); + return false; + } + + ClearBotPath(pBot); + return false; + } + } + + if (BotNavInfo->CurrentPath.size() > 0) + { + if (IsBotPermaStuck(pBot)) + { + BotSuicide(pBot); + return false; + } + + if (pBot->Edict->v.flags & FL_INWATER) + { + BotFollowSwimPath(pBot); + } + else + { + if (bIsFlyingProfile) + { + BotFollowFlightPath(pBot); + } + else + { + BotFollowPath(pBot); + } + } + + // Check to ensure BotFollowFlightPath or BotFollowPath haven't cleared the path (will happen if reached end of path) + if (BotNavInfo->CurrentPathPoint != BotNavInfo->CurrentPath.end()) + { + HandlePlayerAvoidance(pBot, BotNavInfo->CurrentPathPoint->Location); + BotMovementInputs(pBot); + } + + return true; + } + + return false; + +} + +bool MoveTo_OLD(AvHAIPlayer* pBot, const Vector Destination, const BotMoveStyle MoveStyle, const float MaxAcceptableDist) { nav_status* BotNavInfo = &pBot->BotNavInfo; @@ -5563,9 +5699,12 @@ Vector FindClosestPointBackOnPath(AvHAIPlayer* pBot) AvHAIResourceNode* NearestResNode = AITAC_FindNearestResourceNodeToLocation(pBot->Edict->v.origin, &ResNodeFilter); - if (!NearestResNode) { return g_vecZero; } + Vector ValidNavmeshPoint = AITAC_GetTeamStartingLocation(pBot->Player->GetTeam()); - Vector ValidNavmeshPoint = NearestResNode->Location; + if (NearestResNode && vDist2D(pBot->Edict->v.origin, NearestResNode->Location) < vDist2D(pBot->Edict->v.origin, ValidNavmeshPoint)) + { + ValidNavmeshPoint = NearestResNode->Location; + } ValidNavmeshPoint = UTIL_ProjectPointToNavmesh(ValidNavmeshPoint, pBot->BotNavInfo.NavProfile); @@ -6393,8 +6532,6 @@ void ClearBotMovement(AvHAIPlayer* pBot) ClearBotStuck(pBot); ClearBotStuckMovement(pBot); - AITASK_ClearBotTask(pBot, &pBot->BotNavInfo.MovementTask); - pBot->LastPosition = pBot->Edict->v.origin; pBot->TimeSinceLastMovement = 0.0f; } diff --git a/main/source/mod/AIPlayers/AvHAINavigation.h b/main/source/mod/AIPlayers/AvHAINavigation.h index ba4866fb..2092bd10 100644 --- a/main/source/mod/AIPlayers/AvHAINavigation.h +++ b/main/source/mod/AIPlayers/AvHAINavigation.h @@ -21,7 +21,7 @@ */ constexpr auto MIN_PATH_RECALC_TIME = 0.33f; // How frequently can a bot recalculate its path? Default to max 3 times per second -constexpr auto MAX_BOT_STUCK_TIME = 0.0f; // How long a bot can be stuck, unable to move, before giving up and suiciding +constexpr auto MAX_BOT_STUCK_TIME = 30.0f; // How long a bot can be stuck, unable to move, before giving up and suiciding constexpr auto MARINE_BASE_NAV_PROFILE = 0; constexpr auto SKULK_BASE_NAV_PROFILE = 1; diff --git a/main/source/mod/AIPlayers/AvHAIPlayer.cpp b/main/source/mod/AIPlayers/AvHAIPlayer.cpp index a337d0e2..93fbae90 100644 --- a/main/source/mod/AIPlayers/AvHAIPlayer.cpp +++ b/main/source/mod/AIPlayers/AvHAIPlayer.cpp @@ -219,7 +219,7 @@ AvHBasePlayerWeapon* GetPlayerCurrentWeaponReference(const AvHPlayer* Player) bool CanBotLeap(AvHAIPlayer* pBot) { - return (PlayerHasWeapon(pBot->Player, WEAPON_SKULK_LEAP)) || (PlayerHasWeapon(pBot->Player, WEAPON_FADE_BLINK)); + return (PlayerHasWeapon(pBot->Player, WEAPON_SKULK_LEAP) && GetPlayerEnergy(pBot->Edict) >= (float)BALANCE_VAR(kLeapEnergyCost)) || (PlayerHasWeapon(pBot->Player, WEAPON_FADE_BLINK)); } float GetLeapCost(AvHAIPlayer* pBot) @@ -989,6 +989,37 @@ void BotEvolveLifeform(AvHAIPlayer* pBot, Vector DesiredEvolveLocation, AvHMessa { if (!IsPlayerAlien(pBot->Edict)) { return; } + float EvolveCost = 0.0f; + + AvHUser3 TargetUser3 = pBot->Player->GetUser3(); + + switch (TargetLifeform) + { + case ALIEN_LIFEFORM_TWO: + TargetUser3 = AVH_USER3_ALIEN_PLAYER2; + EvolveCost = BALANCE_VAR(kGorgeCost); + break; + case ALIEN_LIFEFORM_THREE: + TargetUser3 = AVH_USER3_ALIEN_PLAYER3; + EvolveCost = BALANCE_VAR(kLerkCost); + break; + case ALIEN_LIFEFORM_FOUR: + TargetUser3 = AVH_USER3_ALIEN_PLAYER4; + EvolveCost = BALANCE_VAR(kFadeCost); + break; + case ALIEN_LIFEFORM_FIVE: + TargetUser3 = AVH_USER3_ALIEN_PLAYER5; + EvolveCost = BALANCE_VAR(kOnosCost); + break; + default: + TargetUser3 = AVH_USER3_ALIEN_PLAYER1; + EvolveCost = 0.0f; + break; + } + + // We're already the target lifeform, don't do anything + if (TargetUser3 == pBot->Player->GetUser3()) { return; } + Vector EvolvePoint = UTIL_ProjectPointToNavmesh(DesiredEvolveLocation, GetBaseNavProfile(STRUCTURE_BASE_NAV_PROFILE)); if (vIsZero(EvolvePoint)) @@ -1002,26 +1033,7 @@ void BotEvolveLifeform(AvHAIPlayer* pBot, Vector DesiredEvolveLocation, AvHMessa return; } - float EvolveCost = 0.0f; - switch (TargetLifeform) - { - case ALIEN_LIFEFORM_TWO: - EvolveCost = BALANCE_VAR(kGorgeCost); - break; - case ALIEN_LIFEFORM_THREE: - EvolveCost = BALANCE_VAR(kLerkCost); - break; - case ALIEN_LIFEFORM_FOUR: - EvolveCost = BALANCE_VAR(kFadeCost); - break; - case ALIEN_LIFEFORM_FIVE: - EvolveCost = BALANCE_VAR(kOnosCost); - break; - default: - EvolveCost = 0.0f; - break; - } if (pBot->Player->GetResources() >= EvolveCost) { @@ -1281,12 +1293,21 @@ void BotUpdateView(AvHAIPlayer* pBot) edict_t* PlayerEdict = INDEXENT(i); int EnemyIndex = i - 1; - if (FNullEnt(PlayerEdict) || !IsPlayerActiveInGame(PlayerEdict) || PlayerEdict->v.team == pBot->Edict->v.team) + if (FNullEnt(PlayerEdict) || PlayerEdict->free || !IsPlayerActiveInGame(PlayerEdict) || PlayerEdict->v.team == pBot->Edict->v.team) { BotClearEnemyTrackingInfo(&pBot->TrackedEnemies[EnemyIndex]); continue; } + AvHPlayer* PlayerRef = dynamic_cast(CBaseEntity::Instance(PlayerEdict)); + + if (!PlayerRef) + { + BotClearEnemyTrackingInfo(&pBot->TrackedEnemies[EnemyIndex]); + continue; + } + + pBot->TrackedEnemies[EnemyIndex].EnemyPlayer = PlayerRef; pBot->TrackedEnemies[EnemyIndex].EnemyEdict = PlayerEdict; enemy_status* TrackingInfo = &pBot->TrackedEnemies[EnemyIndex]; @@ -1307,11 +1328,11 @@ void BotUpdateView(AvHAIPlayer* pBot) Vector VisiblePoint = GetVisiblePointOnPlayerFromObserver(pBot->Edict, Enemy); - bool bHasLOS = (VisiblePoint != ZERO_VECTOR); + bool bHasLOS = !vIsZero(VisiblePoint); bool bIsTracked = (!bHasLOS && (IsPlayerParasited(Enemy) || IsPlayerMotionTracked(Enemy))); - if (pBot->LastSafeLocation != ZERO_VECTOR && UTIL_PlayerHasLOSToLocation(Enemy, pBot->LastSafeLocation, UTIL_MetresToGoldSrcUnits(50.0f))) + if (!vIsZero(pBot->LastSafeLocation) && UTIL_PlayerHasLOSToLocation(Enemy, pBot->LastSafeLocation, UTIL_MetresToGoldSrcUnits(50.0f))) { pBot->LastSafeLocation = ZERO_VECTOR; } @@ -1578,7 +1599,7 @@ void StartNewBotFrame(AvHAIPlayer* pBot) SetBaseNavProfile(pBot); } - UpdateBotMoveProfile(pBot, pBot->BotNavInfo.MoveStyle); + //UpdateBotMoveProfile(pBot, pBot->BotNavInfo.MoveStyle); if (IsPlayerMarine(pBot->Edict)) { @@ -1613,7 +1634,39 @@ void StartNewBotFrame(AvHAIPlayer* pBot) void CustomThink(AvHAIPlayer* pBot) { - UpdateAIAlienPlayerNSRole(pBot); + if (IsPlayerMarine(pBot->Player)) { return; } + + if (!IsPlayerFade(pBot->Edict)) + { + if (pBot->Player->GetResources() < BALANCE_VAR(kFadeCost)) + { + pBot->Player->GiveResources(50.0f); + } + + BotEvolveLifeform(pBot, pBot->Edict->v.origin, ALIEN_LIFEFORM_FOUR); + + return; + } + + pBot->CurrentEnemy = BotGetNextEnemyTarget(pBot); + + if (pBot->CurrentEnemy < 0) + { + MoveTo(pBot, AITAC_GetTeamStartingLocation(AIMGR_GetEnemyTeam(pBot->Player->GetTeam())), MOVESTYLE_NORMAL); + } + else + { + AlienCombatThink(pBot); + } + + AvHAIWeapon DesiredWeapon = (pBot->DesiredMoveWeapon != WEAPON_NONE) ? pBot->DesiredMoveWeapon : pBot->DesiredCombatWeapon; + + if (DesiredWeapon != WEAPON_NONE && GetPlayerCurrentWeapon(pBot->Player) != DesiredWeapon) + { + BotSwitchToWeapon(pBot, DesiredWeapon); + } + + } @@ -1804,6 +1857,8 @@ void AIPlayerNSThink(AvHAIPlayer* pBot) if (!BotTeam) { return; } + pBot->CurrentEnemy = BotGetNextEnemyTarget(pBot); + if (BotTeam->GetTeamType() == AVH_CLASS_TYPE_MARINE) { AIPlayerNSMarineThink(pBot); @@ -1814,6 +1869,428 @@ void AIPlayerNSThink(AvHAIPlayer* pBot) } } +int BotGetNextEnemyTarget(AvHAIPlayer* pBot) +{ + int NearestVisibleEnemy = -1; + int NearestUnseenEnemy = -1; + + float ClosestVisibleDist = 0.0f; + float ClosestUnseenDist = 0.0f; + + for (int i = 0; i < gpGlobals->maxClients; i++) + { + if (!pBot->TrackedEnemies[i].bIsAwareOfPlayer) { continue; } + + enemy_status* TrackingInfo = &pBot->TrackedEnemies[i]; + + float Dist = vDist2DSq(TrackingInfo->LastSeenLocation, pBot->Edict->v.origin); + + if (TrackingInfo->bHasLOS) + { + if (NearestVisibleEnemy < 0 || Dist < ClosestVisibleDist) + { + NearestVisibleEnemy = i; + ClosestVisibleDist = Dist; + } + } + else + { + // Ignore tracked enemies if we've not seen them in a while and we have something important to do + if (pBot->CurrentTask && (pBot->CurrentTask->bTaskIsUrgent || pBot->CurrentTask->bIssuedByCommander)) + { + if ((gpGlobals->time - TrackingInfo->LastSeenTime) > 5.0f) { continue; } + } + + if (Dist > sqrf(UTIL_MetresToGoldSrcUnits(15.0f)) && (gpGlobals->time - TrackingInfo->LastSeenTime) > 10.0f) + { + continue; + } + + if (NearestUnseenEnemy < 0 || Dist < ClosestUnseenDist) + { + NearestUnseenEnemy = i; + ClosestUnseenDist = Dist; + } + } + } + + return (NearestVisibleEnemy > -1) ? NearestVisibleEnemy : NearestUnseenEnemy; + +} + +AvHAICombatStrategy GetBotCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + if (FNullEnt(CurrentEnemy->EnemyEdict) || !IsPlayerActiveInGame(CurrentEnemy->EnemyEdict)) { return COMBAT_STRATEGY_IGNORE; } + + if (IsPlayerAlien(pBot->Edict)) + { + return GetAlienCombatStrategyForTarget(pBot, CurrentEnemy); + } + else + { + return GetMarineCombatStrategyForTarget(pBot, CurrentEnemy); + } +} + +AvHAICombatStrategy GetAlienCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + AvHUser3 PlayerUser3 = pBot->Player->GetUser3(); + + switch (PlayerUser3) + { + case AVH_USER3_ALIEN_PLAYER1: + return GetSkulkCombatStrategyForTarget(pBot, CurrentEnemy); + case AVH_USER3_ALIEN_PLAYER2: + return GetGorgeCombatStrategyForTarget(pBot, CurrentEnemy); + case AVH_USER3_ALIEN_PLAYER3: + return GetLerkCombatStrategyForTarget(pBot, CurrentEnemy); + case AVH_USER3_ALIEN_PLAYER4: + return GetFadeCombatStrategyForTarget(pBot, CurrentEnemy); + case AVH_USER3_ALIEN_PLAYER5: + return GetOnosCombatStrategyForTarget(pBot, CurrentEnemy); + default: + return COMBAT_STRATEGY_IGNORE; + } +} + +AvHAICombatStrategy GetSkulkCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + float CurrentHealthPercent = GetPlayerOverallHealthPercent(pBot->Edict); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + if (CurrentHealthPercent < 0.99f) + { + return COMBAT_STRATEGY_RETREAT; + } + } + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, CurrentEnemy->LastSeenLocation); + + bool bInAmbushRange = DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(15.0f)) && DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(5.0f)); + + // If we are rushing to defend something, ignore enemies who are not a threat to our target + if (pBot->CurrentTask && pBot->CurrentTask->TaskType == TASK_DEFEND) + { + if ((!CurrentEnemy->bHasLOS || DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(10.0f))) && !UTIL_PlayerHasLOSToEntity(CurrentEnemy->EnemyEdict, pBot->CurrentTask->TaskTarget, UTIL_MetresToGoldSrcUnits(30.0f), false)) + { + return COMBAT_STRATEGY_IGNORE; + } + + return COMBAT_STRATEGY_ATTACK; + } + + // Jig's up, just get in there + if (CurrentEnemy->bHasLOS && DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(5.0f))) + { + return COMBAT_STRATEGY_ATTACK; + } + + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = AIMGR_GetEnemyTeam(BotTeam); + + int NumEnemyAllies = AITAC_GetNumPlayersOnTeamWithLOS(EnemyTeam, CurrentEnemy->EnemyEdict->v.origin, UTIL_MetresToGoldSrcUnits(20.0f), CurrentEnemy->EnemyEdict); + int NumFriends = AITAC_GetNumPlayersOnTeamWithLOS(BotTeam, pBot->Edict->v.origin, UTIL_MetresToGoldSrcUnits(5.0f), pBot->Edict); + + Vector EnemyFacing = UTIL_GetForwardVector2D(CurrentEnemy->EnemyEdict->v.angles); + Vector OurOrientation = UTIL_GetVectorNormal2D(pBot->Edict->v.origin - CurrentEnemy->EnemyEdict->v.origin); + + float FacingDot = UTIL_GetDotProduct2D(EnemyFacing, OurOrientation); + + if (NumEnemyAllies <= NumFriends && (DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(10.0f)) || FacingDot < 0.4f)) + { + return COMBAT_STRATEGY_ATTACK; + } + + Vector EnemyVelocity = UTIL_GetVectorNormal2D(CurrentEnemy->LastSeenVelocity); + + float MoveDot = UTIL_GetDotProduct2D(EnemyVelocity, OurOrientation); + + if (MoveDot > 0.0f) + { + return COMBAT_STRATEGY_AMBUSH; + } + else + { + return COMBAT_STRATEGY_SKIRMISH; + } + + return COMBAT_STRATEGY_ATTACK; +} + +AvHAICombatStrategy GetGorgeCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + float CurrentHealthPercent = GetPlayerOverallHealthPercent(pBot->Edict); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + if (CurrentHealthPercent < 0.99f) + { + return COMBAT_STRATEGY_RETREAT; + } + } + + return COMBAT_STRATEGY_ATTACK; +} + +AvHAICombatStrategy GetLerkCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = CurrentEnemy->EnemyPlayer->GetTeam(); + + float CurrentHealthPercent = GetPlayerOverallHealthPercent(pBot->Edict); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + if (CurrentHealthPercent < 0.99f) + { + return COMBAT_STRATEGY_RETREAT; + } + } + + edict_t* EnemyEdict = CurrentEnemy->EnemyEdict; + + float EnemyHealthPercent = GetPlayerOverallHealthPercent(EnemyEdict); + int NumAllies = AITAC_GetNumPlayersOnTeamWithLOS(EnemyTeam, EnemyEdict->v.origin, UTIL_MetresToGoldSrcUnits(20.0f), EnemyEdict); + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, EnemyEdict->v.origin); + + float RetreatHealthPercent = (NumAllies > 1) ? 0.5f : 0.35f; + + if (CurrentHealthPercent < RetreatHealthPercent) + { + return COMBAT_STRATEGY_RETREAT; + } + + // Player has a deadly weapon if they have a shotgun, or they have an HMG with ammo in the chamber and are not reloading (we can strike if they are!) + bool bEnemyHasDeadlyWeapon = (PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_HMG) && UTIL_GetPlayerPrimaryWeaponClipAmmo(CurrentEnemy->EnemyPlayer) > 10 && !IsPlayerReloading(CurrentEnemy->EnemyPlayer)) || PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_SHOTGUN); + + // Should we YOLO? + if (NumAllies == 0 && !bEnemyHasDeadlyWeapon && !PlayerHasHeavyArmour(EnemyEdict)) + { + Vector EnemyFacing = UTIL_GetForwardVector2D(EnemyEdict->v.angles); + Vector OurOrientation = UTIL_GetVectorNormal2D(pBot->Edict->v.origin - EnemyEdict->v.origin); + + float FacingDot = UTIL_GetDotProduct2D(EnemyFacing, OurOrientation); + + if (CurrentHealthPercent > 0.7f || DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(5.0)) || FacingDot < 0.0f) + { + return COMBAT_STRATEGY_ATTACK; + } + } + + // If we are getting low on health, or the player has a weapon that would shred us... + if (CurrentHealthPercent < 0.6f || bEnemyHasDeadlyWeapon) + { + Vector EnemyFacing = UTIL_GetForwardVector2D(EnemyEdict->v.angles); + Vector OurOrientation = UTIL_GetVectorNormal2D(pBot->Edict->v.origin - EnemyEdict->v.origin); + + float FacingDot = UTIL_GetDotProduct2D(EnemyFacing, OurOrientation); + + // Only close in for the kill if the enemy is alone, weak that they won't have time to fight back, we can close the distance quickly, and they're not expecting us + if (EnemyHealthPercent < 0.5f && DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(10.0f)) && NumAllies == 0 && FacingDot < 0.0f) + { + return COMBAT_STRATEGY_ATTACK; + } + else + { + return COMBAT_STRATEGY_SKIRMISH; + } + } + + // We are in good shape and the enemy doesn't have a nasty weapon that could hurt us. Go for the kill if they're low on health and are alone + + if (EnemyHealthPercent < 0.5f && NumAllies == 0) + { + return COMBAT_STRATEGY_ATTACK; + } + else + { + return COMBAT_STRATEGY_SKIRMISH; + } + } + +AvHAICombatStrategy GetFadeCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = AIMGR_GetEnemyTeam(BotTeam); + + edict_t* EnemyEdict = CurrentEnemy->EnemyEdict; + + float CurrentHealthPercent = GetPlayerOverallHealthPercent(pBot->Edict); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + if (CurrentHealthPercent < 0.99f) + { + // We must be fade or onos, be more nuanced about when we decide to jump back into combat + + float MinHealthPercent = 0.5f; + + // Generally don't return to combat until we're at least at 50% capacity + if (CurrentHealthPercent < MinHealthPercent) { return COMBAT_STRATEGY_RETREAT; } + + // If our enemy has a more painful weapon like a shotgun or HMG, make sure we're a little more healed up + if (PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_HMG) || PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_SHOTGUN)) + { + MinHealthPercent += 0.15f; + } + + int NumAllies = AITAC_GetNumPlayersOnTeamWithLOS(CurrentEnemy->EnemyPlayer->GetTeam(), EnemyEdict->v.origin, UTIL_MetresToGoldSrcUnits(20.0f), EnemyEdict); + + if (NumAllies > 0) + { + MinHealthPercent += (0.15f * (float)NumAllies); + } + + MinHealthPercent = clampf(MinHealthPercent, 0.0f, 0.99f); + + // We don't feel strong enough to tackle the challenge yet + if (CurrentHealthPercent < MinHealthPercent) { return COMBAT_STRATEGY_RETREAT; } + } + } + + // Player has a deadly weapon if they have a shotgun, or they have an HMG with ammo in the chamber and are not reloading (we can strike if they are!) + bool bEnemyHasDeadlyWeapon = (PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_HMG) && UTIL_GetPlayerPrimaryWeaponClipAmmo(CurrentEnemy->EnemyPlayer) > 10 && !IsPlayerReloading(CurrentEnemy->EnemyPlayer)) || PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_SHOTGUN); + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, EnemyEdict->v.origin); + + int NumAllies = AITAC_GetNumPlayersOnTeamWithLOS(EnemyTeam, EnemyEdict->v.origin, UTIL_MetresToGoldSrcUnits(20.0f), EnemyEdict); + + // If we are rushing to defend something, ignore enemies who are not a threat to our target + if (pBot->CurrentTask && pBot->CurrentTask->TaskType == TASK_DEFEND) + { + if ((!CurrentEnemy->bHasLOS || DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(10.0f))) && !UTIL_PlayerHasLOSToEntity(CurrentEnemy->EnemyEdict, pBot->CurrentTask->TaskTarget, UTIL_MetresToGoldSrcUnits(30.0f), false)) + { + return COMBAT_STRATEGY_IGNORE; + } + } + + // First, check if we should get the hell out of dodge + float RetreatHealth = 0.33f; + + if (DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(5.0f)) && bEnemyHasDeadlyWeapon) + { + RetreatHealth = 0.5f; + } + + if (CurrentEnemy->bHasLOS && CurrentHealthPercent < RetreatHealth) + { + return COMBAT_STRATEGY_RETREAT; + } + + Vector EnemyFacing = UTIL_GetForwardVector2D(EnemyEdict->v.angles); + Vector OurOrientation = UTIL_GetVectorNormal2D(pBot->Edict->v.origin - EnemyEdict->v.origin); + + float FacingDot = UTIL_GetDotProduct2D(EnemyFacing, OurOrientation); + + // Can we skirmish? + if (PlayerHasWeapon(pBot->Player, WEAPON_FADE_ACIDROCKET)) + { + if (DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(5.0f))) + { + if ((bEnemyHasDeadlyWeapon && (FacingDot > 0.5f || NumAllies > 0)) || NumAllies > 2) + { + return COMBAT_STRATEGY_SKIRMISH; + } + } + } + else + { + if ((bEnemyHasDeadlyWeapon && (FacingDot > 0.5f || NumAllies > 0)) || NumAllies > 2) + { + Vector EnemyVelocity = UTIL_GetVectorNormal2D(CurrentEnemy->LastSeenVelocity); + + float MoveDot = UTIL_GetDotProduct2D(EnemyVelocity, OurOrientation); + + if (MoveDot > 0.0f) + { + return COMBAT_STRATEGY_AMBUSH; + } + } + } + + return COMBAT_STRATEGY_ATTACK; + +} + +AvHAICombatStrategy GetOnosCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = AIMGR_GetEnemyTeam(BotTeam); + + edict_t* EnemyEdict = CurrentEnemy->EnemyEdict; + + float CurrentHealthPercent = GetPlayerOverallHealthPercent(pBot->Edict); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + if (CurrentHealthPercent < 0.99f) + { + // We must be fade or onos, be more nuanced about when we decide to jump back into combat + + float MinHealthPercent = 0.4f; + + // Generally don't return to combat until we're at least at 50% capacity + if (CurrentHealthPercent < MinHealthPercent) { return COMBAT_STRATEGY_RETREAT; } + + // If our enemy has a more painful weapon like a shotgun or HMG, make sure we're a little more healed up + if (PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_HMG) || PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_SHOTGUN)) + { + MinHealthPercent += 0.1f; + } + + int NumAllies = AITAC_GetNumPlayersOnTeamWithLOS(CurrentEnemy->EnemyPlayer->GetTeam(), EnemyEdict->v.origin, UTIL_MetresToGoldSrcUnits(20.0f), EnemyEdict); + + if (NumAllies > 0) + { + MinHealthPercent += (0.1f * (float)NumAllies); + } + + MinHealthPercent = clampf(MinHealthPercent, 0.0f, 0.99f); + + // We don't feel strong enough to tackle the challenge yet + if (CurrentHealthPercent < MinHealthPercent) { return COMBAT_STRATEGY_RETREAT; } + } + } + + // Player has a deadly weapon if they have a shotgun, or they have an HMG with ammo in the chamber and are not reloading (we can strike if they are!) + bool bEnemyHasDeadlyWeapon = (PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_HMG) && UTIL_GetPlayerPrimaryWeaponClipAmmo(CurrentEnemy->EnemyPlayer) > 10 && !IsPlayerReloading(CurrentEnemy->EnemyPlayer)) || PlayerHasWeapon(CurrentEnemy->EnemyPlayer, WEAPON_MARINE_SHOTGUN); + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, EnemyEdict->v.origin); + + int NumAllies = AITAC_GetNumPlayersOnTeamWithLOS(EnemyTeam, EnemyEdict->v.origin, UTIL_MetresToGoldSrcUnits(20.0f), EnemyEdict); + + // If we are rushing to defend something, ignore enemies who are not a threat to our target + if (pBot->CurrentTask && pBot->CurrentTask->TaskType == TASK_DEFEND) + { + if ((!CurrentEnemy->bHasLOS || DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(10.0f))) && !UTIL_PlayerHasLOSToEntity(CurrentEnemy->EnemyEdict, pBot->CurrentTask->TaskTarget, UTIL_MetresToGoldSrcUnits(30.0f), false)) + { + return COMBAT_STRATEGY_IGNORE; + } + } + + // First, check if we should get the hell out of dodge + float RetreatHealth = 0.25f; + + if (DistToEnemy < sqrf(UTIL_MetresToGoldSrcUnits(5.0f)) && (bEnemyHasDeadlyWeapon || NumAllies > 1)) + { + RetreatHealth = 0.35f; + } + + if (CurrentEnemy->bHasLOS && CurrentHealthPercent < RetreatHealth) + { + return COMBAT_STRATEGY_RETREAT; + } + + return COMBAT_STRATEGY_ATTACK; +} + +AvHAICombatStrategy GetMarineCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy) +{ + return COMBAT_STRATEGY_ATTACK; +} + AvHAIPlayerTask* AIPlayerGetNextTask(AvHAIPlayer* pBot) { @@ -1876,24 +2353,23 @@ void AIPlayerNSMarineThink(AvHAIPlayer* pBot) if (!pBot->CurrentTask) { pBot->CurrentTask = &pBot->PrimaryBotTask; } - if (gpGlobals->time < pBot->BotNextTaskEvaluationTime) + if (gpGlobals->time >= pBot->BotNextTaskEvaluationTime) { - if (pBot->CurrentTask && pBot->CurrentTask->TaskType != TASK_NONE) - { - BotProgressTask(pBot, pBot->CurrentTask); - return; - } + pBot->BotNextTaskEvaluationTime = gpGlobals->time + frandrange(0.2f, 0.5f); + + AITASK_BotUpdateAndClearTasks(pBot); + + AIPlayerSetPrimaryMarineTask(pBot, &pBot->PrimaryBotTask); + AIPlayerSetSecondaryMarineTask(pBot, &pBot->SecondaryBotTask); } - pBot->BotNextTaskEvaluationTime = gpGlobals->time + frandrange(0.2f, 0.5f); - - AITASK_BotUpdateAndClearTasks(pBot); - - AIPlayerSetPrimaryMarineTask(pBot, &pBot->PrimaryBotTask); - AIPlayerSetSecondaryMarineTask(pBot, &pBot->SecondaryBotTask); - pBot->CurrentTask = AIPlayerGetNextTask(pBot); + if (pBot->CurrentEnemy > -1) + { + if (MarineCombatThink(pBot)) { return; } + } + if (pBot->CurrentTask && pBot->CurrentTask->TaskType != TASK_NONE) { BotProgressTask(pBot, pBot->CurrentTask); @@ -1905,6 +2381,197 @@ void AIPlayerNSMarineThink(AvHAIPlayer* pBot) } } +void MarineHuntEnemy(AvHAIPlayer* pBot, enemy_status* TrackedEnemy) +{ + edict_t* CurrentEnemy = TrackedEnemy->EnemyEdict; + + if (FNullEnt(CurrentEnemy) || IsPlayerDead(CurrentEnemy)) { return; } + + float TimeSinceLastSighting = (gpGlobals->time - TrackedEnemy->LastSeenTime); + + // If the enemy is being motion tracked, or the last seen time was within the last 5 seconds, and the suspected location is close enough, then throw a grenade! + if (PlayerHasWeapon(pBot->Player, WEAPON_MARINE_GRENADE) || ((PlayerHasWeapon(pBot->Player, WEAPON_MARINE_GL) && (UTIL_GetPlayerPrimaryWeaponClipAmmo(pBot->Player) > 0 || UTIL_GetPlayerPrimaryAmmoReserve(pBot->Player) > 0)))) + { + if (TimeSinceLastSighting < 5.0f && vDist3DSq(pBot->Edict->v.origin, TrackedEnemy->LastSeenLocation) <= sqrf(UTIL_MetresToGoldSrcUnits(10.0f))) + { + Vector GrenadeThrowLocation = UTIL_GetGrenadeThrowTarget(pBot->Edict, TrackedEnemy->LastSeenLocation, UTIL_MetresToGoldSrcUnits(5.0f), false); + + if (GrenadeThrowLocation != ZERO_VECTOR) + { + BotThrowGrenadeAtTarget(pBot, GrenadeThrowLocation); + return; + } + } + } + + pBot->DesiredCombatWeapon = BotMarineChooseBestWeapon(pBot, CurrentEnemy); + + if (GetPlayerCurrentWeapon(pBot->Player) != pBot->DesiredCombatWeapon) { return; } + + if (UTIL_PointIsReachable(pBot->BotNavInfo.NavProfile, pBot->Edict->v.origin, TrackedEnemy->LastSeenLocation, max_player_use_reach)) + { + MoveTo(pBot, TrackedEnemy->LastFloorPosition, MOVESTYLE_NORMAL); + } + + return; +} + +void BotThrowGrenadeAtTarget(AvHAIPlayer* pBot, const Vector TargetPoint) +{ + if (PlayerHasWeapon(pBot->Player, WEAPON_MARINE_GL) && (UTIL_GetPlayerPrimaryWeaponClipAmmo(pBot->Player) > 0 || UTIL_GetPlayerPrimaryAmmoReserve(pBot->Player) > 0)) + { + pBot->DesiredCombatWeapon = WEAPON_MARINE_GL; + + } + else + { + pBot->DesiredCombatWeapon = WEAPON_MARINE_GRENADE; + } + + if (GetPlayerCurrentWeapon(pBot->Player) != pBot->DesiredCombatWeapon) + { + return; + } + + + Vector ThrowAngle = GetPitchForProjectile(pBot->CurrentEyePosition, TargetPoint, UTIL_GetProjectileVelocityForWeapon(GetPlayerCurrentWeapon(pBot->Player)), GOLDSRC_GRAVITY); + + ThrowAngle = UTIL_GetVectorNormal(ThrowAngle); + + Vector ThrowTargetLocation = pBot->CurrentEyePosition + (ThrowAngle * 200.0f); + + BotLookAt(pBot, ThrowTargetLocation); + + if (GetPlayerCurrentWeapon(pBot->Player) == WEAPON_MARINE_GL && UTIL_GetPlayerPrimaryWeaponClipAmmo(pBot->Player) == 0) + { + BotReloadCurrentWeapon(pBot); + return; + } + + BotShootLocation(pBot, GetPlayerCurrentWeapon(pBot->Player), ThrowTargetLocation); +} + +bool MarineCombatThink(AvHAIPlayer* pBot) +{ + edict_t* pEdict = pBot->Edict; + + if (pBot->CurrentEnemy < 0) { return false; } + + edict_t* CurrentEnemy = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyEdict; + enemy_status* TrackedEnemyRef = &pBot->TrackedEnemies[pBot->CurrentEnemy]; + + // ENEMY IS OUT OF SIGHT + + if (!TrackedEnemyRef->bHasLOS) + { + MarineHuntEnemy(pBot, TrackedEnemyRef); + return true; + } + + // ENEMY IS VISIBLE + + AvHAIWeapon DesiredCombatWeapon = BotMarineChooseBestWeapon(pBot, CurrentEnemy); + AvHAIWeapon PrimaryWeapon = UTIL_GetPlayerPrimaryWeapon(pBot->Player); + + BotAttackResult LOSCheck = PerformAttackLOSCheck(pBot, DesiredCombatWeapon, TrackedEnemyRef->LastSeenLocation, CurrentEnemy); + + if (LOSCheck == ATTACK_SUCCESS) + { + BotShootLocation(pBot, DesiredCombatWeapon, TrackedEnemyRef->LastSeenLocation); + } + + float DistFromEnemy = vDist2DSq(pBot->Edict->v.origin, CurrentEnemy->v.origin); + + if (DesiredCombatWeapon != WEAPON_MARINE_KNIFE) + { + if (DistFromEnemy < sqrf(100.0f)) + { + if (IsPlayerReloading(pBot->Player) && CanInterruptWeaponReload(GetPlayerCurrentWeapon(pBot->Player)) && GetPlayerCurrentWeaponClipAmmo(pBot->Player) > 0) + { + InterruptReload(pBot); + } + BotJump(pBot); + } + } + + // We're going to have the marine always try and use their primary weapon, which means + // that they will try and put enough distance between themselves and the enemy to use it effectively, + // and retreat if they need to reload or are out of ammo + + + // We are using our primary weapon right now (has ammo left in the clip) + if (DesiredCombatWeapon == PrimaryWeapon) + { + BotLookAt(pBot, CurrentEnemy); + if (LOSCheck == ATTACK_OUTOFRANGE) + { + MoveTo(pBot, TrackedEnemyRef->LastFloorPosition, MOVESTYLE_NORMAL); + if (gpGlobals->time - TrackedEnemyRef->LastSeenTime > 5.0f) + { + BotReloadWeapons(pBot); + } + return true; + } + + // Note that we already do visibility checks above, so blocked here means there is another player or structure in the way + if (LOSCheck == ATTACK_BLOCKED) + { + edict_t* TracedEntity = UTIL_TraceEntity(pEdict, pBot->CurrentEyePosition, UTIL_GetCentreOfEntity(CurrentEnemy)); + + // Just blast through an alien structure if it's in the way + if (!FNullEnt(TracedEntity) && TracedEntity != CurrentEnemy) + { + if (TracedEntity->v.team != 0 && TracedEntity->v.team != pEdict->v.team) + { + BotShootTarget(pBot, DesiredCombatWeapon, TracedEntity); + } + } + + float MinDesiredDist = GetMinIdealWeaponRange(DesiredCombatWeapon); + + Vector EngagementLocation = pBot->BotNavInfo.TargetDestination; + + float EngagementLocationDist = vDist2DSq(EngagementLocation, CurrentEnemy->v.origin); + + if (!EngagementLocation || EngagementLocationDist < sqrf(MinDesiredDist) || PerformAttackLOSCheck(EngagementLocation + Vector(0.0f, 0.0f, 50.0f), DesiredCombatWeapon, CurrentEnemy) != ATTACK_SUCCESS) + { + EngagementLocation = UTIL_GetRandomPointOnNavmeshInRadius(pBot->BotNavInfo.NavProfile, CurrentEnemy->v.origin, UTIL_MetresToGoldSrcUnits(5.0f)); + + if (EngagementLocation != ZERO_VECTOR && PerformAttackLOSCheck(EngagementLocation + Vector(0.0f, 0.0f, 50.0f), DesiredCombatWeapon, CurrentEnemy) != ATTACK_SUCCESS) + { + EngagementLocation = ZERO_VECTOR; + } + } + + MoveTo(pBot, EngagementLocation, MOVESTYLE_NORMAL); + return true; + } + + if (LOSCheck == ATTACK_SUCCESS) + { + float MinDesiredDist = GetMinIdealWeaponRange(DesiredCombatWeapon); + Vector Orientation = UTIL_GetVectorNormal2D(CurrentEnemy->v.origin - pBot->Edict->v.origin); + + float EnemyMoveDot = UTIL_GetDotProduct2D(UTIL_GetVectorNormal2D(CurrentEnemy->v.velocity), -Orientation); + + // Enemy is too close for comfort, or is moving towards us. Back up + if (DistFromEnemy < MinDesiredDist || EnemyMoveDot > 0.7f) + { + Vector RetreatLocation = pBot->CurrentFloorPosition - (Orientation * 50.0f); + + if (UTIL_PointIsDirectlyReachable(pBot->CurrentFloorPosition, RetreatLocation)) + { + MoveDirectlyTo(pBot, RetreatLocation); + } + } + } + + return true; + } + + return true; +} + void AIPlayerSetPrimaryMarineTask(AvHAIPlayer* pBot, AvHAIPlayerTask* Task) { switch (pBot->BotRole) @@ -2180,31 +2847,25 @@ void AIPlayerNSAlienThink(AvHAIPlayer* pBot) return; } - if (pBot->BotRole == BOT_ROLE_ASSAULT) - { - BotEvolveLifeform(pBot, pBot->Edict->v.origin, ALIEN_LIFEFORM_ONE); - } - if (!pBot->CurrentTask) { pBot->CurrentTask = &pBot->PrimaryBotTask; } - if (gpGlobals->time < pBot->BotNextTaskEvaluationTime) + if (gpGlobals->time >= pBot->BotNextTaskEvaluationTime) { - if (pBot->CurrentTask && pBot->CurrentTask->TaskType != TASK_NONE) - { - BotProgressTask(pBot, pBot->CurrentTask); - return; - } + pBot->BotNextTaskEvaluationTime = gpGlobals->time + frandrange(0.2f, 0.5f); + + AITASK_BotUpdateAndClearTasks(pBot); + + AIPlayerSetPrimaryAlienTask(pBot, &pBot->PrimaryBotTask); + AIPlayerSetSecondaryAlienTask(pBot, &pBot->SecondaryBotTask); } - pBot->BotNextTaskEvaluationTime = gpGlobals->time + frandrange(0.2f, 0.5f); - - AITASK_BotUpdateAndClearTasks(pBot); - - AIPlayerSetPrimaryAlienTask(pBot, &pBot->PrimaryBotTask); - AIPlayerSetSecondaryAlienTask(pBot, &pBot->SecondaryBotTask); - pBot->CurrentTask = AIPlayerGetNextTask(pBot); + if (pBot->CurrentEnemy > -1) + { + if (AlienCombatThink(pBot)) { return; } + } + if (pBot->CurrentTask && pBot->CurrentTask->TaskType != TASK_NONE) { BotProgressTask(pBot, pBot->CurrentTask); @@ -2998,14 +3659,42 @@ void AIPlayerSetAlienAssaultPrimaryTask(AvHAIPlayer* pBot, AvHAIPlayerTask* Task } } - - // TODO: Attack enemy hive/base edict_t* EnemyChair = AITAC_GetCommChair(EnemyTeam); if (!FNullEnt(EnemyChair)) { AITASK_SetAttackTask(pBot, Task, EnemyChair, false); + return; + } + + vector AllEnemyPlayers = AIMGR_GetAllPlayersOnTeam(EnemyTeam); + edict_t* TargetPlayer = nullptr; + + float MinDist = 0.0f; + + for (auto it = AllEnemyPlayers.begin(); it != AllEnemyPlayers.end(); it++) + { + AvHPlayer* ThisPlayer = (*it); + + if (!ThisPlayer) { continue; } + + edict_t* PlayerEdict = ThisPlayer->edict(); + + if (!IsPlayerActiveInGame(PlayerEdict)) { continue; } + + float ThisDist = vDist2DSq(PlayerEdict->v.origin, pBot->Edict->v.origin); + + if (FNullEnt(TargetPlayer) || ThisDist < MinDist) + { + TargetPlayer = PlayerEdict; + MinDist = ThisDist; + } + } + + if (!FNullEnt(TargetPlayer)) + { + MoveTo(pBot, UTIL_GetFloorUnderEntity(TargetPlayer), MOVESTYLE_NORMAL); } } @@ -3019,7 +3708,7 @@ void AIPlayerSetAlienHarasserPrimaryTask(AvHAIPlayer* pBot, AvHAIPlayerTask* Tas { if (Task->TaskType == TASK_EVOLVE && Task->Evolution == ALIEN_LIFEFORM_THREE) { return; } - vector AllTeamHives = AITAC_GetAllTeamHives(pBot->Player->GetTeam()); + vector AllTeamHives = AITAC_GetAllTeamHives(pBot->Player->GetTeam(), false); AvHAIHiveDefinition* NearestHive = nullptr; float MinDist = 0.0f; @@ -3143,4 +3832,738 @@ void AIPlayerSetAlienHarasserPrimaryTask(AvHAIPlayer* pBot, AvHAIPlayerTask* Tas void AIPlayerSetSecondaryAlienTask(AvHAIPlayer* pBot, AvHAIPlayerTask* Task) { +} + +bool AlienCombatThink(AvHAIPlayer* pBot) +{ + if (pBot->CurrentEnemy > -1) + { + edict_t* CurrentEnemy = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyEdict; + + pBot->CurrentCombatStrategy = GetBotCombatStrategyForTarget(pBot, &pBot->TrackedEnemies[pBot->CurrentEnemy]); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_IGNORE) { return false; } + + switch (pBot->Player->GetUser3()) + { + case AVH_USER3_ALIEN_PLAYER1: + return SkulkCombatThink(pBot); + case AVH_USER3_ALIEN_PLAYER2: + return GorgeCombatThink(pBot); + case AVH_USER3_ALIEN_PLAYER3: + return LerkCombatThink(pBot); + case AVH_USER3_ALIEN_PLAYER4: + return FadeCombatThink(pBot); + case AVH_USER3_ALIEN_PLAYER5: + return OnosCombatThink(pBot); + default: + return false; + } + } + + return false; +} + +bool SkulkCombatThink(AvHAIPlayer* pBot) +{ + edict_t* pEdict = pBot->Edict; + + AvHPlayer* EnemyPlayer = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyPlayer; + edict_t* CurrentEnemy = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyEdict; + enemy_status* TrackedEnemyRef = &pBot->TrackedEnemies[pBot->CurrentEnemy]; + + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = EnemyPlayer->GetTeam(); + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, CurrentEnemy->v.origin); + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + edict_t* NearestHealingSource = AITAC_AlienFindNearestHealingSource(pBot->Player->GetTeam(), pBot->Edict->v.origin, pBot->Edict, true); + + // Run away if low on health and have a healing spot + if (!FNullEnt(NearestHealingSource)) + { + float DesiredDistFromHealingSource = (IsEdictPlayer(NearestHealingSource)) ? UTIL_MetresToGoldSrcUnits(2.0f) : UTIL_MetresToGoldSrcUnits(5.0f); + + bool bOutOfEnemyLOS = !UTIL_PlayerHasLOSToEntity(CurrentEnemy, pBot->Edict, UTIL_GoldSrcUnitsToMetres(30.0f), false); + + float DistFromHealingSourceSq = vDist2DSq(pBot->Edict->v.origin, NearestHealingSource->v.origin); + + bool bInHealingRange = (DistFromHealingSourceSq <= sqrf(DesiredDistFromHealingSource)); + + if (!bInHealingRange) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + if (bOutOfEnemyLOS) + { + if (bInHealingRange) + { + BotLookAt(pBot, TrackedEnemyRef->LastLOSPosition); + } + else + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + } + + return true; + } + + if (!UTIL_PlayerHasLOSToLocation(TrackedEnemyRef->EnemyEdict, UTIL_GetEntityGroundLocation(NearestHealingSource) + Vector(0.0f, 0.0f, 16.0f), UTIL_MetresToGoldSrcUnits(30.0f))) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + } + + return false; + } + + bool bShouldBreakAmbush = false; + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_AMBUSH) + { + bShouldBreakAmbush = DistToEnemy < ((TrackedEnemyRef->bHasLOS) ? sqrf(UTIL_MetresToGoldSrcUnits(5.0f)) : sqrf(UTIL_MetresToGoldSrcUnits(3.0f))); + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_ATTACK || (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_AMBUSH && bShouldBreakAmbush)) + { + + AvHAIWeapon DesiredWeapon = WEAPON_SKULK_BITE; + + // If we have xenocide, then choose it if we have lots of good targets in blast radius + if (PlayerHasWeapon(pBot->Player, WEAPON_SKULK_XENOCIDE)) + { + AvHTeamNumber EnemyTeam = AIMGR_GetEnemyTeam(pBot->Player->GetTeam()); + float XenocideRadius = GetMaxIdealWeaponRange(WEAPON_SKULK_XENOCIDE); + + // Add one to include the target themselves + int NumEnemyTargetsInArea = AITAC_GetNumPlayersOnTeamWithLOS(EnemyTeam, CurrentEnemy->v.origin, XenocideRadius, CurrentEnemy) + 1; + + if (NumEnemyTargetsInArea <= 2) + { + AvHTeam* EnemyTeamRef = GetGameRules()->GetTeam(EnemyTeam); + + if (EnemyTeamRef) + { + AvHAIDeployableStructureType StructureSearchType = (EnemyTeamRef->GetTeamType() == AVH_CLASS_TYPE_MARINE) ? SEARCH_ALL_MARINE_STRUCTURES : SEARCH_ALL_ALIEN_STRUCTURES; + + DeployableSearchFilter SearchFilter; + SearchFilter.DeployableTypes = StructureSearchType; + SearchFilter.MaxSearchRadius = XenocideRadius; + SearchFilter.DeployableTeam = EnemyTeam; + + NumEnemyTargetsInArea += AITAC_GetNumDeployablesNearLocation(CurrentEnemy->v.origin, &SearchFilter); + } + } + + // We're going to use Xenocide + if (NumEnemyTargetsInArea > 2) + { + DesiredWeapon = WEAPON_SKULK_XENOCIDE; + } + } + + if (DesiredWeapon != WEAPON_SKULK_XENOCIDE) + { + if (!IsPlayerParasited(CurrentEnemy) && DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(5.0f))) + { + DesiredWeapon = WEAPON_SKULK_PARASITE; + } + } + + BotAttackResult LOSCheck = PerformAttackLOSCheck(pBot, DesiredWeapon, CurrentEnemy); + Vector MoveTarget = UTIL_GetFloorUnderEntity(CurrentEnemy); + + if (LOSCheck == ATTACK_SUCCESS) + { + BotShootTarget(pBot, DesiredWeapon, CurrentEnemy); + + Vector EnemyFacing = UTIL_GetForwardVector2D(CurrentEnemy->v.angles); + Vector BotFacing = UTIL_GetVectorNormal2D(CurrentEnemy->v.origin - pBot->Edict->v.origin); + + float Dot = UTIL_GetDotProduct2D(EnemyFacing, BotFacing); + + if (Dot < 0.0f) + { + Vector TargetLocation = MoveTarget; + Vector BehindPlayer = TargetLocation - (UTIL_GetForwardVector2D(CurrentEnemy->v.v_angle) * 50.0f); + + if (UTIL_PointIsDirectlyReachable(pBot->BotNavInfo.NavProfile, pBot->CurrentFloorPosition, BehindPlayer)) + { + MoveTarget = BehindPlayer; + } + } + } + + MoveTo(pBot, MoveTarget, MOVESTYLE_NORMAL); + + if (DistToEnemy > sqrf(UTIL_MetresToGoldSrcUnits(5.0f))) + { + if (CanBotLeap(pBot)) + { + BotLeap(pBot, CurrentEnemy->v.origin); + } + else + { + if (pBot->BotNavInfo.CurrentPath.size() == 0 || pBot->BotNavInfo.CurrentPathPoint == pBot->BotNavInfo.CurrentPath.end()) { return true; } + + // EVASIVE MANOEUVRES! Only do this if we're running along the floor and aren't approaching a path point (so we don't stray off the path) + if (pBot->BotNavInfo.CurrentPathPoint->flag == SAMPLE_POLYFLAGS_WALK && vDist2DSq(pBot->Edict->v.origin, pBot->BotNavInfo.CurrentPathPoint->Location) > sqrf(50.0f)) + { + Vector RightDir = UTIL_GetCrossProduct(pBot->desiredMovementDir, UP_VECTOR); + + pBot->desiredMovementDir = (pBot->BotNavInfo.bZig) ? UTIL_GetVectorNormal2D(pBot->desiredMovementDir + RightDir) : UTIL_GetVectorNormal2D(pBot->desiredMovementDir - RightDir); + + // Let's get ziggy with it + if (gpGlobals->time > pBot->BotNavInfo.NextZigTime) + { + pBot->BotNavInfo.bZig = !pBot->BotNavInfo.bZig; + pBot->BotNavInfo.NextZigTime = gpGlobals->time + frandrange(0.5f, 1.0f); + } + + BotMovementInputs(pBot); + } + } + } + + return true; + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_AMBUSH) + { + if (TrackedEnemyRef->bHasLOS) + { + if (vIsZero(pBot->LastSafeLocation)) + { + const AvHAIHiveDefinition* NearestHive = AITAC_GetActiveHiveNearestLocation(pBot->Player->GetTeam(), pBot->Edict->v.origin); + + if (NearestHive) + { + pBot->LastSafeLocation = NearestHive->FloorLocation; + } + } + + MoveTo(pBot, pBot->LastSafeLocation, MOVESTYLE_NORMAL); + BotLookAt(pBot, TrackedEnemyRef->LastSeenLocation); + + if (!IsPlayerParasited(CurrentEnemy)) + { + BotShootTarget(pBot, WEAPON_SKULK_PARASITE, CurrentEnemy); + } + + return true; + } + else + { + BotLookAt(pBot, (!vIsZero(TrackedEnemyRef->LastLOSPosition)) ? TrackedEnemyRef->LastLOSPosition : TrackedEnemyRef->LastSeenLocation); + } + + return true; + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_SKIRMISH) + { + if (TrackedEnemyRef->bHasLOS) + { + if (vIsZero(pBot->LastSafeLocation)) + { + const AvHAIHiveDefinition* NearestHive = AITAC_GetActiveHiveNearestLocation(pBot->Player->GetTeam(), pBot->Edict->v.origin); + + if (NearestHive) + { + pBot->LastSafeLocation = NearestHive->FloorLocation; + } + } + + if (GetPlayerEnergy(pBot->Edict) < GetEnergyCostForWeapon(WEAPON_SKULK_PARASITE)) + { + MoveTo(pBot, pBot->LastSafeLocation, MOVESTYLE_NORMAL); + } + + BotLookAt(pBot, TrackedEnemyRef->LastSeenLocation); + + BotShootTarget(pBot, WEAPON_SKULK_PARASITE, CurrentEnemy); + + return true; + } + else + { + if (GetPlayerEnergy(pBot->Edict) >= 0.9f) + { + MoveTo(pBot, TrackedEnemyRef->LastSeenLocation, MOVESTYLE_NORMAL); + return true; + } + BotLookAt(pBot, (!vIsZero(TrackedEnemyRef->LastLOSPosition)) ? TrackedEnemyRef->LastLOSPosition : TrackedEnemyRef->LastSeenLocation); + } + + return true; + } + + return false; +} + +bool GorgeCombatThink(AvHAIPlayer* pBot) +{ + return false; +} + +bool LerkCombatThink(AvHAIPlayer* pBot) +{ + + edict_t* CurrentEnemy = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyEdict; + + if (FNullEnt(CurrentEnemy) || !IsPlayerActiveInGame(CurrentEnemy)) { return false; } + + enemy_status* TrackedEnemyRef = &pBot->TrackedEnemies[pBot->CurrentEnemy]; + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + edict_t* NearestHealingSource = AITAC_AlienFindNearestHealingSource(pBot->Player->GetTeam(), pBot->Edict->v.origin, pBot->Edict, true); + + // Run away if low on health and have a healing spot + if (!FNullEnt(NearestHealingSource)) + { + float DesiredDistFromHealingSource = (IsEdictPlayer(NearestHealingSource)) ? UTIL_MetresToGoldSrcUnits(2.0f) : UTIL_MetresToGoldSrcUnits(5.0f); + + bool bOutOfEnemyLOS = !UTIL_PlayerHasLOSToEntity(CurrentEnemy, pBot->Edict, UTIL_GoldSrcUnitsToMetres(30.0f), false); + + float DistFromHealingSourceSq = vDist2DSq(pBot->Edict->v.origin, NearestHealingSource->v.origin); + + bool bInHealingRange = (DistFromHealingSourceSq <= sqrf(DesiredDistFromHealingSource)); + + Vector SporeLocation = (TrackedEnemyRef->bHasLOS) ? TrackedEnemyRef->LastSeenLocation : TrackedEnemyRef->LastLOSPosition; + + // We will cover our tracks with spores if we have a valid target location, we have enough energy, the area isn't affected by spores already and we have LOS to the spore location + bool bCanSpore = (SporeLocation != ZERO_VECTOR && GetPlayerEnergy(pBot->Edict) > (GetEnergyCostForWeapon(WEAPON_LERK_SPORES) * 1.1f) && !IsAreaAffectedBySpores(SporeLocation) && UTIL_QuickTrace(pBot->Edict, pBot->CurrentEyePosition, SporeLocation)); + + // If we are super low on health then just get the hell out of there + if (GetPlayerOverallHealthPercent(pBot->Edict) <= 0.2) { bCanSpore = false; } + + if (!bInHealingRange) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + + if (bCanSpore) + { + BotShootLocation(pBot, WEAPON_LERK_SPORES, SporeLocation); + } + + return true; + } + + if (bOutOfEnemyLOS) + { + if (bInHealingRange) + { + BotLookAt(pBot, TrackedEnemyRef->LastLOSPosition); + + if (bCanSpore) + { + BotShootLocation(pBot, WEAPON_LERK_SPORES, SporeLocation); + } + + } + else + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + + if (bCanSpore) + { + BotShootLocation(pBot, WEAPON_LERK_SPORES, SporeLocation); + } + } + + return true; + } + + if (!UTIL_PlayerHasLOSToLocation(TrackedEnemyRef->EnemyEdict, UTIL_GetEntityGroundLocation(NearestHealingSource) + Vector(0.0f, 0.0f, 16.0f), UTIL_MetresToGoldSrcUnits(30.0f))) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + } + + return true; + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_ATTACK) + { + pBot->DesiredCombatWeapon = WEAPON_LERK_BITE; + + if (vDist2DSq(pBot->Edict->v.origin, CurrentEnemy->v.origin) > sqrf(UTIL_MetresToGoldSrcUnits(5.0f))) + { + if (!IsAreaAffectedBySpores(CurrentEnemy->v.origin)) + { + pBot->DesiredCombatWeapon = WEAPON_LERK_SPORES; + } + } + + MoveTo(pBot, CurrentEnemy->v.origin, MOVESTYLE_NORMAL); + + BotAttackResult LOSCheck = PerformAttackLOSCheck(pBot, pBot->DesiredCombatWeapon, CurrentEnemy); + + if (LOSCheck == ATTACK_SUCCESS) + { + BotShootTarget(pBot, pBot->DesiredCombatWeapon, CurrentEnemy); + } + + return true; + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_SKIRMISH) + { + pBot->DesiredCombatWeapon = WEAPON_LERK_SPORES; + + if (GetPlayerCurrentWeapon(pBot->Player) != WEAPON_LERK_SPORES) { return true; } + + if (GetPlayerEnergy(pBot->Edict) < (GetEnergyCostForWeapon(WEAPON_LERK_SPORES) * 1.1f) + || IsAreaAffectedBySpores(CurrentEnemy->v.origin) + || GetTimeUntilPlayerNextRefire(pBot->Player) > 0.0f) + { + BotMoveStyle DesiredMoveStyle = (vDist2DSq(pBot->Edict->v.origin, pBot->LastSafeLocation) < sqrf(UTIL_MetresToGoldSrcUnits(3.0f))) ? MOVESTYLE_AMBUSH : MOVESTYLE_NORMAL; + + if (vIsZero(pBot->LastSafeLocation)) + { + const AvHAIHiveDefinition* NearestHive = AITAC_GetActiveHiveNearestLocation(pBot->Player->GetTeam(), pBot->Edict->v.origin); + + if (NearestHive) + { + pBot->LastSafeLocation = NearestHive->FloorLocation; + } + } + + MoveTo(pBot, pBot->LastSafeLocation, DesiredMoveStyle); + + if (DesiredMoveStyle != MOVESTYLE_NORMAL) + { + BotLookAt(pBot, TrackedEnemyRef->LastSeenLocation); + } + return true; + } + + BotAttackResult LOSCheck = PerformAttackLOSCheck(pBot, pBot->DesiredCombatWeapon, CurrentEnemy); + + if (LOSCheck == ATTACK_SUCCESS) + { + BotShootTarget(pBot, pBot->DesiredCombatWeapon, CurrentEnemy); + } + else + { + MoveTo(pBot, TrackedEnemyRef->LastSeenLocation, MOVESTYLE_AMBUSH); + } + + return true; + } + + return true; +} + +bool FadeCombatThink(AvHAIPlayer* pBot) +{ + edict_t* pEdict = pBot->Edict; + + AvHPlayer* EnemyPlayer = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyPlayer; + edict_t* CurrentEnemy = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyEdict; + enemy_status* TrackedEnemyRef = &pBot->TrackedEnemies[pBot->CurrentEnemy]; + + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = EnemyPlayer->GetTeam(); + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, CurrentEnemy->v.origin); + + bool bShouldBreakRetreat = false; + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + edict_t* NearestHealingSource = AITAC_AlienFindNearestHealingSource(pBot->Player->GetTeam(), pBot->Edict->v.origin, pBot->Edict, true); + + // Run away if low on health and have a healing spot + if (!FNullEnt(NearestHealingSource)) + { + float DesiredDistFromHealingSource = (IsEdictPlayer(NearestHealingSource)) ? UTIL_MetresToGoldSrcUnits(2.0f) : UTIL_MetresToGoldSrcUnits(5.0f); + + bool bOutOfEnemyLOS = !UTIL_PlayerHasLOSToEntity(CurrentEnemy, pBot->Edict, UTIL_GoldSrcUnitsToMetres(30.0f), false); + + float DistFromHealingSourceSq = vDist2DSq(pBot->Edict->v.origin, NearestHealingSource->v.origin); + + bool bInHealingRange = (DistFromHealingSourceSq <= sqrf(DesiredDistFromHealingSource)); + + if (!bInHealingRange) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + if (bOutOfEnemyLOS) + { + BotLookAt(pBot, TrackedEnemyRef->LastLOSPosition); + if (PlayerHasWeapon(pBot->Player, WEAPON_FADE_METABOLIZE)) + { + pBot->DesiredCombatWeapon = WEAPON_FADE_METABOLIZE; + + if (GetPlayerCurrentWeapon(pBot->Player) == WEAPON_FADE_METABOLIZE) + { + pBot->Button |= IN_ATTACK; + } + } + return true; + } + + if (!UTIL_PlayerHasLOSToLocation(TrackedEnemyRef->EnemyEdict, UTIL_GetEntityGroundLocation(NearestHealingSource) + Vector(0.0f, 0.0f, 16.0f), UTIL_MetresToGoldSrcUnits(30.0f))) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + // If the enemy can see the healing source, then we must go on the attack + bShouldBreakRetreat = true; + } + } + + bool bShouldBreakAmbush = false; + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_AMBUSH) + { + bShouldBreakAmbush = DistToEnemy < ((TrackedEnemyRef->bHasLOS) ? sqrf(UTIL_MetresToGoldSrcUnits(5.0f)) : sqrf(UTIL_MetresToGoldSrcUnits(3.0f))); + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_ATTACK || bShouldBreakAmbush || bShouldBreakRetreat) + { + AvHAIWeapon DesiredWeapon = WEAPON_FADE_SWIPE; + + BotAttackResult LOSCheck = PerformAttackLOSCheck(pBot, DesiredWeapon, CurrentEnemy); + Vector MoveTarget = UTIL_GetFloorUnderEntity(CurrentEnemy); + + float EnemySpeed = vSize2D(CurrentEnemy->v.velocity); + + if (LOSCheck == ATTACK_SUCCESS) + { + BotShootTarget(pBot, DesiredWeapon, CurrentEnemy); + + Vector EnemyFacing = UTIL_GetForwardVector2D(CurrentEnemy->v.angles); + Vector BotFacing = UTIL_GetVectorNormal2D(CurrentEnemy->v.origin - pBot->Edict->v.origin); + + float Dot = UTIL_GetDotProduct2D(EnemyFacing, BotFacing); + + if (EnemySpeed < 16.0f && Dot < 0.0f) + { + Vector TargetLocation = MoveTarget; + Vector BehindPlayer = TargetLocation - (UTIL_GetForwardVector2D(CurrentEnemy->v.v_angle) * 50.0f); + + if (UTIL_PointIsDirectlyReachable(pBot->BotNavInfo.NavProfile, pBot->CurrentFloorPosition, BehindPlayer)) + { + MoveTarget = BehindPlayer; + } + } + } + + MoveTarget = MoveTarget + (CurrentEnemy->v.velocity * 0.1f); + + MoveTo(pBot, MoveTarget, MOVESTYLE_NORMAL); + + if (LOSCheck == ATTACK_OUTOFRANGE && UTIL_PointIsDirectlyReachable(pBot->CurrentFloorPosition, MoveTarget)) + { + if (CanBotLeap(pBot)) + { + BotLeap(pBot, MoveTarget); + } + } + + return true; + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_AMBUSH) + { + if (TrackedEnemyRef->bHasLOS) + { + if (vIsZero(pBot->LastSafeLocation)) + { + const AvHAIHiveDefinition* NearestHive = AITAC_GetActiveHiveNearestLocation(pBot->Player->GetTeam(), pBot->Edict->v.origin); + + if (NearestHive) + { + pBot->LastSafeLocation = NearestHive->FloorLocation; + } + } + + MoveTo(pBot, pBot->LastSafeLocation, MOVESTYLE_NORMAL); + BotLookAt(pBot, TrackedEnemyRef->LastSeenLocation); + + if (PlayerHasWeapon(pBot->Player, WEAPON_FADE_ACIDROCKET)) + { + BotShootTarget(pBot, WEAPON_FADE_ACIDROCKET, CurrentEnemy); + } + + return true; + } + else + { + BotLookAt(pBot, (!vIsZero(TrackedEnemyRef->LastLOSPosition)) ? TrackedEnemyRef->LastLOSPosition : TrackedEnemyRef->LastSeenLocation); + + if (GetPlayerOverallHealthPercent(pBot->Edict) < 1.0f && PlayerHasWeapon(pBot->Player, WEAPON_FADE_METABOLIZE)) + { + pBot->DesiredCombatWeapon = WEAPON_FADE_METABOLIZE; + + if (GetPlayerCurrentWeapon(pBot->Player) == WEAPON_FADE_METABOLIZE) + { + pBot->Button |= IN_ATTACK; + } + } + } + + return true; + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_SKIRMISH) + { + if (TrackedEnemyRef->bHasLOS) + { + if (vIsZero(pBot->LastSafeLocation)) + { + const AvHAIHiveDefinition* NearestHive = AITAC_GetActiveHiveNearestLocation(pBot->Player->GetTeam(), pBot->Edict->v.origin); + + if (NearestHive) + { + pBot->LastSafeLocation = NearestHive->FloorLocation; + } + } + + if (GetPlayerEnergy(pBot->Edict) < (0.9f - (GetEnergyCostForWeapon(WEAPON_FADE_ACIDROCKET) * 4.0f)) || GetPlayerOverallHealthPercent(pBot->Edict) < 6.0f) + { + MoveTo(pBot, pBot->LastSafeLocation, MOVESTYLE_NORMAL); + } + else + { + Vector EnemyOrientation = UTIL_GetVectorNormal2D(CurrentEnemy->v.origin - pBot->Edict->v.origin); + Vector RightDir = UTIL_GetCrossProduct(EnemyOrientation, UP_VECTOR); + + pBot->desiredMovementDir = (pBot->BotNavInfo.bZig) ? UTIL_GetVectorNormal2D(pBot->desiredMovementDir + RightDir) : UTIL_GetVectorNormal2D(pBot->desiredMovementDir - RightDir); + + // Let's get ziggy with it + if (gpGlobals->time > pBot->BotNavInfo.NextZigTime) + { + pBot->BotNavInfo.bZig = !pBot->BotNavInfo.bZig; + pBot->BotNavInfo.NextZigTime = gpGlobals->time + frandrange(0.5f, 1.0f); + } + + BotMovementInputs(pBot); + } + + BotLookAt(pBot, TrackedEnemyRef->LastSeenLocation); + + BotShootTarget(pBot, WEAPON_FADE_ACIDROCKET, CurrentEnemy); + + return true; + } + else + { + if (GetPlayerEnergy(pBot->Edict) >= 0.9f && GetPlayerOverallHealthPercent(pBot->Edict) > 0.8f) + { + MoveTo(pBot, TrackedEnemyRef->LastSeenLocation, MOVESTYLE_NORMAL); + return true; + } + + if (GetPlayerOverallHealthPercent(pBot->Edict) < 1.0f && PlayerHasWeapon(pBot->Player, WEAPON_FADE_METABOLIZE)) + { + pBot->DesiredCombatWeapon = WEAPON_FADE_METABOLIZE; + + if (GetPlayerCurrentWeapon(pBot->Player) == WEAPON_FADE_METABOLIZE) + { + pBot->Button |= IN_ATTACK; + } + } + + BotLookAt(pBot, (!vIsZero(TrackedEnemyRef->LastLOSPosition)) ? TrackedEnemyRef->LastLOSPosition : TrackedEnemyRef->LastSeenLocation); + } + + return true; + } + + return true; +} + +bool OnosCombatThink(AvHAIPlayer* pBot) +{ + edict_t* pEdict = pBot->Edict; + + AvHPlayer* EnemyPlayer = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyPlayer; + edict_t* CurrentEnemy = pBot->TrackedEnemies[pBot->CurrentEnemy].EnemyEdict; + enemy_status* TrackedEnemyRef = &pBot->TrackedEnemies[pBot->CurrentEnemy]; + + AvHTeamNumber BotTeam = pBot->Player->GetTeam(); + AvHTeamNumber EnemyTeam = EnemyPlayer->GetTeam(); + + float DistToEnemy = vDist2DSq(pBot->Edict->v.origin, CurrentEnemy->v.origin); + + bool bShouldBreakRetreat = false; + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_RETREAT) + { + edict_t* NearestHealingSource = AITAC_AlienFindNearestHealingSource(pBot->Player->GetTeam(), pBot->Edict->v.origin, pBot->Edict, true); + + // Run away if low on health and have a healing spot + if (!FNullEnt(NearestHealingSource)) + { + float DesiredDistFromHealingSource = (IsEdictPlayer(NearestHealingSource)) ? UTIL_MetresToGoldSrcUnits(2.0f) : UTIL_MetresToGoldSrcUnits(5.0f); + + bool bOutOfEnemyLOS = !UTIL_PlayerHasLOSToEntity(CurrentEnemy, pBot->Edict, UTIL_GoldSrcUnitsToMetres(30.0f), false); + + float DistFromHealingSourceSq = vDist2DSq(pBot->Edict->v.origin, NearestHealingSource->v.origin); + + bool bInHealingRange = (DistFromHealingSourceSq <= sqrf(DesiredDistFromHealingSource)); + + if (!bInHealingRange) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + if (bOutOfEnemyLOS) + { + BotLookAt(pBot, TrackedEnemyRef->LastLOSPosition); + return true; + } + + if (!UTIL_PlayerHasLOSToLocation(TrackedEnemyRef->EnemyEdict, UTIL_GetEntityGroundLocation(NearestHealingSource) + Vector(0.0f, 0.0f, 16.0f), UTIL_MetresToGoldSrcUnits(30.0f))) + { + MoveTo(pBot, UTIL_GetEntityGroundLocation(NearestHealingSource), MOVESTYLE_NORMAL, DesiredDistFromHealingSource); + return true; + } + + // If the enemy can see the healing source, then we must go on the attack + bShouldBreakRetreat = true; + } + } + + if (pBot->CurrentCombatStrategy == COMBAT_STRATEGY_ATTACK || bShouldBreakRetreat) + { + Vector MoveTarget = UTIL_GetFloorUnderEntity(CurrentEnemy); + + MoveTarget = MoveTarget + (CurrentEnemy->v.velocity * 0.1f); + + MoveTo(pBot, MoveTarget, MOVESTYLE_NORMAL); + + AvHAIWeapon DesiredWeapon = OnosGetBestWeaponForCombatTarget(pBot, CurrentEnemy); + + if (DesiredWeapon == WEAPON_ONOS_CHARGE) + { + BotShootTarget(pBot, DesiredWeapon, CurrentEnemy); + return true; + } + + BotAttackResult LOSCheck = PerformAttackLOSCheck(pBot, DesiredWeapon, CurrentEnemy); + + if (LOSCheck == ATTACK_SUCCESS) + { + BotShootTarget(pBot, DesiredWeapon, CurrentEnemy); + } + } + + return true; } \ No newline at end of file diff --git a/main/source/mod/AIPlayers/AvHAIPlayer.h b/main/source/mod/AIPlayers/AvHAIPlayer.h index 5b9c669c..1ee9960b 100644 --- a/main/source/mod/AIPlayers/AvHAIPlayer.h +++ b/main/source/mod/AIPlayers/AvHAIPlayer.h @@ -104,4 +104,26 @@ void UpdateAIPlayerDMRole(AvHAIPlayer* pBot); bool ShouldAIPlayerTakeCommand(AvHAIPlayer* pBot); +int BotGetNextEnemyTarget(AvHAIPlayer* pBot); + +AvHAICombatStrategy GetBotCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetAlienCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetSkulkCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetGorgeCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetLerkCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetFadeCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetOnosCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); +AvHAICombatStrategy GetMarineCombatStrategyForTarget(AvHAIPlayer* pBot, enemy_status* CurrentEnemy); + +bool MarineCombatThink(AvHAIPlayer* pBot); +void MarineHuntEnemy(AvHAIPlayer* pBot, enemy_status* TrackedEnemy); +void BotThrowGrenadeAtTarget(AvHAIPlayer* pBot, const Vector TargetPoint); + +bool AlienCombatThink(AvHAIPlayer* pBot); +bool SkulkCombatThink(AvHAIPlayer* pBot); +bool GorgeCombatThink(AvHAIPlayer* pBot); +bool LerkCombatThink(AvHAIPlayer* pBot); +bool FadeCombatThink(AvHAIPlayer* pBot); +bool OnosCombatThink(AvHAIPlayer* pBot); + #endif \ No newline at end of file diff --git a/main/source/mod/AIPlayers/AvHAITactical.cpp b/main/source/mod/AIPlayers/AvHAITactical.cpp index 57cc3d96..d24e79c0 100644 --- a/main/source/mod/AIPlayers/AvHAITactical.cpp +++ b/main/source/mod/AIPlayers/AvHAITactical.cpp @@ -3561,13 +3561,13 @@ const vector AITAC_GetAllHives() return Results; } -const vector AITAC_GetAllTeamHives(AvHTeamNumber Team) +const vector AITAC_GetAllTeamHives(AvHTeamNumber Team, bool bFullyBuiltOnly) { vector Results; for (auto it = Hives.begin(); it != Hives.end(); it++) { - if (it->OwningTeam == Team) + if (it->OwningTeam == Team && (!bFullyBuiltOnly || it->Status == HIVE_STATUS_BUILT)) { Results.push_back(&(*it)); } @@ -4139,4 +4139,64 @@ AvHAIDeployableStructureType AITAC_GetNextMissingUpgradeChamberForTeam(AvHTeamNu } return STRUCTURE_NONE; +} + +edict_t* AITAC_AlienFindNearestHealingSource(AvHTeamNumber Team, Vector SearchLocation, edict_t* SearchingPlayer, bool bIncludeGorges) +{ + edict_t* Result = nullptr; + float MinDist = 0.0f; + + vector AllTeamHives = AITAC_GetAllTeamHives(Team, true); + + for (auto it = AllTeamHives.begin(); it != AllTeamHives.end(); it++) + { + float ThisDist = vDist2DSq((*it)->Location, SearchLocation); + // Factor healing radius into the distance checks, we don't have to be right at the hive to heal + ThisDist -= BALANCE_VAR(kHiveHealRadius) * 0.75f; + + // We're already in healing distance of a hive, that's our healing source + if (ThisDist <= 0.0f) { return (*it)->HiveEntity->edict(); } + + if (FNullEnt(Result) || ThisDist < MinDist) + { + Result = (*it)->HiveEntity->edict(); + MinDist = ThisDist; + } + } + + DeployableSearchFilter DCFilter; + DCFilter.DeployableTeam = Team; + DCFilter.DeployableTypes = STRUCTURE_ALIEN_DEFENCECHAMBER; + DCFilter.MaxSearchRadius = (!FNullEnt(Result)) ? MinDist : 0.0f; // We should always have a result, unless we have no hives left. That's our benchmark: only look for DCs closer than the hive + + vector AllDCs = AITAC_FindAllDeployables(SearchLocation, &DCFilter); + + for (auto it = AllDCs.begin(); it != AllDCs.end(); it++) + { + AvHAIBuildableStructure* ThisDC = (*it); + + float ThisDist = vDist2DSq(ThisDC->Location, SearchLocation); + // Factor healing radius into the distance checks, we don't have to be sat on top of the DC to heal + ThisDist -= BALANCE_VAR(kHiveHealRadius) * 0.75f; + + // We're already in healing distance of a DC, that's our healing source + if (ThisDist <= 0.0f) { return ThisDC->edict; } + + if (FNullEnt(Result) || ThisDist < MinDist) + { + Result = ThisDC->edict; + MinDist = ThisDist; + } + } + + edict_t* FriendlyGorge = nullptr; + + if (bIncludeGorges) + { + float PlayerSearchDist = (!FNullEnt(Result)) ? MinDist : 0.0f; // As before, we only want players closer than our current "winner" + FriendlyGorge = AITAC_GetNearestPlayerOfClassInArea(Team, SearchLocation, PlayerSearchDist, false, SearchingPlayer, AVH_USER3_ALIEN_PLAYER2); + } + + return (!FNullEnt(FriendlyGorge) ? FriendlyGorge : Result); + } \ No newline at end of file diff --git a/main/source/mod/AIPlayers/AvHAITactical.h b/main/source/mod/AIPlayers/AvHAITactical.h index fe835b34..185cbcb8 100644 --- a/main/source/mod/AIPlayers/AvHAITactical.h +++ b/main/source/mod/AIPlayers/AvHAITactical.h @@ -159,7 +159,7 @@ edict_t* AITAC_GetNearestHiddenPlayerInLocation(AvHTeamNumber Team, const Vector const vector AITAC_GetAllResourceNodes(); const vector AITAC_GetAllReachableResourceNodes(AvHTeamNumber Team); const vector AITAC_GetAllHives(); -const vector AITAC_GetAllTeamHives(AvHTeamNumber Team); +const vector AITAC_GetAllTeamHives(AvHTeamNumber Team, bool bFullyBuiltOnly); bool AITAC_AnyPlayerOnTeamWithLOS(AvHTeamNumber Team, const Vector& Location, float SearchRadius); @@ -173,4 +173,6 @@ AvHAIDeployableStructureType AITAC_GetNextMissingUpgradeChamberForTeam(AvHTeamNu void AITAC_OnTeamStartsModified(); +edict_t* AITAC_AlienFindNearestHealingSource(AvHTeamNumber Team, Vector SearchLocation, edict_t* SearchingPlayer, bool bIncludeGorges); + #endif \ No newline at end of file diff --git a/main/source/mod/AIPlayers/AvHAIWeaponHelper.cpp b/main/source/mod/AIPlayers/AvHAIWeaponHelper.cpp index d979b300..6125be74 100644 --- a/main/source/mod/AIPlayers/AvHAIWeaponHelper.cpp +++ b/main/source/mod/AIPlayers/AvHAIWeaponHelper.cpp @@ -111,49 +111,49 @@ float GetEnergyCostForWeapon(const AvHAIWeapon Weapon) switch (Weapon) { case WEAPON_SKULK_BITE: - return kBiteEnergyCost; + return BALANCE_VAR(kBiteEnergyCost); case WEAPON_SKULK_PARASITE: - return kParasiteEnergyCost; + return BALANCE_VAR(kParasiteEnergyCost); case WEAPON_SKULK_LEAP: - return kLeapEnergyCost; + return BALANCE_VAR(kLeapEnergyCost); case WEAPON_SKULK_XENOCIDE: - return kDivineWindEnergyCost; + return BALANCE_VAR(kDivineWindEnergyCost); case WEAPON_GORGE_SPIT: - return kSpitEnergyCost; + return BALANCE_VAR(kSpitEnergyCost); case WEAPON_GORGE_HEALINGSPRAY: - return kHealingSprayEnergyCost; + return BALANCE_VAR(kHealingSprayEnergyCost); case WEAPON_GORGE_BILEBOMB: - return kBileBombEnergyCost; + return BALANCE_VAR(kBileBombEnergyCost); case WEAPON_GORGE_WEB: - return kWebEnergyCost; + return BALANCE_VAR(kWebEnergyCost); case WEAPON_LERK_BITE: - return kBite2EnergyCost; + return BALANCE_VAR(kBite2EnergyCost); case WEAPON_LERK_SPORES: - return kSporesEnergyCost; + return BALANCE_VAR(kSporesEnergyCost); case WEAPON_LERK_UMBRA: - return kUmbraEnergyCost; + return BALANCE_VAR(kUmbraEnergyCost); case WEAPON_LERK_PRIMALSCREAM: - return kPrimalScreamEnergyCost; + return BALANCE_VAR(kPrimalScreamEnergyCost); case WEAPON_FADE_SWIPE: - return kSwipeEnergyCost; + return BALANCE_VAR(kSwipeEnergyCost); case WEAPON_FADE_BLINK: - return kBlinkEnergyCost; + return BALANCE_VAR(kBlinkEnergyCost); case WEAPON_FADE_METABOLIZE: - return kMetabolizeEnergyCost; + return BALANCE_VAR(kMetabolizeEnergyCost); case WEAPON_FADE_ACIDROCKET: - return kAcidRocketEnergyCost; + return BALANCE_VAR(kAcidRocketEnergyCost); case WEAPON_ONOS_GORE: - return kClawsEnergyCost; + return BALANCE_VAR(kClawsEnergyCost); case WEAPON_ONOS_DEVOUR: - return kDevourEnergyCost; + return BALANCE_VAR(kDevourEnergyCost); case WEAPON_ONOS_STOMP: - return kStompEnergyCost; + return BALANCE_VAR(kStompEnergyCost); case WEAPON_ONOS_CHARGE: - return kChargeEnergyCost; + return BALANCE_VAR(kChargeEnergyCost); default: return 0.0f; @@ -207,6 +207,14 @@ bool IsHitscanWeapon(AvHAIWeapon Weapon) return false; } +float GetTimeUntilPlayerNextRefire(const AvHPlayer* Player) +{ + AvHBasePlayerWeapon* WeaponRef = dynamic_cast(Player->m_pActiveItem); + + if (!WeaponRef) { return 0.0f; } + + return WeaponRef->m_flNextPrimaryAttack; +} AvHAIWeapon GetBotMarineSecondaryWeapon(const AvHAIPlayer* pBot) { @@ -387,21 +395,21 @@ float GetMaxIdealWeaponRange(const AvHAIWeapon Weapon) case WEAPON_ONOS_STOMP: return UTIL_MetresToGoldSrcUnits(8.0f); case WEAPON_SKULK_XENOCIDE: - return UTIL_MetresToGoldSrcUnits(5.0f); + return (float)BALANCE_VAR(kDivineWindRadius) * 0.8f; case WEAPON_ONOS_GORE: - return BALANCE_VAR(kClawsRange); + return (float)BALANCE_VAR(kClawsRange); case WEAPON_ONOS_DEVOUR: - return BALANCE_VAR(kDevourRange); + return (float)BALANCE_VAR(kDevourRange); case WEAPON_FADE_SWIPE: - return BALANCE_VAR(kSwipeRange); + return (float)BALANCE_VAR(kSwipeRange); case WEAPON_SKULK_BITE: - return BALANCE_VAR(kBiteRange); + return (float)BALANCE_VAR(kBiteRange); case WEAPON_LERK_BITE: - return BALANCE_VAR(kBite2Range); + return (float)BALANCE_VAR(kBite2Range); case WEAPON_GORGE_HEALINGSPRAY: - return BALANCE_VAR(kHealingSprayRange) * 0.5f; + return (float)BALANCE_VAR(kHealingSprayRange) * 0.5f; case WEAPON_MARINE_WELDER: - return BALANCE_VAR(kWelderRange); + return (float)BALANCE_VAR(kWelderRange); default: return max_player_use_reach; } @@ -692,7 +700,7 @@ AvHAIWeapon BotAlienChooseBestWeaponForStructure(AvHAIPlayer* pBot, edict_t* tar return WEAPON_GORGE_BILEBOMB; } - if (PlayerHasWeapon(pBot->Player, WEAPON_FADE_ACIDROCKET) && StructureType == STRUCTURE_ALIEN_HIVE || IsDamagingStructure(StructureType)) + if (PlayerHasWeapon(pBot->Player, WEAPON_FADE_ACIDROCKET) && (StructureType == STRUCTURE_ALIEN_HIVE || IsDamagingStructure(StructureType))) { return WEAPON_FADE_ACIDROCKET; } @@ -781,21 +789,27 @@ AvHAIWeapon SkulkGetBestWeaponForCombatTarget(AvHAIPlayer* pBot, edict_t* Target if (PlayerHasWeapon(pBot->Player, WEAPON_SKULK_XENOCIDE)) { AvHTeamNumber EnemyTeam = AIMGR_GetEnemyTeam(pBot->Player->GetTeam()); + float XenocideRadius = GetMaxIdealWeaponRange(WEAPON_SKULK_XENOCIDE); - int NumEnemyTargetsInArea = AITAC_GetNumPlayersOfTeamInArea(EnemyTeam, Target->v.origin, UTIL_MetresToGoldSrcUnits(5.0f), false, nullptr, AVH_USER3_NONE); + // Add one to include the target themselves + int NumEnemyTargetsInArea = AITAC_GetNumPlayersOnTeamWithLOS(EnemyTeam, Target->v.origin, XenocideRadius, Target) + 1; - AvHTeam* EnemyTeamRef = GetGameRules()->GetTeam(EnemyTeam); - - if (EnemyTeamRef) + if (NumEnemyTargetsInArea <= 2) { - AvHAIDeployableStructureType StructureSearchType = (EnemyTeamRef->GetTeamType() == AVH_CLASS_TYPE_MARINE) ? SEARCH_ALL_MARINE_STRUCTURES : SEARCH_ALL_ALIEN_STRUCTURES; - DeployableSearchFilter SearchFilter; - SearchFilter.DeployableTypes = StructureSearchType; - SearchFilter.MaxSearchRadius = UTIL_MetresToGoldSrcUnits(5.0f); - SearchFilter.DeployableTeam = EnemyTeam; + AvHTeam* EnemyTeamRef = GetGameRules()->GetTeam(EnemyTeam); - NumEnemyTargetsInArea += AITAC_GetNumDeployablesNearLocation(Target->v.origin, &SearchFilter); + if (EnemyTeamRef) + { + AvHAIDeployableStructureType StructureSearchType = (EnemyTeamRef->GetTeamType() == AVH_CLASS_TYPE_MARINE) ? SEARCH_ALL_MARINE_STRUCTURES : SEARCH_ALL_ALIEN_STRUCTURES; + + DeployableSearchFilter SearchFilter; + SearchFilter.DeployableTypes = StructureSearchType; + SearchFilter.MaxSearchRadius = XenocideRadius; + SearchFilter.DeployableTeam = EnemyTeam; + + NumEnemyTargetsInArea += AITAC_GetNumDeployablesNearLocation(Target->v.origin, &SearchFilter); + } } if (NumEnemyTargetsInArea > 2) @@ -883,12 +897,23 @@ AvHAIWeapon OnosGetBestWeaponForCombatTarget(AvHAIPlayer* pBot, edict_t* Target) return WEAPON_ONOS_STOMP; } - if (!IsPlayerDigesting(pBot->Edict) && DistFromTarget < sqrf(UTIL_MetresToGoldSrcUnits(2.0f))) + AvHAIWeapon AttackWeapon = WEAPON_ONOS_GORE; + + if (!IsPlayerDigesting(pBot->Edict)) { - return WEAPON_ONOS_DEVOUR; + AttackWeapon = WEAPON_ONOS_DEVOUR; } - return WEAPON_ONOS_GORE; + float AttackWeaponRange = GetMaxIdealWeaponRange(AttackWeapon); + + BotAttackResult WeaponAttackResult = PerformAttackLOSCheck(pBot, AttackWeapon, Target); + + if (PlayerHasWeapon(pBot->Player, WEAPON_ONOS_CHARGE) && UTIL_PointIsDirectlyReachable(pBot->Edict->v.origin, Target->v.origin) && WeaponAttackResult == ATTACK_OUTOFRANGE) + { + return WEAPON_ONOS_CHARGE; + } + + return AttackWeapon; } AvHAIWeapon FadeGetBestWeaponForCombatTarget(AvHAIPlayer* pBot, edict_t* Target) @@ -1009,6 +1034,132 @@ BotAttackResult PerformAttackLOSCheck(AvHAIPlayer* pBot, const AvHAIWeapon Weapo return ATTACK_SUCCESS; } +BotAttackResult PerformAttackLOSCheck(AvHAIPlayer* pBot, const AvHAIWeapon Weapon, const Vector TargetLocation, const edict_t* Target) +{ + if (!TargetLocation) { return ATTACK_INVALIDTARGET; } + + if (Weapon == WEAPON_NONE) { return ATTACK_NOWEAPON; } + + // Don't need aiming or special LOS checks for primal scream as it's AoE buff + if (Weapon == WEAPON_LERK_PRIMALSCREAM) + { + return ATTACK_SUCCESS; + } + + // Add a LITTLE bit of give to avoid edge cases where the bot is a smidge out of range + float MaxWeaponRange = GetMaxIdealWeaponRange(Weapon) - 5.0f; + + // Don't need aiming or special LOS checks for Xenocide as it's an AOE attack, just make sure we're close enough and don't have a wall in the way + if (Weapon == WEAPON_SKULK_XENOCIDE) + { + if (vDist3DSq(pBot->Edict->v.origin, TargetLocation) <= sqrf(MaxWeaponRange) && UTIL_QuickTrace(pBot->Edict, pBot->Edict->v.origin, TargetLocation)) + { + return ATTACK_SUCCESS; + } + else + { + return ATTACK_OUTOFRANGE; + } + } + + // For charge and stomp, we can go through stuff so don't need to check for being blocked + if (Weapon == WEAPON_ONOS_CHARGE || Weapon == WEAPON_ONOS_STOMP) + { + if (vDist3DSq(pBot->Edict->v.origin, TargetLocation) > sqrf(MaxWeaponRange)) { return ATTACK_OUTOFRANGE; } + + if (!UTIL_QuickTrace(pBot->Edict, pBot->Edict->v.origin, TargetLocation) || fabsf(TargetLocation.z - TargetLocation.z) > 50.0f) { return ATTACK_OUTOFRANGE; } + + return ATTACK_SUCCESS; + } + + TraceResult hit; + + Vector StartTrace = pBot->CurrentEyePosition; + + Vector AttackDir = UTIL_GetVectorNormal(TargetLocation - StartTrace); + + Vector EndTrace = pBot->CurrentEyePosition + (AttackDir * MaxWeaponRange); + + UTIL_TraceLine(StartTrace, EndTrace, dont_ignore_monsters, dont_ignore_glass, pBot->Edict->v.pContainingEntity, &hit); + + if (FNullEnt(hit.pHit)) { return ATTACK_OUTOFRANGE; } + + if (hit.pHit != Target) + { + if (vDist3DSq(pBot->CurrentEyePosition, TargetLocation) > sqrf(MaxWeaponRange)) + { + return ATTACK_OUTOFRANGE; + } + else + { + return ATTACK_BLOCKED; + } + } + + return ATTACK_SUCCESS; +} + +BotAttackResult PerformAttackLOSCheck(const Vector Location, const AvHAIWeapon Weapon, const edict_t* Target) +{ + if (FNullEnt(Target) || (Target->v.deadflag != DEAD_NO)) { return ATTACK_INVALIDTARGET; } + + if (Weapon == WEAPON_NONE) { return ATTACK_NOWEAPON; } + + float MaxWeaponRange = GetMaxIdealWeaponRange(Weapon); + + // Don't need aiming or special LOS checks for Xenocide as it's an AOE attack, just make sure we're close enough and don't have a wall in the way + if (Weapon == WEAPON_SKULK_XENOCIDE) + { + if (vDist3DSq(Location, Target->v.origin) <= sqrf(MaxWeaponRange) && UTIL_QuickTrace(nullptr, Location, Target->v.origin)) + { + return ATTACK_SUCCESS; + } + else + { + return ATTACK_OUTOFRANGE; + } + } + + // For charge and stomp, we can go through stuff so don't need to check for being blocked + if (Weapon == WEAPON_ONOS_CHARGE || Weapon == WEAPON_ONOS_STOMP) + { + if (vDist3DSq(Location, Target->v.origin) > sqrf(MaxWeaponRange)) { return ATTACK_OUTOFRANGE; } + + if (!UTIL_QuickTrace(nullptr, Location, Target->v.origin) || fabsf(Target->v.origin.z - Target->v.origin.z) > 50.0f) { return ATTACK_OUTOFRANGE; } + + return ATTACK_SUCCESS; + } + + bool bIsMeleeWeapon = IsMeleeWeapon(Weapon); + + TraceResult hit; + + Vector StartTrace = Location; + + Vector AttackDir = UTIL_GetVectorNormal(UTIL_GetCentreOfEntity(Target) - StartTrace); + + Vector EndTrace = Location + (AttackDir * MaxWeaponRange); + + UTIL_TraceLine(StartTrace, EndTrace, dont_ignore_monsters, dont_ignore_glass, nullptr, &hit); + + if (FNullEnt(hit.pHit)) { return ATTACK_OUTOFRANGE; } + + if (hit.pHit != Target) + { + if (vDist3DSq(Location, Target->v.origin) > sqrf(MaxWeaponRange)) + { + return ATTACK_OUTOFRANGE; + } + else + { + return ATTACK_BLOCKED; + } + } + + return ATTACK_SUCCESS; +} + + bool IsAreaAffectedBySpores(const Vector Location) { bool Result = false; diff --git a/main/source/mod/AIPlayers/AvHAIWeaponHelper.h b/main/source/mod/AIPlayers/AvHAIWeaponHelper.h index da368be5..589157d3 100644 --- a/main/source/mod/AIPlayers/AvHAIWeaponHelper.h +++ b/main/source/mod/AIPlayers/AvHAIWeaponHelper.h @@ -64,8 +64,11 @@ bool CanInterruptWeaponReload(AvHAIWeapon Weapon); void InterruptReload(AvHAIPlayer* pBot); bool IsHitscanWeapon(AvHAIWeapon Weapon); +float GetTimeUntilPlayerNextRefire(const AvHPlayer* Player); BotAttackResult PerformAttackLOSCheck(AvHAIPlayer* pBot, const AvHAIWeapon Weapon, const edict_t* Target); +BotAttackResult PerformAttackLOSCheck(AvHAIPlayer* pBot, const AvHAIWeapon Weapon, const Vector TargetLocation, const edict_t* Target); +BotAttackResult PerformAttackLOSCheck(const Vector Location, const AvHAIWeapon Weapon, const edict_t* Target); float UTIL_GetProjectileVelocityForWeapon(const AvHAIWeapon Weapon); bool IsAreaAffectedBySpores(const Vector Location);