openmohaa/code/fgame/playerbot.cpp

1231 lines
31 KiB
C++

/*
===========================================================================
Copyright (C) 2024 the OpenMoHAA team
This file is part of OpenMoHAA source code.
OpenMoHAA source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
OpenMoHAA source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
===========================================================================
*/
// playerbot.cpp: Multiplayer bot system.
#include "g_local.h"
#include "actor.h"
#include "playerbot.h"
#include "consoleevent.h"
#include "debuglines.h"
#include "scriptexception.h"
#include "vehicleturret.h"
#include "weaputils.h"
#include "g_bot.h"
// We assume that we have limited access to the server-side
// and that most logic come from the playerstate_s structure
cvar_t *bot_manualmove;
CLASS_DECLARATION(Listener, BotController, NULL) {
{NULL, NULL}
};
BotController::botfunc_t BotController::botfuncs[MAX_BOT_FUNCTIONS];
BotController::BotController()
{
if (LoadingSavegame) {
return;
}
m_botCmd.serverTime = 0;
m_botCmd.msec = 0;
m_botCmd.buttons = 0;
m_botCmd.angles[0] = ANGLE2SHORT(0);
m_botCmd.angles[1] = ANGLE2SHORT(0);
m_botCmd.angles[2] = ANGLE2SHORT(0);
m_botCmd.forwardmove = 0;
m_botCmd.rightmove = 0;
m_botCmd.upmove = 0;
m_botEyes.angles[0] = 0;
m_botEyes.angles[1] = 0;
m_botEyes.ofs[0] = 0;
m_botEyes.ofs[1] = 0;
m_botEyes.ofs[2] = DEFAULT_VIEWHEIGHT;
m_iCuriousTime = 0;
m_iAttackTime = 0;
m_iConfirmTime = 0;
m_iEnemyEyesTag = -1;
m_iNextTauntTime = 0;
m_StateFlags = 0;
m_RunLabel.TrySetScript("global/bot_run.scr");
}
BotController::~BotController()
{
if (controlledEnt) {
controlledEnt->delegate_gotKill.Remove(delegateHandle_gotKill);
controlledEnt->delegate_killed.Remove(delegateHandle_killed);
controlledEnt->delegate_stufftext.Remove(delegateHandle_stufftext);
}
}
BotMovement& BotController::GetMovement()
{
return movement;
}
void BotController::Init(void)
{
bot_manualmove = gi.Cvar_Get("bot_manualmove", "0", 0);
for (int i = 0; i < MAX_BOT_FUNCTIONS; i++) {
botfuncs[i].BeginState = &BotController::State_DefaultBegin;
botfuncs[i].EndState = &BotController::State_DefaultEnd;
}
InitState_Attack(&botfuncs[0]);
InitState_Curious(&botfuncs[1]);
InitState_Grenade(&botfuncs[2]);
InitState_Idle(&botfuncs[3]);
//InitState_Weapon(&botfuncs[4]);
}
void BotController::GetUsercmd(usercmd_t *ucmd)
{
*ucmd = m_botCmd;
}
void BotController::GetEyeInfo(usereyes_t *eyeinfo)
{
*eyeinfo = m_botEyes;
}
void BotController::UpdateBotStates(void)
{
if (bot_manualmove->integer) {
memset(&m_botCmd, 0, sizeof(usercmd_t));
return;
}
if (!controlledEnt->client->pers.dm_primary[0]) {
Event *event;
//
// Primary weapon
//
event = new Event(EV_Player_PrimaryDMWeapon);
event->AddString("auto");
controlledEnt->ProcessEvent(event);
}
if (controlledEnt->GetTeam() == TEAM_NONE || controlledEnt->GetTeam() == TEAM_SPECTATOR) {
float time;
// Add some delay to avoid telefragging
time = controlledEnt->entnum / 20.0;
if (controlledEnt->EventPending(EV_Player_AutoJoinDMTeam)) {
return;
}
//
// Team
//
controlledEnt->PostEvent(EV_Player_AutoJoinDMTeam, time);
return;
}
if (controlledEnt->IsDead() || controlledEnt->IsSpectator()) {
// The bot should respawn
m_botCmd.buttons ^= BUTTON_ATTACKLEFT;
return;
}
m_botCmd.buttons |= BUTTON_RUN;
m_botCmd.serverTime = level.svsTime;
m_botEyes.ofs[0] = 0;
m_botEyes.ofs[1] = 0;
m_botEyes.ofs[2] = controlledEnt->viewheight;
m_botEyes.angles[0] = 0;
m_botEyes.angles[1] = 0;
CheckStates();
movement.MoveThink(m_botCmd);
rotation.TurnThink(m_botCmd, m_botEyes);
CheckUse();
CheckValidWeapon();
}
void BotController::CheckUse(void)
{
Vector dir;
Vector start;
Vector end;
trace_t trace;
controlledEnt->angles.AngleVectorsLeft(&dir);
start = controlledEnt->origin + Vector(0, 0, controlledEnt->viewheight);
end = controlledEnt->origin + Vector(0, 0, controlledEnt->viewheight) + dir * 32;
trace = G_Trace(start, vec_zero, vec_zero, end, controlledEnt, MASK_USABLE, false, "BotController::CheckUse");
// It may be a door
if ((trace.allsolid || trace.startsolid || trace.fraction != 1.0f) && trace.ent) {
if (trace.ent->entity->IsSubclassOfDoor()) {
Door *door = static_cast<Door *>(trace.ent->entity);
if (door->isOpen()) {
m_botCmd.buttons &= ~BUTTON_USE;
return;
}
}
//
// Toggle the use button
//
m_botCmd.buttons ^= BUTTON_USE;
} else {
m_botCmd.buttons &= ~BUTTON_USE;
}
}
void BotController::CheckValidWeapon() {
Weapon* weapon = controlledEnt->GetActiveWeapon(WEAPON_MAIN);
if (!weapon) {
// If holstered, use the best weapon available
UseWeaponWithAmmo();
} else if (!weapon->HasAmmo(FIRE_PRIMARY) && !controlledEnt->GetNewActiveWeapon()) {
// In case the current weapon has no ammo, use the best available weapon
UseWeaponWithAmmo();
}
}
void BotController::SendCommand(const char *text)
{
char *buffer;
char *data;
size_t len;
ConsoleEvent ev;
len = strlen(text) + 1;
buffer = (char *)gi.Malloc(len);
data = buffer;
Q_strncpyz(data, text, len);
const char *com_token = COM_Parse(&data);
if (!com_token) {
return;
}
controlledEnt->m_lastcommand = com_token;
if (!Event::GetEvent(com_token)) {
return;
}
ev = ConsoleEvent(com_token);
if (!(ev.GetEventFlags(ev.eventnum) & EV_CONSOLE)) {
gi.Free(buffer);
return;
}
ev.SetConsoleEdict(controlledEnt->edict);
while (1) {
com_token = COM_Parse(&data);
if (!com_token || !*com_token) {
break;
}
ev.AddString(com_token);
}
gi.Free(buffer);
try {
controlledEnt->ProcessEvent(ev);
} catch (ScriptException& exc) {
gi.DPrintf("*** Bot Command Exception *** %s\n", exc.string.c_str());
}
}
/*
====================
AimAtAimNode
Make the bot face toward the current path
====================
*/
void BotController::AimAtAimNode(void)
{
Vector goal;
if (!movement.IsMoving()) {
return;
}
goal = movement.GetCurrentGoal();
if (goal != controlledEnt->origin) {
rotation.AimAt(goal);
}
Vector targetAngles = rotation.GetTargetAngles();
targetAngles.x = 0;
rotation.SetTargetAngles(targetAngles);
}
/*
====================
CheckReload
Make the bot reload if necessary
====================
*/
void BotController::CheckReload(void)
{
Weapon *weap = controlledEnt->GetActiveWeapon(WEAPON_MAIN);
if (weap && weap->CheckReload(FIRE_PRIMARY)) {
SendCommand("reload");
}
}
/*
====================
NoticeEvent
Warn the bot of an event
====================
*/
void BotController::NoticeEvent(Vector vPos, int iType, Entity *pEnt, float fDistanceSquared, float fRadiusSquared)
{
Sentient *pSentOwner;
float fRangeFactor;
fRangeFactor = 1.0 - (fDistanceSquared / fRadiusSquared);
if (fRangeFactor < random()) {
return;
}
if (pEnt->IsSubclassOfSentient()) {
pSentOwner = static_cast<Sentient *>(pEnt);
} else if (pEnt->IsSubclassOfVehicleTurretGun()) {
VehicleTurretGun *pVTG = static_cast<VehicleTurretGun *>(pEnt);
pSentOwner = pVTG->GetSentientOwner();
} else if (pEnt->IsSubclassOfItem()) {
Item *pItem = static_cast<Item *>(pEnt);
pSentOwner = pItem->GetOwner();
} else if (pEnt->IsSubclassOfProjectile()) {
Projectile *pProj = static_cast<Projectile *>(pEnt);
pSentOwner = pProj->GetOwner();
} else {
pSentOwner = NULL;
}
if (pSentOwner) {
if (pSentOwner == controlledEnt) {
// Ignore self
return;
}
if ((pSentOwner->flags & FL_NOTARGET) || pSentOwner->getSolidType() == SOLID_NOT) {
return;
}
// Ignore teammates
if (pSentOwner->IsSubclassOfPlayer()) {
Player *p = static_cast<Player *>(pSentOwner);
if (g_gametype->integer >= GT_TEAM && p->GetTeam() == controlledEnt->GetTeam()) {
return;
}
}
}
switch (iType) {
case AI_EVENT_MISC:
case AI_EVENT_MISC_LOUD:
break;
case AI_EVENT_WEAPON_FIRE:
case AI_EVENT_WEAPON_IMPACT:
case AI_EVENT_EXPLOSION:
case AI_EVENT_AMERICAN_VOICE:
case AI_EVENT_GERMAN_VOICE:
case AI_EVENT_AMERICAN_URGENT:
case AI_EVENT_GERMAN_URGENT:
case AI_EVENT_FOOTSTEP:
case AI_EVENT_GRENADE:
default:
m_iCuriousTime = level.inttime + 20000;
m_vNewCuriousPos = vPos;
break;
}
}
/*
====================
ClearEnemy
Clear the bot's enemy
====================
*/
void BotController::ClearEnemy(void)
{
m_iAttackTime = 0;
m_iConfirmTime = 0;
m_pEnemy = NULL;
m_iEnemyEyesTag = -1;
m_vOldEnemyPos = vec_zero;
m_vLastEnemyPos = vec_zero;
}
/*
====================
Bot states
--------------------
____________________
--------------------
____________________
--------------------
____________________
--------------------
____________________
====================
*/
void BotController::CheckStates(void)
{
m_StateCount = 0;
for (int i = 0; i < MAX_BOT_FUNCTIONS; i++) {
botfunc_t *func = &botfuncs[i];
if (func->CheckCondition) {
if ((this->*func->CheckCondition)()) {
if (!(m_StateFlags & (1 << i))) {
m_StateFlags |= 1 << i;
if (func->BeginState) {
(this->*func->BeginState)();
}
}
if (func->ThinkState) {
m_StateCount++;
(this->*func->ThinkState)();
}
} else {
if ((m_StateFlags & (1 << i))) {
m_StateFlags &= ~(1 << i);
if (func->EndState) {
(this->*func->EndState)();
}
}
}
} else {
if (func->ThinkState) {
m_StateCount++;
(this->*func->ThinkState)();
}
}
}
assert(m_StateCount);
if (!m_StateCount) {
gi.DPrintf("*** WARNING *** %s was stuck with no states !!!", controlledEnt->client->pers.netname);
State_Reset();
}
}
/*
====================
Default state
====================
*/
void BotController::State_DefaultBegin(void)
{
movement.ClearMove();
}
void BotController::State_DefaultEnd(void) {}
void BotController::State_Reset(void)
{
m_iCuriousTime = 0;
m_iAttackTime = 0;
m_vLastCuriousPos = vec_zero;
m_vOldEnemyPos = vec_zero;
m_vLastEnemyPos = vec_zero;
m_vLastDeathPos = vec_zero;
m_pEnemy = NULL;
m_iEnemyEyesTag = -1;
}
/*
====================
Idle state
Make the bot move to random directions
====================
*/
void BotController::InitState_Idle(botfunc_t *func)
{
func->CheckCondition = &BotController::CheckCondition_Idle;
func->ThinkState = &BotController::State_Idle;
}
bool BotController::CheckCondition_Idle(void)
{
if (m_iCuriousTime) {
return false;
}
if (m_iAttackTime) {
return false;
}
return true;
}
void BotController::State_Idle(void)
{
AimAtAimNode();
CheckReload();
if (!movement.MoveToBestAttractivePoint() && !movement.IsMoving()) {
if (m_vLastDeathPos != vec_zero) {
movement.MoveTo(m_vLastDeathPos);
if (movement.MoveDone()) {
m_vLastDeathPos = vec_zero;
}
} else {
Vector randomDir(G_CRandom(16), G_CRandom(16), G_CRandom(16));
Vector preferredDir = Vector(controlledEnt->orientation[0]) * (rand() % 5 ? 1024 : -1024);
float radius = 512 + G_Random(2048);
movement.AvoidPath(controlledEnt->origin + randomDir, radius, preferredDir);
}
}
}
/*
====================
Curious state
Forward to the last event position
====================
*/
void BotController::InitState_Curious(botfunc_t *func)
{
func->CheckCondition = &BotController::CheckCondition_Curious;
func->ThinkState = &BotController::State_Curious;
}
bool BotController::CheckCondition_Curious(void)
{
if (m_iAttackTime) {
m_iCuriousTime = 0;
return false;
}
if (level.inttime > m_iCuriousTime) {
if (m_iCuriousTime) {
movement.ClearMove();
m_iCuriousTime = 0;
}
return false;
}
return true;
}
void BotController::State_Curious(void)
{
AimAtAimNode();
if (!movement.MoveToBestAttractivePoint(3) && (!movement.IsMoving() || m_vLastCuriousPos != m_vNewCuriousPos)) {
movement.MoveTo(m_vNewCuriousPos);
m_vLastCuriousPos = m_vNewCuriousPos;
}
if (movement.MoveDone()) {
m_iCuriousTime = 0;
}
}
/*
====================
Attack state
Attack the enemy
====================
*/
void BotController::InitState_Attack(botfunc_t *func)
{
func->CheckCondition = &BotController::CheckCondition_Attack;
func->EndState = &BotController::State_EndAttack;
func->ThinkState = &BotController::State_Attack;
}
static Vector bot_origin;
static int sentients_compare(const void *elem1, const void *elem2)
{
Entity *e1, *e2;
float delta[3];
float d1, d2;
e1 = *(Entity **)elem1;
e2 = *(Entity **)elem2;
VectorSubtract(bot_origin, e1->origin, delta);
d1 = VectorLengthSquared(delta);
VectorSubtract(bot_origin, e2->origin, delta);
d2 = VectorLengthSquared(delta);
if (d2 <= d1) {
return d1 > d2;
} else {
return -1;
}
}
bool BotController::IsValidEnemy(Sentient *sent) const
{
if (sent == controlledEnt) {
return false;
}
if (sent->hidden() || (sent->flags & FL_NOTARGET)) {
// Ignore hidden / non-target enemies
return false;
}
if (sent->IsDead()) {
// Ignore dead enemies
return false;
}
if (sent->getSolidType() == SOLID_NOT) {
// Ignore non-solid, like spectators
return false;
}
if (sent->IsSubclassOfPlayer()) {
Player *player = static_cast<Player *>(sent);
if (g_gametype->integer >= GT_TEAM && player->GetTeam() == controlledEnt->GetTeam()) {
return false;
}
} else {
if (sent->m_Team == controlledEnt->m_Team) {
return false;
}
}
return true;
}
bool BotController::CheckCondition_Attack(void)
{
Container<Sentient *> sents = SentientList;
float maxDistance = 0;
bot_origin = controlledEnt->origin;
sents.Sort(sentients_compare);
for (int i = 1; i <= sents.NumObjects(); i++) {
Sentient *sent = sents.ObjectAt(i);
if (!IsValidEnemy(sent)) {
continue;
}
maxDistance = Q_min(world->m_fAIVisionDistance, world->farplane_distance * 0.828);
if (controlledEnt->CanSee(sent, 80, maxDistance, false)) {
if (m_pEnemy != sent) {
m_iEnemyEyesTag = -1;
}
if (!m_pEnemy) {
// Slight reaction time
m_iConfirmTime = level.inttime + (200 + G_Random(200));
m_iAttackTime = 0;
}
m_pEnemy = sent;
m_vLastEnemyPos = m_pEnemy->origin;
if (level.inttime < m_iConfirmTime) {
return false;
}
}
if (m_pEnemy && level.inttime >= m_iConfirmTime) {
m_iAttackTime = level.inttime + 1000;
return true;
}
}
if (level.inttime > m_iAttackTime) {
if (m_iAttackTime) {
movement.ClearMove();
m_iAttackTime = 0;
}
return false;
}
return true;
}
void BotController::State_EndAttack(void)
{
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
controlledEnt->ZoomOff();
}
void BotController::State_Attack(void)
{
bool bMelee = false;
bool bCanSee = false;
float fMinDistance = 128;
float fMinDistanceSquared = fMinDistance * fMinDistance;
float fEnemyDistanceSquared;
Weapon *pWeap = controlledEnt->GetActiveWeapon(WEAPON_MAIN);
bool bNoMove = false;
if (!m_pEnemy || !IsValidEnemy(m_pEnemy)) {
// Ignore dead enemies
m_iAttackTime = 0;
return;
}
float fDistanceSquared = (m_pEnemy->origin - controlledEnt->origin).lengthSquared();
m_vOldEnemyPos = m_vLastEnemyPos;
bCanSee =
controlledEnt->CanSee(m_pEnemy, 20, Q_min(world->m_fAIVisionDistance, world->farplane_distance * 0.828), false);
if (bCanSee) {
if (!pWeap) {
return;
}
float fPrimaryBulletRange = pWeap->GetBulletRange(FIRE_PRIMARY) / 1.25f;
float fPrimaryBulletRangeSquared = fPrimaryBulletRange * fPrimaryBulletRange;
float fSecondaryBulletRange = pWeap->GetBulletRange(FIRE_SECONDARY);
float fSecondaryBulletRangeSquared = fSecondaryBulletRange * fSecondaryBulletRange;
float fSpreadFactor = pWeap->GetSpreadFactor(FIRE_PRIMARY);
//
// check the fire movement speed if the weapon has a max fire movement
//
if (pWeap->GetMaxFireMovement() < 1 && pWeap->HasAmmoInClip(FIRE_PRIMARY)) {
float length;
length = controlledEnt->velocity.length();
if ((length / sv_runspeed->value) > (pWeap->GetMaxFireMovementMult())) {
bNoMove = true;
movement.ClearMove();
}
}
fMinDistance = fPrimaryBulletRange;
if (fMinDistance > 256) {
fMinDistance = 256;
}
fMinDistanceSquared = fMinDistance * fMinDistance;
if (controlledEnt->client->ps.stats[STAT_AMMO] > 0 || controlledEnt->client->ps.stats[STAT_CLIPAMMO] > 0) {
if (fDistanceSquared <= fPrimaryBulletRangeSquared) {
if (pWeap->IsSemiAuto()) {
if (controlledEnt->client->ps.iViewModelAnim == VM_ANIM_IDLE
|| controlledEnt->client->ps.iViewModelAnim >= VM_ANIM_IDLE_0
&& controlledEnt->client->ps.iViewModelAnim <= VM_ANIM_IDLE_2) {
if (fSpreadFactor < 0.25) {
m_botCmd.buttons ^= BUTTON_ATTACKLEFT;
if (pWeap->GetZoom()) {
if (!controlledEnt->IsZoomed()) {
m_botCmd.buttons |= BUTTON_ATTACKRIGHT;
} else {
m_botCmd.buttons &= ~BUTTON_ATTACKRIGHT;
}
}
} else {
bNoMove = true;
movement.ClearMove();
}
} else {
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
controlledEnt->ZoomOff();
}
} else {
m_botCmd.buttons |= BUTTON_ATTACKLEFT;
}
} else {
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
controlledEnt->ZoomOff();
}
} else {
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
controlledEnt->ZoomOff();
}
if (pWeap->GetFireType(FIRE_SECONDARY) == FT_MELEE) {
if (controlledEnt->client->ps.stats[STAT_AMMO] <= 0 && controlledEnt->client->ps.stats[STAT_CLIPAMMO] <= 0) {
bMelee = true;
} else if (fDistanceSquared <= fSecondaryBulletRangeSquared) {
bMelee = true;
}
}
if (bMelee) {
m_botCmd.buttons &= ~BUTTON_ATTACKLEFT;
if (fDistanceSquared <= fSecondaryBulletRangeSquared) {
m_botCmd.buttons ^= BUTTON_ATTACKRIGHT;
} else {
m_botCmd.buttons &= ~BUTTON_ATTACKRIGHT;
}
}
m_iAttackTime = level.inttime + 1000;
m_iAttackStopAimTime = level.inttime + 3000;
m_vLastEnemyPos = m_pEnemy->centroid;
} else {
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
fMinDistanceSquared = 0;
}
if (bCanSee || level.inttime < m_iAttackStopAimTime) {
Vector vRandomOffset;
Vector vTarget;
orientation_t eyes_or;
if (m_iEnemyEyesTag == -1) {
// Cache the tag
m_iEnemyEyesTag = gi.Tag_NumForName(m_pEnemy->edict->tiki, "eyes bone");
}
if (m_iEnemyEyesTag != -1) {
// Use the enemy's eyes bone
m_pEnemy->GetTag(m_iEnemyEyesTag, &eyes_or);
vRandomOffset = Vector(G_CRandom(8), G_CRandom(8), -G_Random(32));
vTarget = eyes_or.origin + vRandomOffset;
rotation.AimAt(eyes_or.origin + vRandomOffset);
} else {
vRandomOffset = Vector(G_CRandom(8), G_CRandom(8), 16 + G_Random(m_pEnemy->viewheight - 16));
vTarget = m_pEnemy->origin + vRandomOffset;
}
rotation.AimAt(vTarget);
} else {
AimAtAimNode();
}
if (bNoMove) {
return;
}
fEnemyDistanceSquared = (controlledEnt->origin - m_vLastEnemyPos).lengthSquared();
if ((!movement.MoveToBestAttractivePoint(5) && !movement.IsMoving())
|| (m_vOldEnemyPos != m_vLastEnemyPos && !movement.MoveDone()) || fEnemyDistanceSquared < fMinDistanceSquared) {
if (!bMelee || !bCanSee) {
if (fEnemyDistanceSquared < fMinDistanceSquared) {
Vector vDir = controlledEnt->origin - m_vLastEnemyPos;
VectorNormalizeFast(vDir);
movement.AvoidPath(m_vLastEnemyPos, fMinDistance, Vector(controlledEnt->orientation[1]) * 512);
} else {
movement.MoveTo(m_vLastEnemyPos);
}
if (!bCanSee && movement.MoveDone()) {
// Lost track of the enemy
ClearEnemy();
return;
}
} else {
movement.MoveTo(m_vLastEnemyPos);
}
}
if (movement.IsMoving()) {
m_iAttackTime = level.inttime + 1000;
}
}
/*
====================
Grenade state
Avoid any grenades
====================
*/
void BotController::InitState_Grenade(botfunc_t *func)
{
func->CheckCondition = &BotController::CheckCondition_Grenade;
func->ThinkState = &BotController::State_Grenade;
}
bool BotController::CheckCondition_Grenade(void)
{
// FIXME: TODO
return false;
}
void BotController::State_Grenade(void)
{
// FIXME: TODO
}
/*
====================
Weapon state
Change weapon when necessary
====================
*/
void BotController::InitState_Weapon(botfunc_t *func)
{
func->CheckCondition = &BotController::CheckCondition_Weapon;
func->BeginState = &BotController::State_BeginWeapon;
}
bool BotController::CheckCondition_Weapon(void)
{
return controlledEnt->GetActiveWeapon(WEAPON_MAIN)
!= controlledEnt->BestWeapon(NULL, false, WEAPON_CLASS_THROWABLE);
}
void BotController::State_BeginWeapon(void)
{
Weapon *weap = controlledEnt->BestWeapon(NULL, false, WEAPON_CLASS_THROWABLE);
if (weap == NULL) {
SendCommand("safeholster 1");
return;
}
SendCommand(va("use \"%s\"", weap->model.c_str()));
}
Weapon* BotController::FindWeaponWithAmmo() {
Weapon *next;
int n;
int j;
int bestrank;
Weapon *bestweapon;
const Container<int>& inventory = controlledEnt->getInventory();
n = inventory.NumObjects();
// Search until we find the best weapon with ammo
bestweapon = NULL;
bestrank = -999999;
for (j = 1; j <= n; j++) {
next = (Weapon *)G_GetEntity(inventory.ObjectAt(j));
assert(next);
if (!next->IsSubclassOfWeapon() || next->IsSubclassOfInventoryItem()) {
continue;
}
if (next->GetWeaponClass() & WEAPON_CLASS_THROWABLE) {
continue;
}
if (next->GetRank() < bestrank) {
continue;
}
if (!next->HasAmmo(FIRE_PRIMARY)) {
continue;
}
bestweapon = (Weapon*)next;
bestrank = bestweapon->GetRank();
}
return bestweapon;
}
Weapon* BotController::FindMeleeWeapon() {
Weapon *next;
int n;
int j;
int bestrank;
Weapon *bestweapon;
const Container<int>& inventory = controlledEnt->getInventory();
n = inventory.NumObjects();
// Search until we find the best weapon with ammo
bestweapon = NULL;
bestrank = -999999;
for (j = 1; j <= n; j++) {
next = (Weapon *)G_GetEntity(inventory.ObjectAt(j));
assert(next);
if (!next->IsSubclassOfWeapon() || next->IsSubclassOfInventoryItem()) {
continue;
}
if (next->GetRank() < bestrank) {
continue;
}
if (next->GetFireType(FIRE_SECONDARY) != FT_MELEE) {
continue;
}
bestweapon = (Weapon*)next;
bestrank = bestweapon->GetRank();
}
return bestweapon;
}
void BotController::UseWeaponWithAmmo() {
Weapon* bestWeapon = FindWeaponWithAmmo();
if (!bestWeapon) {
//
// If there is no weapon with ammo, fallback to a weapon that can melee
//
bestWeapon = FindMeleeWeapon();
}
if (!bestWeapon || bestWeapon == controlledEnt->GetActiveWeapon(WEAPON_MAIN)) {
return;
}
controlledEnt->useWeapon(bestWeapon, WEAPON_MAIN);
}
void BotController::Spawned(void)
{
ClearEnemy();
m_iCuriousTime = 0;
m_botCmd.buttons = 0;
}
void BotController::Think()
{
usercmd_t ucmd;
usereyes_t eyeinfo;
UpdateBotStates();
GetUsercmd(&ucmd);
GetEyeInfo(&eyeinfo);
G_ClientThink(controlledEnt->edict, &ucmd, &eyeinfo);
}
void BotController::Killed(const Event& ev)
{
Entity *attacker;
// send the respawn buttons
if (!(m_botCmd.buttons & BUTTON_ATTACKLEFT)) {
m_botCmd.buttons |= BUTTON_ATTACKLEFT;
} else {
m_botCmd.buttons &= ~BUTTON_ATTACKLEFT;
}
m_botEyes.ofs[0] = 0;
m_botEyes.ofs[1] = 0;
m_botEyes.ofs[2] = 0;
m_botEyes.angles[0] = 0;
m_botEyes.angles[1] = 0;
attacker = ev.GetEntity(1);
if (attacker && rand() % 5 == 0) {
// 1/5 chance to go back to the attacker position
m_vLastDeathPos = attacker->origin;
} else {
m_vLastDeathPos = vec_zero;
}
// Choose a new random primary weapon
Event event(EV_Player_PrimaryDMWeapon);
event.AddString("auto");
controlledEnt->ProcessEvent(event);
//
// This is useful to change nationality in Spearhead and Breakthrough
// this allows the AI to use more weapons
//
Info_SetValueForKey(controlledEnt->client->pers.userinfo, "dm_playermodel", G_GetRandomAlliedPlayerModel());
Info_SetValueForKey(controlledEnt->client->pers.userinfo, "dm_playergermanmodel", G_GetRandomGermanPlayerModel());
G_ClientUserinfoChanged(controlledEnt->edict, controlledEnt->client->pers.userinfo);
}
void BotController::GotKill(const Event& ev)
{
ClearEnemy();
m_iCuriousTime = 0;
if (level.inttime >= m_iNextTauntTime && (rand() % 5) == 0) {
//
// Randomly play a taunt
//
Event event("dmmessage");
event.AddInteger(0);
if (g_protocol >= protocol_e::PROTOCOL_MOHTA_MIN) {
event.AddString("*5" + str(1 + (rand() % 8)));
} else {
event.AddString("*4" + str(1 + (rand() % 9)));
}
controlledEnt->ProcessEvent(event);
m_iNextTauntTime = level.inttime + 5000;
}
}
void BotController::EventStuffText(const str& text)
{
SendCommand(text);
}
void BotController::setControlledEntity(Player *player)
{
controlledEnt = player;
movement.SetControlledEntity(player);
rotation.SetControlledEntity(player);
delegateHandle_gotKill = player->delegate_gotKill.Add(std::bind(&BotController::GotKill, this, std::placeholders::_1));
delegateHandle_killed = player->delegate_killed.Add(std::bind(&BotController::Killed, this, std::placeholders::_1));
delegateHandle_stufftext = player->delegate_stufftext.Add(std::bind(&BotController::EventStuffText, this, std::placeholders::_1));
}
Player *BotController::getControlledEntity() const
{
return controlledEnt;
}
BotController *BotControllerManager::createController(Player *player)
{
BotController *controller = new BotController();
controller->setControlledEntity(player);
controllers.AddObject(controller);
return controller;
}
void BotControllerManager::removeController(BotController *controller)
{
controllers.RemoveObject(controller);
delete controller;
}
BotController *BotControllerManager::findController(Entity *ent)
{
int i;
for (i = 1; i <= controllers.NumObjects(); i++) {
BotController *controller = controllers.ObjectAt(i);
if (controller->getControlledEntity() == ent) {
return controller;
}
}
return nullptr;
}
const Container<BotController *>& BotControllerManager::getControllers() const
{
return controllers;
}
BotControllerManager::~BotControllerManager()
{
Cleanup();
}
void BotControllerManager::Init()
{
BotController::Init();
}
void BotControllerManager::Cleanup()
{
int i;
BotController::Init();
for (i = 1; i <= controllers.NumObjects(); i++) {
BotController *controller = controllers.ObjectAt(i);
delete controller;
}
controllers.FreeObjectList();
}
void BotControllerManager::ThinkControllers()
{
int i;
// Delete controllers that don't have associated player entity
// This cannot happen unless some mods remove them
for (i = controllers.NumObjects(); i > 0; i--) {
BotController* controller = controllers.ObjectAt(i);
if (!controller->getControlledEntity()) {
gi.DPrintf("Bot %d has no associated player entity. This shouldn't happen unless the entity has been removed by a script. The controller will be removed, please fix.\n", i);
// Remove the controller, it will be recreated later to match `sv_numbots`
delete controller;
controllers.RemoveObjectAt(i);
}
}
for (i = 1; i <= controllers.NumObjects(); i++) {
BotController *controller = controllers.ObjectAt(i);
controller->Think();
}
}