mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-12 16:21:04 +02:00
1450 lines
38 KiB
C++
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;
|
|
}
|
|
}
|