Alien Combat

This commit is contained in:
RGreenlees 2024-01-24 20:55:17 +00:00 committed by pierow
parent f31e2bbf1c
commit 3a1a92c505
9 changed files with 1928 additions and 119 deletions

View file

@ -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

View file

@ -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;
}

View file

@ -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;

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -3561,13 +3561,13 @@ const vector<AvHAIHiveDefinition*> AITAC_GetAllHives()
return Results;
}
const vector<AvHAIHiveDefinition*> AITAC_GetAllTeamHives(AvHTeamNumber Team)
const vector<AvHAIHiveDefinition*> AITAC_GetAllTeamHives(AvHTeamNumber Team, bool bFullyBuiltOnly)
{
vector<AvHAIHiveDefinition*> 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<AvHAIHiveDefinition*> 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<AvHAIBuildableStructure*> 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);
}

View file

@ -159,7 +159,7 @@ edict_t* AITAC_GetNearestHiddenPlayerInLocation(AvHTeamNumber Team, const Vector
const vector<AvHAIResourceNode*> AITAC_GetAllResourceNodes();
const vector<AvHAIResourceNode*> AITAC_GetAllReachableResourceNodes(AvHTeamNumber Team);
const vector<AvHAIHiveDefinition*> AITAC_GetAllHives();
const vector<AvHAIHiveDefinition*> AITAC_GetAllTeamHives(AvHTeamNumber Team);
const vector<AvHAIHiveDefinition*> 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

View file

@ -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<AvHBasePlayerWeapon*>(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;

View file

@ -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);