Files
assaultcube-/source/src/bot/ac_bot_ai.cpp
T
2026-10-27 10:52:16 +00:00

510 lines
16 KiB
C++

//
// C++ Implementation: ac_bot_ai
//
// Description: The AI part of the bot for ac is here(navigation, shooting etc)
//
//
// Author: <[email protected]>
//
#include "cube.h"
#include "bot.h"
weaponinfo_s WeaponInfoTable[MAX_WEAPONS] =
{
// DD: desired distance
// FD: fire distance
// mA: min desired Ammo
// ---- Type : -- minDD -- maxDD -- minFD -- maxFD -- mA
{ TYPE_MELEE, 0.0f, 5.0f, 0.0f, 3.5f, 0 }, // KNIFE
{ TYPE_NORMAL, 3.5f, 15.0f, 3.0f, 60.0f, 6 }, // PISTOL
{ TYPE_SNIPER, 15.0f, 80.0f, 3.0f, 180.0f, 2 }, // CARBINE // min and max FD just guesstimates for now
{ TYPE_SHOTGUN, 0.0f, 15.0f, 0.0f, 50.0f, 3 }, // SHOTGUN
{ TYPE_AUTO, 0.0f, 25.0f, 3.0f, 60.0f, 10 }, // SUBGUN
{ TYPE_SNIPER, 10.0f, 125.0f, 3.0f, 200.0f, 3 }, // SNIPER
{ TYPE_AUTO, 20.0f, 80.0f, 3.0f, 150.0f, 6 }, // ASSAULT
{ TYPE_GRENADE, 5.0f, 30.0f, 3.0f, 50.0f, 1 }, // GRENADE
{ TYPE_AUTO, 0.0f, 50.0f, 3.0f, 50.0f, 0 } // AKIMBO
};
// The best+ (elite) bots don't use the bonus-point scheme above: they weigh
// every weapon on the damage it can actually land at the current distance.
// flCloseRange/flFarRange are where the weapon starts/stop being useful,
// flDps is the raw damage per second and flAccuracy is how much of that lands
// at the ideal range (spread, recoil, travel time).
struct elite_weaponinfo_s
{
float flCloseRange;
float flIdealRange;
float flFarRange;
float flDps;
float flAccuracy;
};
static const elite_weaponinfo_s EliteWeaponInfo[MAX_WEAPONS] =
{
// ---- close -- ideal -- far ---- dps -- accuracy
{ 0.0f, 1.5f, 3.5f, 100.0f, 0.95f }, // KNIFE
{ 2.0f, 12.0f, 45.0f, 112.0f, 0.85f }, // PISTOL
{ 4.0f, 22.0f, 60.0f, 83.0f, 0.90f }, // CARBINE
{ 0.0f, 6.0f, 14.0f, 240.0f, 0.85f }, // SHOTGUN
{ 0.0f, 10.0f, 30.0f, 190.0f, 0.80f }, // SUBGUN
{ 12.0f, 50.0f, 160.0f, 70.0f, 0.95f }, // SNIPER
{ 3.0f, 18.0f, 50.0f, 200.0f, 0.75f }, // ASSAULT
{ 8.0f, 15.0f, 25.0f, 200.0f, 0.50f }, // GRENADE
{ 0.0f, 8.0f, 25.0f, 200.0f, 0.60f } // AKIMBO
};
// Code of CACBot - Start
void CACBot::GetPreferredRange(float &flMinDist, float &flMaxDist)
{
if (!m_pBotSkill->bSmartWeapons)
{
CBot::GetPreferredRange(flMinDist, flMaxDist);
return;
}
const elite_weaponinfo_s &info = EliteWeaponInfo[m_pMyEnt->gunselect];
// The range the bot actually wants to fight at, based on what the weapon
// can do (not on the optimistic table the other skills use)
flMinDist = info.flCloseRange;
flMaxDist = min(info.flFarRange, info.flIdealRange * 2.0f);
if (flMaxDist < flMinDist + 1.0f)
flMaxDist = flMinDist + 1.0f;
}
float CACBot::ScoreWeapon(int iGun, float flDist)
{
const elite_weaponinfo_s &info = EliteWeaponInfo[iGun];
// An empty magazine is no weapon at all (the knife is the exception)
if ((iGun != GUN_KNIFE) && !m_pMyEnt->mag[iGun])
return -1.0f;
// A grenade is a tool, not a primary weapon: pull it out when the throw
// makes sense, throw it and switch straight back to a gun
if (iGun == GUN_GRENADE)
{
if (!m_pBotSkill->bSmartGrenades || !ShouldThrowGrenade(flDist))
return -1.0f;
return 250.0f;
}
float flRangeFactor;
// Full damage in the ideal window, a steep drop outside of it
if ((flDist < info.flCloseRange) || (flDist > info.flFarRange))
flRangeFactor = 0.1f;
else if (flDist < info.flIdealRange)
flRangeFactor = 0.1f + 0.9f * ((flDist - info.flCloseRange) /
(info.flIdealRange - info.flCloseRange));
else
flRangeFactor = 1.0f - 0.9f * ((flDist - info.flIdealRange) /
(info.flFarRange - info.flIdealRange));
float flScore = info.flDps * info.flAccuracy * flRangeFactor;
// The weapon we already hold has no switch delay
if (iGun == m_pMyEnt->gunselect)
flScore *= 1.15f;
// Reloading in the middle of a fight is a waste of a magazine
if (m_pMyEnt->weapons[iGun] && m_pMyEnt->weapons[iGun]->reloading)
flScore *= 0.25f;
// Running dry hurts: save the last rounds for when they matter
if ((iGun != GUN_KNIFE) && (iGun != GUN_GRENADE) &&
(m_pMyEnt->ammo[iGun] <= 0) && (m_pMyEnt->mag[iGun] <= 2))
flScore *= 0.5f;
// Being able to finish the enemy with a single shot is worth setting up
if (m_pMyEnt->enemy && (m_pMyEnt->enemy->health <= guns[iGun].damage))
flScore *= 1.3f;
return flScore;
}
bool CACBot::ChooseEliteWeapon()
{
if (!m_pMyEnt->enemy)
return false;
// Keep the grenade in hand while a throw is being lined up: switching away
// lets go of it in whatever direction the bot happens to be facing
if (m_iGrenadeThrowTime > lastmillis)
return true;
float flDist = GetDistance(m_pMyEnt->enemy->o);
float flBestScore = ScoreWeapon(m_pMyEnt->gunselect, flDist);
int iBestWeapon = m_pMyEnt->gunselect;
for (int i=0;i<MAX_WEAPONS;i++)
{
if (i == m_pMyEnt->gunselect) continue;
if (!m_pMyEnt->weapons[i] || !m_pMyEnt->weapons[i]->selectable()) continue;
float flScore = ScoreWeapon(i, flDist);
// Only switch when the other weapon is clearly better: constantly
// swapping guns costs more time than a slightly better range gives
if (flScore > flBestScore * 1.15f)
{
flBestScore = flScore;
iBestWeapon = i;
}
}
if ((iBestWeapon != m_pMyEnt->gunselect) && (lastmillis > m_iChangeWeaponDelay))
SelectGun(iBestWeapon);
return true;
}
bool CACBot::ChoosePreferredWeapon()
{
if (m_pBotSkill->bSmartWeapons)
return ChooseEliteWeapon();
// Same hold while the pin is out
if (m_iGrenadeThrowTime > lastmillis)
return true;
short bestWeapon = m_pMyEnt->gunselect;
short bestWeaponScore = 0;
short sWeaponScore;
float flDist = GetDistance(m_pMyEnt->enemy->o);
// Choose a weapon
for(int i=0;i<MAX_WEAPONS;i++)
{
sWeaponScore = 0; // Minimal score for a weapon
sWeaponScore = i > 1 ? 5 : 0; // Primary are usually better
if (!m_pMyEnt->mag[i] && WeaponInfoTable[i].eWeaponType != TYPE_MELEE)
{
continue;
}
else
{
sWeaponScore += 5;
}
if((flDist >= WeaponInfoTable[i].flMinDesiredDistance) &&
(flDist <= WeaponInfoTable[i].flMaxDesiredDistance))
{
// In desired range for this weapon
sWeaponScore += 5; // Increase score much
if(i == GUN_PISTOL || WeaponInfoTable[i].eWeaponType == TYPE_MELEE)
{
if(WeaponInfoTable[m_pMyEnt->primary].eWeaponType == TYPE_SNIPER)
{
sWeaponScore += 10; // At a close range, knife & pistol are strong with a sniper like primary weapon
}
if(WeaponInfoTable[i].eWeaponType == TYPE_MELEE && WeaponInfoTable[m_pMyEnt->primary].eWeaponType == TYPE_SHOTGUN)
{
sWeaponScore -= 2; // Penalize a bit knife on close range with shotgun
}
}
}
else if ((flDist < WeaponInfoTable[i].flMinFireDistance) ||
(flDist > WeaponInfoTable[i].flMaxFireDistance))
{
continue; // Wrong distance for this weapon
}
if(i == GUN_GRENADE)
{
// Nades have high priority, but only when the throw is worth it:
// a grenade chosen just because it scores well is a grenade the
// bot holds while doing nothing else
if (!ShouldThrowGrenade(flDist))
continue;
sWeaponScore += 30;
}
// The ideal distance would be between the Min and Max desired distance.
// Score on the difference of the avarage of the Min and Max desired distance.
float flAvarage = (WeaponInfoTable[i].flMinDesiredDistance +
WeaponInfoTable[i].flMaxDesiredDistance) / 2.0f;
float flIdealDiff = fabs(flDist - flAvarage);
if (flIdealDiff < 0.5f) // Close to ideal distance
sWeaponScore += 4;
else if (flIdealDiff <= 1.0f)
sWeaponScore += 2;
// Now rate the weapon on available ammo...
if (WeaponInfoTable[i].sMinDesiredAmmo > 0)
{
// Calculate how much percent of the min desired ammo the bot has
float flDesiredPercent = (float(m_pMyEnt->ammo[i]) /
float(WeaponInfoTable[i].sMinDesiredAmmo)) *
100.0f;
if (flDesiredPercent >= 400.0f)
sWeaponScore += 4;
else if (flDesiredPercent >= 200.0f)
sWeaponScore += 3;
else if (flDesiredPercent >= 100.0f)
sWeaponScore += 1;
}
else
{
// Not needing ammo is an advantage...
sWeaponScore += 10;
}
if(sWeaponScore > bestWeaponScore)
{
bestWeaponScore = sWeaponScore;
bestWeapon = i;
}
}
if(lastmillis > m_iChangeWeaponDelay)
{
SelectGun(bestWeapon);
}
return true;
};
void CACBot::Reload(int Gun)
{
};
entity *CACBot::SearchForEnts(bool bUseWPs, float flRange, float flMaxHeight)
{
/* Entities are scored on the following things:
- Visibility
- For ammo: Need(ie has this bot much of this type or not)
- distance
*/
float flDist;
entity *pNewTargetEnt = NULL;
waypoint_s *pWptNearBot = NULL, *pBestWpt = NULL;
short sScore, sHighestScore = 0;
if ((WaypointClass.m_iWaypointCount >= 1) && bUseWPs)
pWptNearBot = GetNearestWaypoint(15.0f);
#ifdef WP_FLOOD
if (!pWptNearBot && bUseWPs)
pWptNearBot = GetNearestFloodWP(5.0f);
#endif
loopv(ents)
{
sScore = 0;
entity &e = ents[i];
vec o(e.x, e.y, S(e.x, e.y)->floor+player1->eyeheight);
if (!ents[i].spawned) continue;
if (OUTBORD(e.x, e.y)) continue;
bool bInteresting = false;
short sAmmo = 0, sMaxAmmo = 0;
switch(e.type)
{
case I_CLIPS:
sMaxAmmo = ammostats[GUN_PISTOL].max;
bInteresting = (m_pMyEnt->ammo[GUN_PISTOL]<sMaxAmmo);
sAmmo = m_pMyEnt->ammo[GUN_PISTOL];
break;
case I_AMMO:
sMaxAmmo = ammostats[m_pMyEnt->primary].max;
bInteresting = (m_pMyEnt->ammo[m_pMyEnt->primary]<sMaxAmmo);
sAmmo = m_pMyEnt->ammo[m_pMyEnt->primary];
break;
case I_GRENADE:
sMaxAmmo = ammostats[GUN_GRENADE].max;
bInteresting = (m_pMyEnt->mag[GUN_GRENADE]<sMaxAmmo);
sAmmo = -1;
break;
case I_HEALTH:
sMaxAmmo = powerupstats[0].max;
bInteresting = (m_pMyEnt->health < sMaxAmmo);
sAmmo = m_pMyEnt->health;
break;
case I_HELMET:
case I_ARMOUR:
sMaxAmmo = powerupstats[I_ARMOUR-I_HEALTH].max;
bInteresting = (m_pMyEnt->armour < sMaxAmmo);
sAmmo = m_pMyEnt->armour;
break;
case I_AKIMBO:
bInteresting = !m_pMyEnt->akimbo;
sAmmo = -1;
break;
};
if (!bInteresting)
continue; // Not an interesting item, skip
// Score on ammo and need
// Akimbo & nade
if (sAmmo == -1)
{
sScore += 75; // Bonus
}
else
{
// Calculate current percentage of max ammo
float percent = ((float)sAmmo / (float)sMaxAmmo) * 100.0f;
if (percent > 100.0f) percent = 100.0f;
sScore += ((100 - short(percent))/2);
}
flDist = GetDistance(o);
if (flDist > flRange) continue;
// Score on distance
float f = flDist;
if (f > 100.0f) f = 100.0f;
sScore += ((100 - short(f)) / 2);
waypoint_s *pWptNearEnt = NULL;
// If this entity isn't visible check if there is a nearby waypoint
if (!IsReachable(o, flMaxHeight))//(!IsVisible(o))
{
if (!pWptNearBot) continue;
#ifdef WP_FLOOD
if (pWptNearBot->pNode->iFlags & W_FL_FLOOD)
pWptNearEnt = GetNearestFloodWP(o, 8.0f);
else
#endif
pWptNearEnt = GetNearestWaypoint(o, 15.0f);
if (!pWptNearEnt) continue;
}
// Score on visibility
if (pWptNearEnt == NULL) // Ent is visible
sScore += 30;
else
sScore += 15;
if (sScore > sHighestScore)
{
// Found a valid wp near the bot and the ent,so...lets store it :)
if (pWptNearEnt)
pBestWpt = pWptNearEnt;
else
pBestWpt = NULL; // Best ent so far doesn't need any waypoints
sHighestScore = sScore;
pNewTargetEnt = &ents[i];
}
}
if (pNewTargetEnt)
{
// Need waypoints to reach it?
if (pBestWpt)
{
ResetWaypointVars();
SetCurrentWaypoint(pWptNearBot);
SetCurrentGoalWaypoint(pBestWpt);
}
m_vGoal.x = pNewTargetEnt->x;
m_vGoal.y = pNewTargetEnt->y;
m_vGoal.z = S(pNewTargetEnt->x, pNewTargetEnt->y)->floor+player1->eyeheight;
}
return pNewTargetEnt;
}
bool CACBot::HeadToTargetEnt()
{
if (m_pTargetEnt)
{
vec o(m_pTargetEnt->x, m_pTargetEnt->y,
S(m_pTargetEnt->x, m_pTargetEnt->y)->floor+m_pMyEnt->eyeheight);
if (m_pTargetEnt->spawned && (!UnderWater(m_pMyEnt->o) ||
!UnderWater(o)))
{
bool bIsVisible = false;
if (m_pCurrentGoalWaypoint)
{
if ((GetDistance(o) <= 20.0f) && IsReachable(o, 1.0f))
bIsVisible = true;
else if (HeadToGoal())
{
//debugbeam(m_pMyEnt->o, m_pCurrentWaypoint->pNode->v_origin);
//debugbeam(m_pMyEnt->o,
// m_pCurrentGoalWaypoint->pNode->v_origin);
AddDebugText("Using WPs for ents");
return true;
}
}
else
bIsVisible = IsVisible(o);
if (bIsVisible)
{
if (m_pCurrentWaypoint || m_pCurrentGoalWaypoint)
{
condebug("ent is now visible");
ResetWaypointVars();
}
float flHeightDiff = o.z - m_pMyEnt->o.z;
bool bToHigh = false;
if (Get2DDistance(o) <= 2.0f)
{
if (flHeightDiff >= 1.5f)
{
if (flHeightDiff <= JUMP_HEIGHT)
{
#ifndef RELEASE_BUILD
char sz[64];
sprintf(sz, "Ent z diff: %f", o.z-m_pMyEnt->o.z);
condebug(sz);
#endif
// Jump if close to ent and the ent is high
m_pMyEnt->jumpnext = true;
}
else
bToHigh = true;
}
}
if (!bToHigh)
{
AimToVec(o);
// Elite bots strafe-run to the item and walk around the
// geometry instead of into it
if (m_pBotSkill->bSmartMovement)
SteerTo(o, true);
return true;
}
}
}
}
return false;
}
bool CACBot::DoSPStuff()
{
return false;
}
// Code of CACBot - End