Files
2026-05-22 22:26:19 -07:00

1450 lines
38 KiB
C++

// BotAI.cpp
//
#pragma hdrstop
#include "precompiled.h"
#include "../Game_local.h"
#include "../ai/NAVCallback_FindCoverArea.h"
#define IDEAL_ATTACKDIST 140
int rvmBot::WP_MACHINEGUN = -1;
int rvmBot::WP_SHOTGUN = -1;
int rvmBot::WP_PLASMAGUN = -1;
int rvmBot::WP_ROCKET_LAUNCHER = -1;
static bool BotAIValidEntityNum(int entityNum) {
return entityNum >= 0 && entityNum < gameLocal.num_entities && gameLocal.entities[entityNum] != NULL;
}
static float BotAIClampFloat(float minValue, float maxValue, float value) {
if (value < minValue) {
return minValue;
}
if (value > maxValue) {
return maxValue;
}
return value;
}
static idVec3 BotAIEntityCenter(idEntity* ent) {
if (ent == NULL || ent->GetPhysics() == NULL) {
return idVec3(0.0f, 0.0f, 0.0f);
}
return ent->GetPhysics()->GetOrigin() + ent->GetPhysics()->GetBounds().GetCenter();
}
static bool BotAIPointWithinFOV(const idVec3& eye, const idAngles& viewangles, const idVec3& point, float fov) {
if (fov >= 359.0f) {
return true;
}
idVec3 dir = point - eye;
if (dir.LengthSqr() < Square(1.0f)) {
return true;
}
idAngles targetAngles = dir.ToAngles();
const float halfFov = fov * 0.5f;
const float yawDelta = idMath::Fabs(idMath::AngleDelta(targetAngles[YAW], viewangles[YAW]));
const float pitchDelta = idMath::Fabs(idMath::AngleDelta(targetAngles[PITCH], viewangles[PITCH]));
return yawDelta <= halfFov && pitchDelta <= halfFov;
}
static bool BotAITraceCanHitEntity(int passEntityNum, int targetEntityNum, const idVec3& start, const idVec3& end, int mask) {
trace_t trace;
gameLocal.Trace(trace, start, end, mask, passEntityNum);
return trace.fraction >= 1.0f || trace.c.entityNum == targetEntityNum;
}
static bool BotAIHasUsableFightWeapon(const int* inventory) {
if (inventory == NULL) {
return false;
}
return (inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] > 0) ||
(inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] > 0) ||
(inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] > 0) ||
(inventory[INVENTORY_MACHINEGUN] > 0 && inventory[INVENTORY_BULLETS] > 0);
}
static bool BotAIHasPowerFightWeapon(const int* inventory) {
if (inventory == NULL) {
return false;
}
return (inventory[INVENTORY_ROCKETLAUNCHER] > 0 && inventory[INVENTORY_ROCKETS] > 0) ||
(inventory[INVENTORY_PLASMAGUN] > 0 && inventory[INVENTORY_CELLS] > 0) ||
(inventory[INVENTORY_SHOTGUN] > 0 && inventory[INVENTORY_SHELLS] > 0);
}
static bool BotAIHasOnlyPistolOrDryWeapons(const bot_state_t* bs) {
if (bs == NULL) {
return true;
}
const int* inventory = bs->inventory;
const bool ownsNonPistolWeapon = inventory[INVENTORY_MACHINEGUN] > 0 ||
inventory[INVENTORY_SHOTGUN] > 0 ||
inventory[INVENTORY_PLASMAGUN] > 0 ||
inventory[INVENTORY_ROCKETLAUNCHER] > 0;
if (!ownsNonPistolWeapon) {
return true;
}
if (!BotAIHasUsableFightWeapon(inventory)) {
return true;
}
const bool onlyMachinegun = inventory[INVENTORY_MACHINEGUN] > 0 &&
inventory[INVENTORY_SHOTGUN] <= 0 &&
inventory[INVENTORY_PLASMAGUN] <= 0 &&
inventory[INVENTORY_ROCKETLAUNCHER] <= 0;
return onlyMachinegun && inventory[INVENTORY_BULLETS] < 25;
}
static bool BotAINeedsTacticalPickup(const bot_state_t* bs) {
if (bs == NULL) {
return false;
}
if (BotAIHasOnlyPistolOrDryWeapons(bs)) {
return true;
}
if (bs->inventory[INVENTORY_HEALTH] < 55) {
return true;
}
if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] <= 2) {
return true;
}
if (bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] <= 12) {
return true;
}
if (bs->inventory[INVENTORY_SHOTGUN] > 0 && bs->inventory[INVENTORY_SHELLS] <= 4) {
return true;
}
return false;
}
static float BotAITacticalPickupRange(const bot_state_t* bs, float requestedRange) {
float range = requestedRange;
if (bs == NULL || bs->enemy < 0) {
return range;
}
if (BotAINeedsTacticalPickup(bs)) {
if (range < 650.0f) {
range = 650.0f;
}
}
else if (!BotAIHasPowerFightWeapon(bs->inventory)) {
if (range < 400.0f) {
range = 400.0f;
}
}
else if (range < 260.0f) {
range = 260.0f;
}
return range;
}
/*
=========================
rvmBot::BotIsDead
=========================
*/
bool rvmBot::BotIsDead(bot_state_t* bs)
{
if (bs == NULL || !BotAIValidEntityNum(bs->client))
{
return true;
}
idPlayer* player = gameLocal.GetClientByNum(bs->client);
return player == NULL || player->health <= 0;
}
/*
==================
rvmBot::BotReachedGoal
==================
*/
bool rvmBot::BotReachedGoal(bot_state_t* bs, bot_goal_t* goal)
{
if (goal->flags & GFL_ITEM)
{
//if touching the goal
if (botGoalManager.BotTouchingGoal(bs->origin, goal))
{
if (!(goal->flags & GFL_DROPPED))
{
botGoalManager.BotSetAvoidGoalTime(bs->gs, goal->number, -1);
}
return true;
}
//if the goal isn't there
if (botGoalManager.BotItemGoalInVisButNotVisible(bs->entitynum, bs->eye, bs->viewangles, goal))
{
return true;
}
}
else
{
//if touching the goal
if (botGoalManager.BotTouchingGoal(bs->origin, goal))
{
return true;
}
}
return false;
}
/*
==================
rvmBot::BotChooseWeapon
==================
*/
void rvmBot::BotChooseWeapon(bot_state_t* bs)
{
if (bs == NULL)
{
return;
}
const bool haveRocketLauncher = WP_ROCKET_LAUNCHER >= 0 && bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 && bs->inventory[INVENTORY_ROCKETS] > 0;
const bool havePlasmaGun = WP_PLASMAGUN >= 0 && bs->inventory[INVENTORY_PLASMAGUN] > 0 && bs->inventory[INVENTORY_CELLS] > 0;
const bool haveShotgun = WP_SHOTGUN >= 0 && bs->inventory[INVENTORY_SHOTGUN] > 0 && bs->inventory[INVENTORY_SHELLS] > 0;
const bool haveMachinegun = WP_MACHINEGUN >= 0 && bs->inventory[INVENTORY_MACHINEGUN] > 0 && bs->inventory[INVENTORY_BULLETS] > 0;
// The fuzzy weapon table is still evaluated, but it should not be able to
// pin a bot to the machinegun after the bot has picked up rockets, plasma,
// or shells. Doom 3 weapon slots are runtime values from SlotForWeapon(), so
// choose from those real slots here instead of trusting the Quake-style
// weapon config numbers blindly.
int practicalWeaponNum = -1;
const int enemyDist = (bs->enemy >= 0) ? bs->inventory[ENEMY_HORIZONTAL_DIST] : 0;
if (bs->enemy >= 0 && enemyDist > 0)
{
if (enemyDist < 160)
{
// Very close: avoid splash damage unless there is no safer weapon.
if (haveShotgun)
{
practicalWeaponNum = WP_SHOTGUN;
}
else if (havePlasmaGun)
{
practicalWeaponNum = WP_PLASMAGUN;
}
else if (haveMachinegun)
{
practicalWeaponNum = WP_MACHINEGUN;
}
else if (haveRocketLauncher)
{
practicalWeaponNum = WP_ROCKET_LAUNCHER;
}
}
else if (enemyDist < 700)
{
// Mid range: rockets are the highest value deathmatch weapon.
if (haveRocketLauncher)
{
practicalWeaponNum = WP_ROCKET_LAUNCHER;
}
else if (havePlasmaGun)
{
practicalWeaponNum = WP_PLASMAGUN;
}
else if (haveShotgun)
{
practicalWeaponNum = WP_SHOTGUN;
}
else if (haveMachinegun)
{
practicalWeaponNum = WP_MACHINEGUN;
}
}
else
{
// Long range: plasma is easier to land consistently; use rockets if
// plasma is unavailable.
if (havePlasmaGun)
{
practicalWeaponNum = WP_PLASMAGUN;
}
else if (haveRocketLauncher)
{
practicalWeaponNum = WP_ROCKET_LAUNCHER;
}
else if (haveMachinegun)
{
practicalWeaponNum = WP_MACHINEGUN;
}
else if (haveShotgun)
{
practicalWeaponNum = WP_SHOTGUN;
}
}
}
else
{
// No active enemy or no distance sample yet: keep the bot on the best
// generally useful weapon so it is ready when a fight starts.
if (haveRocketLauncher)
{
practicalWeaponNum = WP_ROCKET_LAUNCHER;
}
else if (havePlasmaGun)
{
practicalWeaponNum = WP_PLASMAGUN;
}
else if (haveShotgun)
{
practicalWeaponNum = WP_SHOTGUN;
}
else if (haveMachinegun)
{
practicalWeaponNum = WP_MACHINEGUN;
}
}
int newweaponnum = practicalWeaponNum;
if (newweaponnum < 0)
{
// BotChooseBestFightWeapon returns bot weapon-config numbers. Only use
// the fuzzy result if it already matches one of the engine runtime slots;
// otherwise keep the current weapon instead of sending a bogus impulse.
const int fuzzyWeaponNum = botWeaponInfoManager.BotChooseBestFightWeapon(bs->ws, bs->inventory);
if (fuzzyWeaponNum == WP_MACHINEGUN || fuzzyWeaponNum == WP_SHOTGUN ||
fuzzyWeaponNum == WP_PLASMAGUN || fuzzyWeaponNum == WP_ROCKET_LAUNCHER)
{
newweaponnum = fuzzyWeaponNum;
}
}
if (newweaponnum < 0)
{
if (bs->weaponnum >= 0)
{
newweaponnum = bs->weaponnum;
}
else
{
newweaponnum = spawnArgs.GetInt("current_weapon", "1");
}
}
if (bs->weaponnum != newweaponnum)
{
bs->weaponchange_time = Bot_Time();
bs->botinput.lastWeaponNum = -1;
}
bs->weaponnum = newweaponnum;
bs->botinput.weapon = bs->weaponnum;
SelectWeapon(bs->weaponnum, false);
}
/*
==================
rvmBot::BotGetItemLongTermGoal
==================
*/
int rvmBot::BotGetItemLongTermGoal(bot_state_t* bs, int tfl, bot_goal_t* goal)
{
//if the bot has no goal
if (!botGoalManager.BotGetTopGoal(bs->gs, goal))
{
//BotAI_Print(PRT_MESSAGE, "no ltg on stack\n");
bs->ltg_time = 0;
}
//if the bot touches the current goal
else if (BotReachedGoal(bs, goal))
{
BotChooseWeapon(bs);
bs->ltg_time = 0;
}
// Check to see that we can get to our goal, if not get a new goal.
//if (bs->numMovementWaypoints > 0)
//{
// trace_t tr;
// gentity_t* ent = &g_entities[bs->client];
// vec3_t waypoint;
//
// VectorCopy(bs->movement_waypoints[bs->currentWaypoint], waypoint);
// waypoint[2] += 5.0f;
//
// trap_Trace(&tr, ent->r.currentOrigin, NULL, NULL, waypoint, bs->client, CONTENTS_SOLID);
//
// if (tr.fraction <= 0.7f)
// {
// BotChooseWeapon(bs);
// bs->ltg_time = 0;
// }
//}
//if it is time to find a new long term goal
if (bs->ltg_time == 0)
{
//pop the current goal from the stack
botGoalManager.BotPopGoal(bs->gs);
//BotAI_Print(PRT_MESSAGE, "%s: choosing new ltg\n", ClientName(bs->client, netname, sizeof(netname)));
//choose a new goal
//BotAI_Print(PRT_MESSAGE, "%6.1f client %d: BotChooseLTGItem\n", Bot_Time(), bs->client);
if (botGoalManager.BotChooseLTGItem(bs->gs, bs->origin, bs->inventory, tfl))
{
char buf[128];
//get the goal at the top of the stack
botGoalManager.BotGetTopGoal(bs->gs, goal);
botGoalManager.BotGoalName(goal->number, buf, sizeof(buf));
common->Printf("%1.1f: new long term goal %s\n", Bot_Time(), buf);
bs->ltg_time = Bot_Time() + 20;
bs->currentGoal.framenum = gameLocal.framenum;
}
else //the bot gets sorta stuck with all the avoid timings, shouldn't happen though
{
//
//trap_BotDumpAvoidGoals(bs->gs);
//reset the avoid goals and the avoid reach
botGoalManager.BotResetAvoidGoals(bs->gs);
//BotResetAvoidReach(bs->ms);
}
//get the goal at the top of the stack
if (!botGoalManager.BotGetTopGoal(bs->gs, goal))
{
return false;
}
bs->currentGoal.framenum = gameLocal.framenum;
return true;
}
return true;
}
/*
==================
rvmBot::EntityIsDead
==================
*/
bool rvmBot::EntityIsDead(idEntity* entity)
{
if (entity == NULL)
{
return true;
}
idPlayer* player = entity->Cast<idPlayer>();
return player != NULL && player->health <= 0;
}
/*
==================
BotEntityVisibleTest
returns visibility in the range [0, 1] taking fog and water surfaces into account
==================
*/
float rvmBot::BotEntityVisibleTest(int viewer, idVec3 eye, idAngles viewangles, float fov, int ent, bool allowHeightTest)
{
if (!BotAIValidEntityNum(viewer) || !BotAIValidEntityNum(ent))
{
return 0.0f;
}
idEntity* entinfo = gameLocal.entities[ent];
if (entinfo == NULL || entinfo->GetPhysics() == NULL)
{
return 0.0f;
}
idBounds bounds = entinfo->GetPhysics()->GetBounds();
idVec3 origin = entinfo->GetPhysics()->GetOrigin();
idVec3 center = bounds.GetCenter();
idVec3 testPoints[3];
testPoints[0] = origin + center;
testPoints[1] = origin + idVec3(center[0], center[1], bounds[0][2] + 8.0f);
testPoints[2] = origin + idVec3(center[0], center[1], bounds[1][2] - 4.0f);
if (!BotAIPointWithinFOV(eye, viewangles, testPoints[0], fov))
{
return 0.0f;
}
const int pc = gameLocal.clip.PointContents(eye);
const int infog = (pc & CONTENTS_FOG);
const int inwater = (pc & (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER));
float bestvis = 0.0f;
for (int i = 0; i < 3; i++)
{
idVec3 start = eye;
idVec3 end = testPoints[i];
int contents_mask = CONTENTS_SOLID | CONTENTS_PLAYERCLIP;
int passent = viewer;
int hitent = ent;
if (gameLocal.clip.PointContents(end) & (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER))
{
contents_mask |= (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER);
}
if (inwater)
{
if (!(contents_mask & (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER)))
{
passent = ent;
hitent = viewer;
start = end;
end = eye;
}
contents_mask ^= (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER);
}
trace_t trace;
gameLocal.Trace(trace, start, end, contents_mask, passent);
if (trace.fraction < 0.9f && allowHeightTest)
{
idVec3 highEnd = end;
highEnd[2] += 50.0f;
gameLocal.Trace(trace, start, highEnd, contents_mask, passent);
}
float waterfactor = 1.0f;
if (trace.c.contents & (CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER))
{
contents_mask &= ~(CONTENTS_LAVA | CONTENTS_SLIME | CONTENTS_WATER);
gameLocal.Trace(trace, trace.endpos, end, contents_mask, passent);
waterfactor = 0.5f;
}
if (trace.fraction >= 1.0f || trace.c.entityNum == hitent)
{
float squaredfogdist = 0.0f;
const int otherinfog = (gameLocal.clip.PointContents(end) & CONTENTS_FOG);
idVec3 dir;
if (infog && otherinfog)
{
dir = trace.endpos - eye;
squaredfogdist = dir.LengthSqr();
}
else if (infog)
{
idVec3 fogStart = trace.endpos;
gameLocal.Trace(trace, fogStart, eye, CONTENTS_FOG, viewer);
dir = eye - trace.endpos;
squaredfogdist = dir.LengthSqr();
}
else if (otherinfog)
{
idVec3 fogEnd = trace.endpos;
gameLocal.Trace(trace, eye, fogEnd, CONTENTS_FOG, viewer);
dir = fogEnd - trace.endpos;
squaredfogdist = dir.LengthSqr();
}
float vis = 1.0f / ((squaredfogdist * 0.001f) < 1.0f ? 1.0f : (squaredfogdist * 0.001f));
vis *= waterfactor;
if (vis > bestvis)
{
bestvis = vis;
}
if (bestvis >= 0.95f)
{
return bestvis;
}
}
}
return bestvis;
}
/*
==================
rvmBot::BotEntityVisible
==================
*/
float rvmBot::BotEntityVisible(int viewer, idVec3 eye, idAngles viewangles, float fov, int ent)
{
return BotEntityVisibleTest(viewer, eye, viewangles, fov, ent, true);
}
/*
==================
rvmBot::BotUpdateBattleInventory
==================
*/
void rvmBot::BotUpdateBattleInventory(bot_state_t* bs, int enemy)
{
if (bs == NULL || !BotAIValidEntityNum(enemy))
{
return;
}
idEntity* entinfo = gameLocal.entities[enemy];
if (entinfo == NULL || entinfo->GetPhysics() == NULL)
{
return;
}
idVec3 dir = entinfo->GetPhysics()->GetOrigin() - bs->origin;
bs->inventory[ENEMY_HEIGHT] = (int)dir[2];
dir[2] = 0;
bs->inventory[ENEMY_HORIZONTAL_DIST] = (int)dir.Length();
}
/*
==================
rvmBot::BotAggression
==================
*/
float rvmBot::BotAggression(bot_state_t* bs)
{
//if the bot has quad
if (bs->inventory[INVENTORY_QUAD])
{
//if the bot is not holding the gauntlet or the enemy is really nearby
if (bs->weaponnum != 0 ||
bs->inventory[ENEMY_HORIZONTAL_DIST] < 80)
{
return 70;
}
}
//if the enemy is located way higher than the bot
if (bs->inventory[ENEMY_HEIGHT] > 200)
{
return 0;
}
//if the bot is very low on health
if (bs->inventory[INVENTORY_HEALTH] < 60)
{
return 0;
}
//if the bot is low on health
if (bs->inventory[INVENTORY_HEALTH] < 80)
{
//if the bot has insufficient armor
if (bs->inventory[INVENTORY_ARMOR] < 40)
{
return 0;
}
}
//if the bot can use the bfg
if (bs->inventory[INVENTORY_BFG10K] > 0 &&
bs->inventory[INVENTORY_BFGAMMO] > 7)
{
return 100;
}
//if the bot can use the railgun
if (bs->inventory[INVENTORY_RAILGUN] > 0 &&
bs->inventory[INVENTORY_SLUGS] > 5)
{
return 95;
}
//if the bot can use the lightning gun
if (bs->inventory[INVENTORY_LIGHTNING] > 0 &&
bs->inventory[INVENTORY_LIGHTNINGAMMO] > 50)
{
return 90;
}
//if the bot can use the rocketlauncher
if (bs->inventory[INVENTORY_ROCKETLAUNCHER] > 0 &&
bs->inventory[INVENTORY_ROCKETS] > 5)
{
return 90;
}
//if the bot can use the plasmagun
if (bs->inventory[INVENTORY_PLASMAGUN] > 0 &&
bs->inventory[INVENTORY_CELLS] > 40)
{
return 85;
}
//if the bot can use the grenade launcher
if (bs->inventory[INVENTORY_GRENADELAUNCHER] > 0 &&
bs->inventory[INVENTORY_GRENADES] > 10)
{
return 80;
}
//if the bot can use the shotgun
if (bs->inventory[INVENTORY_SHOTGUN] > 0 &&
bs->inventory[INVENTORY_SHELLS] > 10)
{
return 50;
}
if (bs->inventory[INVENTORY_BULLETS] > 0)
{
return 60;
}
//otherwise the bot is not feeling too good
return 0;
}
/*
==================
rvmBot::BotWantsToRetreat
==================
*/
int rvmBot::BotWantsToRetreat(bot_state_t* bs)
{
if (BotAggression(bs) < 50)
{
return true;
}
return false;
}
/*
==================
rvmBot::BotBattleUseItems
==================
*/
void rvmBot::BotBattleUseItems(bot_state_t* bs)
{
if (bs->inventory[INVENTORY_HEALTH] < 40)
{
if (bs->inventory[INVENTORY_TELEPORTER] > 0)
{
bs->botinput.actionflags |= ACTION_USE;
}
}
if (bs->inventory[INVENTORY_HEALTH] < 60)
{
if (bs->inventory[INVENTORY_MEDKIT] > 0)
{
//trap_EA_Use(bs->client);
bs->botinput.actionflags |= ACTION_USE;
}
}
}
/*
==================
rvmBot::BotFindEnemy
==================
*/
int rvmBot::BotFindEnemy(bot_state_t* bs, int curenemy)
{
if (bs == NULL || !BotAIValidEntityNum(bs->client))
{
return false;
}
float alertness = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_ALERTNESS, 0, 1);
float easyfragger = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_EASY_FRAGGER, 0, 1);
idPlayer* clientEnt = gameLocal.entities[bs->client]->Cast<idPlayer>();
if (clientEnt == NULL)
{
return false;
}
const bool healthdecrease = bs->lasthealth > bs->inventory[INVENTORY_HEALTH];
bs->lasthealth = bs->inventory[INVENTORY_HEALTH];
float cursquaredist = 0.0f;
if (curenemy >= 0 && BotAIValidEntityNum(curenemy))
{
idPlayer* curenemyinfo = gameLocal.entities[curenemy]->Cast<idPlayer>();
if (curenemyinfo != NULL)
{
idVec3 dir = curenemyinfo->GetPhysics()->GetOrigin() - bs->origin;
cursquaredist = dir.LengthSqr();
}
}
for (int i = 0; i < MAX_CLIENTS && i < gameLocal.num_entities; i++)
{
if (i == bs->client || i == curenemy || !BotAIValidEntityNum(i))
{
continue;
}
idPlayer* entinfo = gameLocal.entities[i]->Cast<idPlayer>();
if (entinfo == NULL)
{
continue;
}
if (EntityIsDead(entinfo) || i == bs->entitynum || entinfo->spectating)
{
continue;
}
idVec3 potentialTargetOrigin = entinfo->GetPhysics()->GetOrigin();
idVec3 dir = potentialTargetOrigin - bs->origin;
float squaredist = dir.LengthSqr();
if (curenemy >= 0 && cursquaredist > 0.0f && squaredist > cursquaredist)
{
continue;
}
if (squaredist > Square(900.0f + alertness * 4000.0f))
{
continue;
}
float fov;
if (curenemy < 0 && (healthdecrease || entinfo->IsShooting()))
{
fov = 360.0f;
}
else
{
const float clampedDist = squaredist > Square(810.0f) ? Square(810.0f) : squaredist;
fov = 180.0f - (90.0f - clampedDist / (810.0f * 9.0f));
}
float vis = 1.0f;
if (bs->attackerEntity != entinfo)
{
vis = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, fov, i);
if (vis <= 0.0f)
{
continue;
}
}
// If the bot can avoid a distant, unaware enemy and does not want a fight,
// do not switch away from the current goal.
if (curenemy < 0 && squaredist > Square(100.0f) && !healthdecrease && !entinfo->IsShooting())
{
if (!entinfo->CheckFOV(clientEnt->GetPhysics()->GetOrigin()))
{
BotUpdateBattleInventory(bs, i);
if (BotWantsToRetreat(bs) && easyfragger < 0.5f)
{
continue;
}
}
}
bs->enemy = i;
bs->enemysight_time = curenemy >= 0 ? Bot_Time() - 2.0f : Bot_Time();
bs->enemysuicide = false;
bs->enemydeath_time = 0;
bs->enemyvisible_time = Bot_Time();
bs->lastenemyorigin = potentialTargetOrigin;
bs->last_enemy_visible_position = potentialTargetOrigin;
bs->enemyorigin = potentialTargetOrigin;
bs->enemyposition_time = Bot_Time();
return true;
}
return false;
}
/*
==================
rvmBot::BotMoveToGoal
==================
*/
void rvmBot::BotMoveToGoal(bot_state_t* bs, bot_goal_t* goal)
{
bs->currentGoal = *goal;
bs->currentGoal.framenum = gameLocal.framenum;
}
/*
==================
rvmBot::BotAimAtEnemy
==================
*/
void rvmBot::BotAimAtEnemy(bot_state_t* bs)
{
if (bs == NULL || bs->enemy < 0 || !BotAIValidEntityNum(bs->enemy) || !BotAIValidEntityNum(bs->entitynum))
{
return;
}
idPlayer* self = gameLocal.entities[bs->entitynum]->Cast<idPlayer>();
idPlayer* entinfo = gameLocal.entities[bs->enemy]->Cast<idPlayer>();
if (self == NULL || entinfo == NULL || entinfo->GetPhysics() == NULL)
{
return;
}
float aim_skill = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL, 0, 1);
float aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY, 0, 1);
if (aim_skill > 0.95f)
{
const float reactiontime = 0.5f * botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
if (bs->enemysight_time > Bot_Time() - reactiontime || bs->teleport_time > Bot_Time() - reactiontime)
{
return;
}
}
weaponinfo_t wi;
botWeaponInfoManager.BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);
const bool selectedMachinegun = bs->weaponnum == WP_MACHINEGUN;
const bool selectedShotgun = bs->weaponnum == WP_SHOTGUN;
const bool selectedPlasmaGun = bs->weaponnum == WP_PLASMAGUN;
const bool selectedRocketLauncher = bs->weaponnum == WP_ROCKET_LAUNCHER;
const int selectedWeaponInventory = selectedMachinegun ? INVENTORY_MACHINEGUN :
selectedShotgun ? INVENTORY_SHOTGUN :
selectedPlasmaGun ? INVENTORY_PLASMAGUN :
selectedRocketLauncher ? INVENTORY_ROCKETLAUNCHER : -1;
if (selectedWeaponInventory >= 0 && wi.weaponindex != selectedWeaponInventory)
{
for (int weaponInfoNum = 0; weaponInfoNum < BOT_MAX_WEAPONS; weaponInfoNum++)
{
weaponinfo_t candidateWi;
botWeaponInfoManager.BotGetWeaponInfo(bs->ws, weaponInfoNum, &candidateWi);
if (candidateWi.number == weaponInfoNum && candidateWi.weaponindex == selectedWeaponInventory)
{
wi = candidateWi;
break;
}
}
}
if (selectedMachinegun)
{
aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_MACHINEGUN, 0, 1);
}
else if (selectedShotgun)
{
aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_SHOTGUN, 0, 1);
}
else if (selectedRocketLauncher)
{
aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_ROCKETLAUNCHER, 0, 1);
aim_skill = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_ROCKETLAUNCHER, 0, 1);
}
else if (selectedPlasmaGun)
{
aim_accuracy = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_ACCURACY_PLASMAGUN, 0, 1);
aim_skill = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_AIM_SKILL_PLASMAGUN, 0, 1);
}
aim_accuracy = BotAIClampFloat(0.0001f, 1.0f, aim_accuracy);
aim_skill = BotAIClampFloat(0.0f, 1.0f, aim_skill);
const float enemyvisible = BotEntityVisible(bs->entitynum, bs->eye, bs->viewangles, 360.0f, bs->enemy);
idVec3 bestorigin;
idVec3 start = bs->eye;
if (enemyvisible > 0.0f)
{
bs->enemyvisible_time = Bot_Time();
bs->lastenemyorigin = entinfo->GetPhysics()->GetOrigin();
bs->last_enemy_visible_position = bs->lastenemyorigin;
// Aim center-mass by default. Eye-only aiming misses badly when the bot is
// above the target on stairs because the head/eye ray is often clipped by the
// step edge while the torso is visible.
idVec3 enemyOrigin = entinfo->GetPhysics()->GetOrigin();
idBounds enemyBounds = entinfo->GetPhysics()->GetBounds();
const float enemyMinZ = enemyBounds[0][2];
const float enemyMaxZ = enemyBounds[1][2];
const float enemyCenterZ = (enemyMinZ + enemyMaxZ) * 0.5f;
const float enemyChestZ = enemyMinZ + (enemyMaxZ - enemyMinZ) * 0.65f;
idVec3 candidates[5];
candidates[0] = enemyOrigin + idVec3(0.0f, 0.0f, enemyChestZ);
candidates[1] = enemyOrigin + idVec3(0.0f, 0.0f, enemyCenterZ);
candidates[2] = entinfo->GetEyePosition();
candidates[3] = enemyOrigin + idVec3(0.0f, 0.0f, enemyMinZ + 12.0f);
candidates[4] = BotAIEntityCenter(entinfo);
bestorigin = (wi.proj.damagetype & DAMAGETYPE_RADIAL) ? candidates[1] : candidates[0];
for (int i = 0; i < 5; i++)
{
if (BotAITraceCanHitEntity(bs->entitynum, bs->enemy, start, candidates[i], MASK_SHOT))
{
bestorigin = candidates[i];
break;
}
}
// Linear lead for projectile weapons. The old code copied an uninitialized
// vector backwards and then changed the movement goal instead of changing aim.
if (wi.speed > 1.0f)
{
idVec3 toTarget = bestorigin - start;
float leadTime = toTarget.Length() / wi.speed;
leadTime = BotAIClampFloat(0.0f, 1.25f, leadTime);
idVec3 enemyVelocity = entinfo->GetPhysics()->GetLinearVelocity();
// Horizontal lead matters most. Clamp vertical lead so stairs/elevators do
// not make projectile aim sail over or under the body.
enemyVelocity[2] = BotAIClampFloat(-120.0f, 120.0f, enemyVelocity[2]);
bestorigin += enemyVelocity * (leadTime * aim_skill);
}
// Rockets are usually stronger when aimed at the floor near the enemy, but
// only do this if the impact point is visible and not dangerously close.
if (aim_skill > 0.6f && (wi.proj.damagetype & DAMAGETYPE_RADIAL))
{
idVec3 end = entinfo->GetPhysics()->GetOrigin();
end[2] -= 96.0f;
trace_t trace;
gameLocal.Trace(trace, entinfo->GetPhysics()->GetOrigin(), end, MASK_SHOT, bs->enemy);
idVec3 groundtarget = trace.endpos;
groundtarget[2] -= 4.0f;
gameLocal.Trace(trace, start, groundtarget, MASK_SHOT, bs->entitynum);
if (idMath::Fabs(trace.endpos[2] - groundtarget[2]) < 50.0f)
{
idVec3 fromBot = trace.endpos - start;
idVec3 fromEnemy = trace.endpos - entinfo->GetPhysics()->GetOrigin();
if (fromBot.LengthSqr() > Square(100.0f) && fromEnemy.LengthSqr() < Square(160.0f))
{
bestorigin = trace.endpos;
}
}
}
const float miss = 1.0f - aim_accuracy;
bestorigin[0] += 10.0f * rvmBotUtil::crandom() * miss;
bestorigin[1] += 10.0f * rvmBotUtil::crandom() * miss;
bestorigin[2] += 6.0f * rvmBotUtil::crandom() * miss;
}
else
{
bestorigin = bs->lastenemyorigin;
if (bestorigin.LengthSqr() < Square(1.0f))
{
bestorigin = entinfo->GetPhysics()->GetOrigin();
}
bestorigin[2] += 16.0f;
}
trace_t trace;
gameLocal.Trace(trace, bs->eye, bestorigin, MASK_SHOT, bs->entitynum);
if (enemyvisible > 0.0f && (trace.fraction >= 1.0f || trace.c.entityNum == bs->enemy))
{
bs->aimtarget = bestorigin;
}
else
{
bs->aimtarget = trace.endpos;
}
idVec3 dir = bestorigin - bs->eye;
if (dir.LengthSqr() < Square(1.0f))
{
return;
}
if (selectedMachinegun || selectedShotgun)
{
float dist = dir.Length();
if (dist > 150.0f)
{
dist = 150.0f;
}
aim_accuracy *= 0.6f + dist / 150.0f * 0.4f;
}
if (aim_accuracy < 0.8f)
{
dir.Normalize();
const float miss = 1.0f - aim_accuracy;
for (int i = 0; i < 3; i++)
{
dir[i] += 0.12f * rvmBotUtil::crandom() * miss;
}
}
bs->viewangles = dir.ToAngles();
bs->viewangles[PITCH] += 2.0f * wi.vspread * rvmBotUtil::crandom() * (1.0f - aim_accuracy);
bs->viewangles[PITCH] = idMath::AngleMod(bs->viewangles[PITCH]);
bs->viewangles[YAW] += 2.0f * wi.hspread * rvmBotUtil::crandom() * (1.0f - aim_accuracy);
bs->viewangles[YAW] = idMath::AngleMod(bs->viewangles[YAW]);
bs->botinput.viewangles = bs->viewangles;
}
/*
==================
rvmBot::BotWantsToChase
==================
*/
bool rvmBot::BotWantsToChase(bot_state_t* bs)
{
if (BotAggression(bs) > 50)
{
return true;
}
return false;
}
/*
==================
rvmBot::BotNearbyGoal
==================
*/
int rvmBot::BotNearbyGoal(bot_state_t* bs, int tfl, bot_goal_t* ltg, float range)
{
if (bs == NULL)
{
return false;
}
// In battle the old code only considered tiny detours, so a bot with only a
// pistol or no ammo would keep strafing at the enemy instead of routing through
// a nearby weapon/ammo pickup. Expand the NBG budget only when combat inventory
// makes the pickup tactically useful; otherwise keep it as a short opportunistic
// detour.
range = BotAITacticalPickupRange(bs, range);
return botGoalManager.BotChooseNBGItem(bs->gs, bs->origin, bs->inventory, tfl, ltg, range);
}
/*
=======================
rvmBot::BotGetRandomPointNearPosition
=======================
*/
void rvmBot::BotGetRandomPointNearPosition(const idVec3 point, idVec3& randomPoint, float radius) {
randomPoint = point;
idNAV* aas = this->aas;
if (!aas) {
return;
}
idVec3 basePoint = point;
const int baseAreaNum = aas->AdjustPositionAndGetArea(basePoint);
if (baseAreaNum <= 0) {
return;
}
if (radius <= 0.0f) {
randomPoint = aas->AreaCenter(baseAreaNum);
aas->PushPointIntoAreaNum(baseAreaNum, randomPoint);
return;
}
for (int i = 0; i < 24; i++) {
const float angle = gameLocal.random.RandomFloat() * idMath::TWO_PI;
const float dist = gameLocal.random.RandomFloat() * radius;
idVec3 testPoint = point;
testPoint.x += idMath::Cos(angle) * dist;
testPoint.y += idMath::Sin(angle) * dist;
const int testAreaNum = aas->AdjustPositionAndGetArea(testPoint);
if (testAreaNum <= 0) {
continue;
}
aasPath_t path;
if (aas->WalkPathToGoal(path, baseAreaNum, basePoint, testAreaNum, testPoint, TFL_WALK | TFL_AIR))
{
aas->PushPointIntoAreaNum(testAreaNum, testPoint);
randomPoint = testPoint;
return;
}
}
randomPoint = aas->AreaCenter(baseAreaNum);
aas->PushPointIntoAreaNum(baseAreaNum, randomPoint);
}
/*
=======================
rvmBot::BotMoveInRandomDirection
=======================
*/
int rvmBot::BotMoveInRandomDirection(bot_state_t* bs)
{
if (bs == NULL || !BotAIValidEntityNum(bs->client))
{
return 0;
}
rvmBot* ent = gameLocal.entities[bs->client]->Cast<rvmBot>();
if (ent == NULL)
{
return 0;
}
float dist = idMath::Distance(ent->GetPhysics()->GetOrigin(), bs->random_move_position);
if (bs->random_move_position.LengthSqr() < Square(1.0f) || !ent->PointVisible(bs->random_move_position))
{
dist = 0.0f;
}
if (dist < 25.0f)
{
BotGetRandomPointNearPosition(ent->GetPhysics()->GetOrigin(), bs->random_move_position, 80.0f);
}
idVec3 dir = bs->random_move_position - ent->GetPhysics()->GetOrigin();
dir[2] = 0.0f;
if (dir.LengthSqr() > Square(1.0f))
{
dir.Normalize();
}
else
{
dir = idVec3(0.0f, 0.0f, 0.0f);
}
bs->botinput.dir = dir;
bs->botinput.speed = 400.0f;
bs->useRandomPosition = true;
return 0;
}
/*
============
rvmBot::ShowHideArea
============
*/
void rvmBot::MoveToCoverPoint(void)
{
int areaNum, numObstacles;
idVec3 target;
aasGoal_t goal;
aasObstacle_t obstacles[10];
idVec3 origin = GetOrigin();
idNAV* aas = gameLocal.GetBotNAV();
if (aas == NULL)
{
return;
}
areaNum = aas->PointReachableAreaNum(origin, aas->DefaultSearchBounds(), (AREA_REACHABLE_WALK | AREA_REACHABLE_FLY));
if (areaNum <= 0)
{
return;
}
idPlayer* localPlayer = gameLocal.GetLocalPlayer();
if (localPlayer == NULL)
{
return;
}
const int targetAreaNum = aas->PointAreaNum(localPlayer->GetOrigin());
if (targetAreaNum <= 0)
{
return;
}
target = aas->AreaCenter(targetAreaNum);
// consider the target an obstacle
obstacles[0].absBounds = idBounds(target).Expand(16);
numObstacles = 1;
idNAVCallback_FindCoverArea findCover(target);
if (aas->FindNearestGoal(goal, areaNum, origin, target, TFL_WALK | TFL_AIR, obstacles, numObstacles, findCover))
{
bs.currentGoal.origin = goal.origin;
}
}
/*
==================
rvmBot::BotCheckAttack
==================
*/
void rvmBot::BotCheckAttack(bot_state_t* bs)
{
if (bs == NULL || bs->enemy < 0 || !BotAIValidEntityNum(bs->enemy) || !BotAIValidEntityNum(bs->client))
{
return;
}
const int attackentity = bs->enemy;
idEntity* entinfo = gameLocal.entities[attackentity];
idPlayer* self = gameLocal.entities[bs->client]->Cast<idPlayer>();
if (entinfo == NULL || entinfo->GetPhysics() == NULL || self == NULL)
{
return;
}
// Make sure battle states that enter through "attacked" or chase still have
// a real weapon selected before the fire decision is made.
if (bs->weaponnum < 0)
{
BotChooseWeapon(bs);
}
const float reactiontime = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_REACTIONTIME, 0, 1);
if (bs->enemysight_time > Bot_Time() - reactiontime || bs->teleport_time > Bot_Time() - reactiontime)
{
return;
}
if (bs->weaponchange_time > Bot_Time() - 0.1f)
{
return;
}
if (bs->firethrottlewait_time > Bot_Time())
{
return;
}
float firethrottle = botCharacterStatsManager.Characteristic_BFloat(bs->character, CHARACTERISTIC_FIRETHROTTLE, 0, 1);
firethrottle = BotAIClampFloat(0.0f, 1.0f, firethrottle);
if (bs->firethrottleshoot_time < Bot_Time())
{
if (rvmBotUtil::random() > firethrottle)
{
bs->firethrottlewait_time = Bot_Time() + firethrottle;
bs->firethrottleshoot_time = 0.0f;
}
else
{
bs->firethrottleshoot_time = Bot_Time() + 1.0f - firethrottle;
bs->firethrottlewait_time = 0.0f;
}
}
weaponinfo_t wi;
botWeaponInfoManager.BotGetWeaponInfo(bs->ws, bs->weaponnum, &wi);
const bool selectedMachinegun = bs->weaponnum == WP_MACHINEGUN;
const bool selectedShotgun = bs->weaponnum == WP_SHOTGUN;
const bool selectedPlasmaGun = bs->weaponnum == WP_PLASMAGUN;
const bool selectedRocketLauncher = bs->weaponnum == WP_ROCKET_LAUNCHER;
const int selectedWeaponInventory = selectedMachinegun ? INVENTORY_MACHINEGUN :
selectedShotgun ? INVENTORY_SHOTGUN :
selectedPlasmaGun ? INVENTORY_PLASMAGUN :
selectedRocketLauncher ? INVENTORY_ROCKETLAUNCHER : -1;
if (selectedWeaponInventory >= 0 && wi.weaponindex != selectedWeaponInventory)
{
for (int weaponInfoNum = 0; weaponInfoNum < BOT_MAX_WEAPONS; weaponInfoNum++)
{
weaponinfo_t candidateWi;
botWeaponInfoManager.BotGetWeaponInfo(bs->ws, weaponInfoNum, &candidateWi);
if (candidateWi.number == weaponInfoNum && candidateWi.weaponindex == selectedWeaponInventory)
{
wi = candidateWi;
break;
}
}
}
// Hard visibility gate: movement/chase states may aim at last known position,
// but they should not press attack unless the enemy is currently visible.
if (BotEntityVisibleTest(bs->entitynum, bs->eye, bs->viewangles, 360.0f, attackentity, false) <= 0.0f)
{
bs->flags &= ~BFL_ATTACKED;
return;
}
idVec3 dir = bs->aimtarget - bs->eye;
if (dir.LengthSqr() < Square(1.0f))
{
bs->aimtarget = BotAIEntityCenter(entinfo);
dir = bs->aimtarget - bs->eye;
if (dir.LengthSqr() < Square(1.0f))
{
return;
}
}
// Keep the safety checks from the firing-regression fix, but do not fire until
// the real player view is close enough to the computed aim point. Previously
// BotCheckAttack used only bs->viewangles, while BotInputToUserCommand still
// smoothed the actual usercmd view, so the bot often shot several frames early.
const float distToAim = dir.Length();
const float idealAttackFov = distToAim < 100.0f ? 140.0f : 90.0f;
if (!BotAIPointWithinFOV(bs->eye, bs->viewangles, bs->aimtarget, idealAttackFov))
{
return;
}
float actualAttackFov = 14.0f;
if (distToAim < 120.0f)
{
actualAttackFov = 45.0f;
}
else if (selectedShotgun)
{
actualAttackFov = 32.0f;
}
else if (wi.proj.damagetype & DAMAGETYPE_RADIAL)
{
actualAttackFov = 24.0f;
}
else if (wi.speed > 1.0f)
{
actualAttackFov = 18.0f;
}
if (!BotAIPointWithinFOV(bs->eye, viewAngles, bs->aimtarget, actualAttackFov))
{
return;
}
trace_t sightTrace;
gameLocal.Trace(sightTrace, bs->eye, bs->aimtarget, MASK_SHOT, bs->entitynum);
const bool eyeTraceClear = sightTrace.fraction >= 1.0f || sightTrace.c.entityNum == attackentity;
if (!eyeTraceClear && !(wi.proj.damagetype & DAMAGETYPE_RADIAL))
{
return;
}
idVec3 forward, right, start, end;
bs->viewangles.ToVectors(&forward, &right, NULL);
start = bs->eye;
start[0] += forward[0] * wi.offset[0] + right[0] * wi.offset[1];
start[1] += forward[1] * wi.offset[0] + right[1] * wi.offset[1];
start[2] += forward[2] * wi.offset[0] + right[2] * wi.offset[1] + wi.offset[2];
end = start + forward * 1000.0f;
start = start - forward * 12.0f;
trace_t weaponTrace;
gameLocal.Trace(weaponTrace, start, end, MASK_SHOT, bs->entitynum);
// Do not shoot through another client. Team checks can be added here later;
// for now this prevents the regression fix from blindly firing through bodies.
if (weaponTrace.c.entityNum >= 0 && weaponTrace.c.entityNum < MAX_CLIENTS && weaponTrace.c.entityNum != attackentity)
{
return;
}
if (weaponTrace.c.entityNum != attackentity)
{
if (wi.proj.damagetype & DAMAGETYPE_RADIAL)
{
// Splash shots are allowed, but only when the visible impact/aim point is
// close enough to the currently visible enemy to plausibly damage them.
if (weaponTrace.fraction < 1.0f && weaponTrace.fraction * 1000.0f < wi.proj.radius + 24.0f)
{
return;
}
const float splashRadius = wi.proj.radius > 1.0f ? wi.proj.radius : 96.0f;
const idVec3 splashPoint = weaponTrace.fraction < 1.0f ? weaponTrace.endpos : bs->aimtarget;
const idVec3 enemyCenter = BotAIEntityCenter(entinfo);
if ((splashPoint - enemyCenter).LengthSqr() > Square(splashRadius + 96.0f))
{
return;
}
}
else if (!eyeTraceClear)
{
return;
}
}
if (wi.flags & WFL_FIRERELEASED)
{
if (bs->flags & BFL_ATTACKED)
{
bs->botinput.actionflags |= ACTION_ATTACK;
bs->flags &= ~BFL_ATTACKED;
}
else
{
bs->flags |= BFL_ATTACKED;
}
}
else
{
bs->botinput.actionflags |= ACTION_ATTACK;
bs->flags |= BFL_ATTACKED;
}
}