Further bot enhancements

* Improved jump and blink movement
* Hopefully fixed bug with bots trying to walk between phase gates
* Fixed issue with bots constantly switching weapons when trying to reload
* Added a max AI time for a match, with bots throwing the game if there are no humans and it goes on too long (configurable in nsbots.ini, default is 90 minutes)
This commit is contained in:
RGreenlees 2024-04-05 21:34:50 +01:00 committed by pierow
parent eba41379e3
commit d87bb3d600
8 changed files with 217 additions and 49 deletions

View file

@ -960,7 +960,11 @@ bool AICOMM_IsRequestValid(ai_commander_request* Request)
case BUILD_TURRET_FACTORY:
return !AITAC_IsStructureOfTypeNearLocation(RequestorTeam, (STRUCTURE_MARINE_TURRETFACTORY | STRUCTURE_MARINE_ADVTURRETFACTORY), Requestor->v.origin, UTIL_MetresToGoldSrcUnits(5.0f));
case BUILD_ARMORY:
return !AITAC_IsStructureOfTypeNearLocation(RequestorTeam, (STRUCTURE_MARINE_ARMOURY | STRUCTURE_MARINE_ADVARMOURY), Requestor->v.origin, UTIL_MetresToGoldSrcUnits(5.0f));
return !AITAC_IsStructureOfTypeNearLocation(RequestorTeam, (STRUCTURE_MARINE_ARMOURY | STRUCTURE_MARINE_ADVARMOURY), Requestor->v.origin, UTIL_MetresToGoldSrcUnits(5.0f));
case BUILD_COMMANDSTATION:
return !AITAC_IsStructureOfTypeNearLocation(RequestorTeam, STRUCTURE_MARINE_COMMCHAIR, Requestor->v.origin, UTIL_MetresToGoldSrcUnits(10.0f));
case BUILD_SCAN:
return !AITAC_ItemExistsInLocation(Requestor->v.origin, DEPLOYABLE_ITEM_SCAN, RequestorTeam, AI_REACHABILITY_NONE, 0.0f, UTIL_MetresToGoldSrcUnits(5.0f), false);
default:
return true;
}
@ -2843,6 +2847,86 @@ bool AICOMM_CheckForNextSupportAction(AvHAIPlayer* pBot)
return true;
}
if (NextRequest->RequestType == BUILD_SCAN)
{
DeployableSearchFilter ObsFilter;
ObsFilter.DeployableTeam = CommanderTeam;
ObsFilter.DeployableTypes = STRUCTURE_MARINE_OBSERVATORY;
ObsFilter.IncludeStatusFlags = STRUCTURE_STATUS_COMPLETED;
ObsFilter.ExcludeStatusFlags = STRUCTURE_STATUS_RECYCLING;
AvHAIBuildableStructure NearestObservatory = AITAC_FindClosestDeployableToLocation(Requestor->v.origin, &ObsFilter);
if (!NearestObservatory.IsValid())
{
char msg[128];
sprintf(msg, "We don't have an observatory yet %s, ask again later.", STRING(Requestor->v.netname));
BotSay(pBot, true, 0.5f, msg);
NextRequest->bResponded = true;
return false;
}
Vector IdealDeployLocation = Requestor->v.origin + (UTIL_GetForwardVector2D(Requestor->v.angles) * 75.0f);
Vector ProjectedDeployLocation = AdjustPointForPathfinding(IdealDeployLocation, GetBaseNavProfile(STRUCTURE_BASE_NAV_PROFILE));
if (!vIsZero(ProjectedDeployLocation))
{
bool bSuccess = AICOMM_DeployStructure(pBot, STRUCTURE_MARINE_PHASEGATE, ProjectedDeployLocation, STRUCTURE_PURPOSE_NONE);
if (bSuccess)
{
NextRequest->bResponded = true;
return true;
}
}
Vector DeployLocation = UTIL_GetRandomPointOnNavmeshInRadius(GetBaseNavProfile(STRUCTURE_BASE_NAV_PROFILE), Requestor->v.origin, UTIL_MetresToGoldSrcUnits(1.0f));
if (!vIsZero(DeployLocation))
{
bool bSuccess = AICOMM_DeployStructure(pBot, STRUCTURE_MARINE_PHASEGATE, DeployLocation, STRUCTURE_PURPOSE_NONE);
if (bSuccess)
{
NextRequest->bResponded = true;
return true;
}
}
DeployLocation = UTIL_GetRandomPointOnNavmeshInRadiusIgnoreReachability(GetBaseNavProfile(STRUCTURE_BASE_NAV_PROFILE), Requestor->v.origin, UTIL_MetresToGoldSrcUnits(5.0f));
if (!vIsZero(DeployLocation))
{
bool bSuccess = AICOMM_DeployItem(pBot, DEPLOYABLE_ITEM_SCAN, DeployLocation);
if (bSuccess)
{
NextRequest->bResponded = true;
return true;
}
else
{
char msg[128];
sprintf(msg, "I can't find a good scan spot, %s. Try again elsewhere.", STRING(Requestor->v.netname));
BotSay(pBot, true, 0.5f, msg);
NextRequest->bResponded = true;
return false;
}
}
else
{
char msg[128];
sprintf(msg, "I can't find a good scan spot, %s. Try again elsewhere.", STRING(Requestor->v.netname));
BotSay(pBot, true, 0.5f, msg);
NextRequest->bResponded = true;
return false;
}
return false;
}
if (NextRequest->RequestType == BUILD_PHASEGATE)
{
if (!AITAC_ResearchIsComplete(CommanderTeam, TECH_RESEARCH_PHASETECH))
@ -2927,9 +3011,11 @@ bool AICOMM_CheckForNextSupportAction(AvHAIPlayer* pBot)
}
if (NextRequest->RequestType == BUILD_ARMORY)
if (NextRequest->RequestType == BUILD_ARMORY || NextRequest->RequestType == BUILD_COMMANDSTATION)
{
if (pBot->Player->GetResources() < BALANCE_VAR(kArmoryCost))
float RequiredRes = (NextRequest->RequestType == BUILD_ARMORY) ? BALANCE_VAR(kArmoryCost) : BALANCE_VAR(kCommandStationCost);
if (pBot->Player->GetResources() < RequiredRes)
{
if (!NextRequest->bAcknowledged)
{
@ -2945,9 +3031,11 @@ bool AICOMM_CheckForNextSupportAction(AvHAIPlayer* pBot)
Vector IdealDeployLocation = Requestor->v.origin + (UTIL_GetForwardVector2D(Requestor->v.angles) * 75.0f);
Vector ProjectedDeployLocation = AdjustPointForPathfinding(IdealDeployLocation, GetBaseNavProfile(STRUCTURE_BASE_NAV_PROFILE));
AvHAIDeployableStructureType StructureToDeploy = (NextRequest->RequestType == BUILD_ARMORY) ? STRUCTURE_MARINE_ARMOURY : STRUCTURE_MARINE_COMMCHAIR;
if (!vIsZero(ProjectedDeployLocation))
{
bool bSuccess = AICOMM_DeployStructure(pBot, STRUCTURE_MARINE_ARMOURY, ProjectedDeployLocation, STRUCTURE_PURPOSE_NONE);
bool bSuccess = AICOMM_DeployStructure(pBot, StructureToDeploy, ProjectedDeployLocation, STRUCTURE_PURPOSE_NONE);
if (bSuccess)
{
@ -2960,7 +3048,7 @@ bool AICOMM_CheckForNextSupportAction(AvHAIPlayer* pBot)
if (!vIsZero(DeployLocation))
{
bool bSuccess = AICOMM_DeployStructure(pBot, STRUCTURE_MARINE_ARMOURY, DeployLocation, STRUCTURE_PURPOSE_NONE);
bool bSuccess = AICOMM_DeployStructure(pBot, StructureToDeploy, DeployLocation, STRUCTURE_PURPOSE_NONE);
if (bSuccess)
{
@ -2973,7 +3061,7 @@ bool AICOMM_CheckForNextSupportAction(AvHAIPlayer* pBot)
if (!vIsZero(DeployLocation))
{
bool bSuccess = AICOMM_DeployStructure(pBot, STRUCTURE_MARINE_ARMOURY, DeployLocation, STRUCTURE_PURPOSE_NONE);
bool bSuccess = AICOMM_DeployStructure(pBot, StructureToDeploy, DeployLocation, STRUCTURE_PURPOSE_NONE);
if (bSuccess)
{

View file

@ -8,6 +8,8 @@
BotFillTiming CurrentBotFillTiming = FILLTIMING_ALLHUMANS;
float MaxAIMatchTimeMinutes = 90.0f;
std::unordered_map<std::string, TeamSizeDefinitions> TeamSizeMap;
bot_skill BotSkillLevels[4];
@ -54,6 +56,11 @@ float CONFIG_GetMaxStuckTime()
return avh_botmaxstucktime.value;
}
float CONFIG_GetMaxAIMatchTimeMinutes()
{
return MaxAIMatchTimeMinutes;
}
string CONFIG_GetBotPrefix()
{
return string(BotPrefix);
@ -208,6 +215,14 @@ void CONFIG_ParseConfigFile()
continue;
}
if (!stricmp(keyChar, "MaxAIMatchTime"))
{
float MaxMinutes = std::stof(value.c_str());
MaxAIMatchTimeMinutes = MaxMinutes;
continue;
}
if (!stricmp(keyChar, "BotFillTiming"))
{
int FillSetting = atoi(value.c_str());
@ -550,6 +565,11 @@ void CONFIG_RegenerateIniFile()
fprintf(NewConfigFile, "# What prefix to put in front of a bot's name (can leave blank)\n");
fprintf(NewConfigFile, "Prefix=[BOT]\n\n");
fprintf(NewConfigFile, "# After this many minutes into a match, the bots will leave the game if there are no humans playing\n");
fprintf(NewConfigFile, "# Helps prevent stalemates and bugs that happen after extremely long matches\n");
fprintf(NewConfigFile, "# Default = 90 minutes\n");
fprintf(NewConfigFile, "MaxAIMatchTime=90\n\n");
fprintf(NewConfigFile, "# When should the server start adding bots? Note: bots will always be added after round start regardless\n");
fprintf(NewConfigFile, "# 0 = On map load (after 5 second grace period)\n");
fprintf(NewConfigFile, "# 1 = When all humans have joined a team (i.e. no more humans left in ready room)\n");

View file

@ -46,6 +46,8 @@ bool CONFIG_IsOnosAllowed();
// Returns the max time a bot is allowed to be stuck before suiciding (0 means forever)
float CONFIG_GetMaxStuckTime();
float CONFIG_GetMaxAIMatchTimeMinutes();
// Returns the desired marine team size for the given map, indexes into TeamSizeMap
int CONFIG_GetTeamASizeForMap(const char* MapName);
// Returns the desired alien team size for the given map, indexes into TeamSizeMap

View file

@ -3530,6 +3530,25 @@ void BlockedMove(AvHAIPlayer* pBot, const Vector StartPoint, const Vector EndPoi
pBot->desiredMovementDir = vForward;
Vector CurrVelocity = UTIL_GetVectorNormal2D(pBot->Edict->v.velocity);
float Dot = UTIL_GetDotProduct2D(vForward, CurrVelocity);
Vector FaceDir = UTIL_GetForwardVector2D(pBot->Edict->v.angles);
float FaceDot = UTIL_GetDotProduct2D(FaceDir, vForward);
// Yes this is cheating, but is it not cheating for humans to have millions of years of evolution
// driving their ability to judge a jump, while the bots have a single year of coding from a moron?
if (FaceDot < 0.95f)
{
float MoveSpeed = vSize2D(pBot->Edict->v.velocity);
Vector NewVelocity = vForward * MoveSpeed;
NewVelocity.z = pBot->Edict->v.velocity.z;
pBot->Edict->v.velocity = NewVelocity;
}
BotJump(pBot);
}
@ -3544,6 +3563,25 @@ void JumpMove(AvHAIPlayer* pBot, const Vector StartPoint, const Vector EndPoint)
pBot->desiredMovementDir = vForward;
Vector CurrVelocity = UTIL_GetVectorNormal2D(pBot->Edict->v.velocity);
float Dot = UTIL_GetDotProduct2D(vForward, CurrVelocity);
Vector FaceDir = UTIL_GetForwardVector2D(pBot->Edict->v.angles);
float FaceDot = UTIL_GetDotProduct2D(FaceDir, vForward);
// Yes this is cheating, but is it not cheating for humans to have millions of years of evolution
// driving their ability to judge a jump, while the bots have a single year of coding from a moron?
if (FaceDot < 0.95f)
{
float MoveSpeed = vSize2D(pBot->Edict->v.velocity);
Vector NewVelocity = vForward * MoveSpeed;
NewVelocity.z = pBot->Edict->v.velocity.z;
pBot->Edict->v.velocity = NewVelocity;
}
BotJump(pBot);
bool bCanDuck = (IsPlayerMarine(pBot->Edict) || IsPlayerFade(pBot->Edict) || IsPlayerOnos(pBot->Edict));
@ -4315,10 +4353,10 @@ bool IsBotOffPath(const AvHAIPlayer* pBot)
case SAMPLE_POLYFLAGS_LIFT:
return IsBotOffLiftNode(pBot, MoveFrom, MoveTo, NextMoveLocation, NextMoveFlag);
default:
return IsBotOffWalkNode(pBot, MoveFrom, MoveTo, NextMoveLocation, NextMoveFlag);
return IsBotOffFallNode(pBot, MoveFrom, MoveTo, NextMoveLocation, NextMoveFlag);
}
return IsBotOffWalkNode(pBot, MoveFrom, MoveTo, NextMoveLocation, NextMoveFlag);
return IsBotOffFallNode(pBot, MoveFrom, MoveTo, NextMoveLocation, NextMoveFlag);
}
@ -4341,6 +4379,9 @@ bool IsBotOffWalkNode(const AvHAIPlayer* pBot, Vector MoveStart, Vector MoveEnd,
{
if (!pBot->BotNavInfo.IsOnGround) { return false; }
// This shouldn't happen... but does occasionally. Walk moves should always be directly reachable from start to end
if (!UTIL_PointIsDirectlyReachable(MoveStart, MoveEnd)) { return true; }
Vector NearestPointOnLine = vClosestPointOnLine2D(MoveStart, MoveEnd, pBot->Edict->v.origin);
if (vDist2DSq(pBot->Edict->v.origin, NearestPointOnLine) > sqrf(GetPlayerRadius(pBot->Edict) * 3.0f)) { return true; }
@ -4464,18 +4505,10 @@ void BlinkClimbMove(AvHAIPlayer* pBot, const Vector StartPoint, const Vector End
if (GetPlayerCurrentWeapon(pBot->Player) != WEAPON_FADE_BLINK) { return; }
// Only blink if we're below the target climb height
if (pEdict->v.origin.z < RequiredClimbHeight)
if (pEdict->v.origin.z < RequiredClimbHeight + 32.0f)
{
float HeightToClimb = (fabsf(pEdict->v.origin.z - RequiredClimbHeight));
if (HeightToClimb > (GetPlayerHeight(pBot->Edict, false) * 2.0f))
{
if (GetPlayerEnergy(pBot->Edict) < 0.15f && pBot->BotNavInfo.IsOnGround)
{
return;
}
}
Vector CurrVelocity = UTIL_GetVectorNormal2D(pBot->Edict->v.velocity);
float Dot = UTIL_GetDotProduct2D(MoveDir, CurrVelocity);
@ -4484,30 +4517,37 @@ void BlinkClimbMove(AvHAIPlayer* pBot, const Vector StartPoint, const Vector End
float FaceDot = UTIL_GetDotProduct2D(FaceDir, MoveDir);
// Don't start blinking unless we're already in the air, or we're moving in the correct direction. Stops fade shooting off sideways when approaching a climb point from the side
if (FaceDot > 0.9f)
// Yes this is cheating, but the fades were struggling with zipping off-target when trying to blink
// Better this than fades getting constantly chewed up by marines because they can't escape properly
if (FaceDot < 0.95f)
{
float ZDiff = fabs(pEdict->v.origin.z - RequiredClimbHeight);
float MoveSpeed = vSize2D(pBot->Edict->v.velocity);
Vector NewVelocity = MoveDir * MoveSpeed;
NewVelocity.z = pBot->Edict->v.velocity.z;
// We don't want to blast off like a rocket, so only apply enough blink until our upwards velocity is enough to carry us to the desired height
float DesiredZVelocity = sqrtf(2.0f * GOLDSRC_GRAVITY * (ZDiff + 10.0f));
pBot->Edict->v.velocity = NewVelocity;
}
if (pBot->Edict->v.velocity.z < DesiredZVelocity || pBot->Edict->v.velocity.z < 300.0f)
float ZDiff = fabs(pEdict->v.origin.z - (RequiredClimbHeight + 72.0f));
// We don't want to blast off like a rocket, so only apply enough blink until our upwards velocity is enough to carry us to the desired height
float DesiredZVelocity = sqrtf(2.0f * GOLDSRC_GRAVITY * (ZDiff + 10.0f));
if (pBot->Edict->v.velocity.z < DesiredZVelocity || pBot->Edict->v.velocity.z < 300.0f)
{
// We're going to cheat and give the bot the necessary energy to make the move. Better the fade cheats a bit than gets stuck somewhere
if (GetPlayerEnergy(pBot->Edict) < 0.1f)
{
// We're going to cheat and give the bot the necessary energy to make the move. Better the fade cheats a bit than gets stuck somewhere
if (GetPlayerEnergy(pBot->Edict) < 0.1f)
{
pBot->Player->Energize(0.1f);
}
BotMoveLookAt(pBot, EndPoint + Vector(0.0f, 0.0f, 100.0f));
pBot->Button |= IN_ATTACK2;
}
else
{
Vector LookAtTarget = EndPoint;
LookAtTarget.z = pBot->CurrentEyePosition.z;
BotMoveLookAt(pBot, LookAtTarget);
pBot->Player->Energize(0.1f);
}
BotMoveLookAt(pBot, EndPoint + Vector(0.0f, 0.0f, 100.0f));
pBot->Button |= IN_ATTACK2;
}
else
{
Vector LookAtTarget = EndPoint;
LookAtTarget.z = pBot->CurrentEyePosition.z;
BotMoveLookAt(pBot, LookAtTarget);
}
}
}
@ -8770,7 +8810,7 @@ void NAV_ProgressMovementTask(AvHAIPlayer* pBot)
}
BotMoveLookAt(pBot, AimLocation);
pBot->DesiredCombatWeapon = WEAPON_MARINE_WELDER;
pBot->DesiredMoveWeapon = WEAPON_MARINE_WELDER;
if (GetPlayerCurrentWeapon(pBot->Player) != WEAPON_MARINE_WELDER)
{

View file

@ -379,7 +379,13 @@ void BotSay(AvHAIPlayer* pBot, bool bTeamSay, float Delay, char* textToSay)
bool BotReloadWeapons(AvHAIPlayer* pBot)
{
// Aliens and commander don't reload
if (!IsPlayerMarine(pBot->Edict) || !IsPlayerActiveInGame(pBot->Edict) || IsPlayerReloading(pBot->Player)) { return false; }
if (!IsPlayerMarine(pBot->Edict) || !IsPlayerActiveInGame(pBot->Edict)) { return false; }
if (IsPlayerReloading(pBot->Player))
{
pBot->DesiredCombatWeapon = GetPlayerCurrentWeapon(pBot->Player);
}
AvHAIWeapon PrimaryWeapon = UTIL_GetPlayerPrimaryWeapon(pBot->Player);
AvHAIWeapon SecondaryWeapon = GetBotMarineSecondaryWeapon(pBot);
@ -791,12 +797,14 @@ void BotShootTarget(AvHAIPlayer* pBot, AvHAIWeapon AttackWeapon, edict_t* Target
// We can be less accurate with spores and umbra since they have AoE effects
float MinAcceptableAccuracy = 0.9f;
Vector GetPosition = pBot->Player->GetGunPosition();
bWillHit = (AimDot >= MinAcceptableAccuracy);
if (!bWillHit && IsHitscanWeapon(CurrentWeapon))
{
edict_t* HitEntity = UTIL_TraceEntity(pBot->Edict, pBot->CurrentEyePosition, pBot->CurrentEyePosition + (AimDir * GetMaxIdealWeaponRange(CurrentWeapon)));
edict_t* HitEntity = UTIL_TraceEntity(pBot->Edict, pBot->Player->GetGunPosition(), pBot->Player->GetGunPosition() + (AimDir * GetMaxIdealWeaponRange(CurrentWeapon)));
bWillHit = (HitEntity == Target);
}
@ -871,7 +879,7 @@ void BotShootLocation(AvHAIPlayer* pBot, AvHAIWeapon AttackWeapon, const Vector
return;
}
if (CurrentWeapon == WEAPON_NONE) { return; }
if (CurrentWeapon == WEAPON_INVALID) { return; }
if (CurrentWeapon == WEAPON_SKULK_XENOCIDE)
{
@ -2820,7 +2828,7 @@ void AIPlayerNSMarineThink(AvHAIPlayer* pBot)
BotProgressTask(pBot, pBot->CurrentTask);
}
if (pBot->DesiredCombatWeapon == WEAPON_NONE)
if (pBot->DesiredCombatWeapon == WEAPON_INVALID)
{
pBot->DesiredCombatWeapon = BotMarineChooseBestWeapon(pBot, nullptr);
}
@ -4245,7 +4253,7 @@ void AIPlayerNSAlienThink(AvHAIPlayer* pBot)
BotProgressTask(pBot, pBot->CurrentTask);
}
if (pBot->DesiredCombatWeapon == WEAPON_NONE)
if (pBot->DesiredCombatWeapon == WEAPON_INVALID)
{
pBot->DesiredCombatWeapon = UTIL_GetPlayerPrimaryWeapon(pBot->Player);
}

View file

@ -12,8 +12,6 @@
#include "../dlls/client.h"
#include <time.h>
float MAX_MATCH_TIME = 7200.0f;
double last_think_time = 0.0;
vector<AvHAIPlayer> ActiveAIPlayers;
@ -129,9 +127,14 @@ void AIMGR_UpdateAIPlayerCounts()
LastAIPlayerCountUpdate = gpGlobals->time;
bool bMatchExceededMaxLength = (gpGlobals->time - AIStartedTime) > MAX_MATCH_TIME;
float MaxMinutes = CONFIG_GetMaxAIMatchTimeMinutes();
float MaxSeconds = MaxMinutes * 60.0f;
// If bots are disabled, ensure we've removed all bots from the game
bool bMatchExceededMaxLength = (GetGameRules()->GetGameTime() > MaxSeconds);
// If bots are disabled or we've exceeded max AI time and no humans are playing, ensure we've removed all bots from the game
// Max AI time is configurable in nsbots.ini, and helps prevent infinite stalemates
// Default time is 90 minutes before bots start leaving to let the map cycle
if (!AIMGR_IsBotEnabled() || (bMatchExceededMaxLength && AIMGR_GetNumActiveHumanPlayers() == 0))
{
if (AIMGR_GetNumAIPlayers() > 0)

View file

@ -563,9 +563,10 @@ bool AITASK_IsResupplyTaskStillValid(AvHAIPlayer* pBot, AvHAIPlayerTask* Task)
if (!UTIL_StructureIsFullyBuilt(Task->TaskTarget) || UTIL_StructureIsRecycling(Task->TaskTarget)) { return false; }
return ((pBot->Edict->v.health < pBot->Edict->v.max_health)
|| (UTIL_GetPlayerPrimaryAmmoReserve(pBot->Player) < UTIL_GetPlayerPrimaryMaxAmmoReserve(pBot->Player))
|| (BotGetSecondaryWeaponAmmoReserve(pBot) < BotGetSecondaryWeaponMaxAmmoReserve(pBot))
return (
(pBot->Edict->v.health < pBot->Edict->v.max_health)
|| (UTIL_GetPlayerPrimaryAmmoReserve(pBot->Player) < UTIL_GetPlayerPrimaryMaxAmmoReserve(pBot->Player))
|| (UTIL_GetPlayerSecondaryAmmoReserve(pBot->Player) < UTIL_GetPlayerSecondaryMaxAmmoReserve(pBot->Player))
);
}

View file

@ -1977,6 +1977,12 @@ void AvHCommandStation::CommandUse( CBaseEntity* pActivator, CBaseEntity* pCalle
this->mTimeToPlayOnlineSound = this->GetTimeAnimationDone();
GetGameRules()->MarkDramaticEvent(kCCNewCommanderPriority, thePlayer, this);
// A human used the comm chair, let the AI know to keep away
if (!(thePlayer->pev->flags & FL_FAKECLIENT))
{
AIMGR_SetCommanderAllowedTime(theStationTeamNumber, gpGlobals->time + 20.0f);
}
}
else
{