mirror of
https://github.com/openmoh/openmohaa.git
synced 2025-04-29 06:07:57 +03:00
1310 lines
30 KiB
C++
1310 lines
30 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 2023 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"
|
|
|
|
// 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(Player, PlayerBot, NULL) {
|
|
{&EV_Killed, &PlayerBot::Killed },
|
|
{&EV_GotKill, &PlayerBot::GotKill },
|
|
{&EV_Player_StuffText, &PlayerBot::EventStuffText},
|
|
{NULL, NULL }
|
|
};
|
|
|
|
PlayerBot::botfunc_t PlayerBot::botfuncs[MAX_BOT_FUNCTIONS];
|
|
|
|
PlayerBot::PlayerBot()
|
|
{
|
|
entflags |= ECF_BOT;
|
|
|
|
if (LoadingSavegame) {
|
|
return;
|
|
}
|
|
|
|
m_Path.SetFallHeight(96);
|
|
m_bPathing = false;
|
|
m_bTempAway = false;
|
|
m_bDeltaMove = true;
|
|
|
|
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] = viewheight;
|
|
|
|
m_vAngSpeed = vec_zero;
|
|
m_vTargetAng = vec_zero;
|
|
m_vCurrentAng = vec_zero;
|
|
m_iCheckPathTime = 0;
|
|
m_iTempAwayTime = 0;
|
|
m_fYawSpeedMult = 1.0f;
|
|
|
|
m_iCuriousTime = 0;
|
|
m_iAttackTime = 0;
|
|
|
|
m_StateFlags = 0;
|
|
m_RunLabel.TrySetScript("global/bot_run.scr");
|
|
}
|
|
|
|
void PlayerBot::Init(void)
|
|
{
|
|
bot_manualmove = gi.Cvar_Get("bot_manualmove", "0", 0);
|
|
|
|
for (int i = 0; i < MAX_BOT_FUNCTIONS; i++) {
|
|
botfuncs[i].BeginState = &PlayerBot::State_DefaultBegin;
|
|
botfuncs[i].EndState = &PlayerBot::State_DefaultEnd;
|
|
}
|
|
|
|
InitState_Attack(&botfuncs[0]);
|
|
InitState_Curious(&botfuncs[1]);
|
|
InitState_Grenade(&botfuncs[2]);
|
|
InitState_Idle(&botfuncs[3]);
|
|
InitState_Weapon(&botfuncs[4]);
|
|
}
|
|
|
|
float AngleDifference(float ang1, float ang2)
|
|
{
|
|
float diff;
|
|
|
|
diff = ang1 - ang2;
|
|
if (ang1 > ang2) {
|
|
if (diff > 180.0) {
|
|
diff -= 360.0;
|
|
}
|
|
} else {
|
|
if (diff < -180.0) {
|
|
diff += 360.0;
|
|
}
|
|
}
|
|
return diff;
|
|
}
|
|
|
|
void PlayerBot::TurnThink(void)
|
|
{
|
|
float diff, factor, maxchange, anglespeed, desired_speed;
|
|
int i;
|
|
|
|
if (m_vTargetAng[PITCH] > 180) {
|
|
m_vTargetAng[PITCH] -= 360;
|
|
}
|
|
|
|
factor = 0.25f;
|
|
maxchange = 360;
|
|
|
|
if (maxchange < 240) {
|
|
maxchange = 240;
|
|
}
|
|
|
|
maxchange *= level.frametime;
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
//over reaction view model
|
|
m_vCurrentAng[i] = AngleMod(m_vCurrentAng[i]);
|
|
m_vTargetAng[i] = AngleMod(m_vTargetAng[i]);
|
|
diff = AngleDifference(m_vCurrentAng[i], m_vTargetAng[i]);
|
|
desired_speed = diff * factor;
|
|
m_vAngSpeed[i] += (m_vAngSpeed[i] - desired_speed);
|
|
|
|
if (m_vAngSpeed[i] > 180) {
|
|
m_vAngSpeed[i] = maxchange;
|
|
}
|
|
|
|
if (m_vAngSpeed[i] < -180) {
|
|
m_vAngSpeed[i] = -maxchange;
|
|
}
|
|
|
|
anglespeed = m_vAngSpeed[i];
|
|
|
|
if (anglespeed > maxchange) {
|
|
anglespeed = maxchange;
|
|
}
|
|
|
|
if (anglespeed < -maxchange) {
|
|
anglespeed = -maxchange;
|
|
}
|
|
|
|
m_vCurrentAng[i] += anglespeed;
|
|
m_vCurrentAng[i] = AngleMod(m_vCurrentAng[i]);
|
|
|
|
//demping
|
|
m_vAngSpeed[i] *= 0.45 * (1 - factor);
|
|
}
|
|
|
|
if (m_vCurrentAng[PITCH] > 180) {
|
|
m_vCurrentAng[PITCH] -= 360;
|
|
}
|
|
|
|
m_botEyes.angles[0] = m_vCurrentAng[0];
|
|
m_botEyes.angles[1] = m_vCurrentAng[1];
|
|
m_botCmd.angles[0] = ANGLE2SHORT(m_vCurrentAng[0]) - client->ps.delta_angles[0];
|
|
m_botCmd.angles[1] = ANGLE2SHORT(m_vCurrentAng[1]) - client->ps.delta_angles[1];
|
|
m_botCmd.angles[2] = ANGLE2SHORT(m_vCurrentAng[2]) - client->ps.delta_angles[2];
|
|
}
|
|
|
|
void PlayerBot::CheckAttractiveNodes(void)
|
|
{
|
|
for (int i = m_attractList.NumObjects(); i > 0; i--) {
|
|
nodeAttract_t *a = m_attractList.ObjectAt(i);
|
|
|
|
if (a->m_pNode == NULL || !a->m_pNode->CheckTeam(this) || level.time > a->m_fRespawnTime) {
|
|
delete a;
|
|
m_attractList.RemoveObjectAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PlayerBot::MoveThink(void)
|
|
{
|
|
Vector vDir;
|
|
Vector vAngles;
|
|
Vector vWishDir;
|
|
|
|
m_botCmd.forwardmove = 0;
|
|
m_botCmd.rightmove = 0;
|
|
|
|
CheckAttractiveNodes();
|
|
|
|
if (!IsMoving()) {
|
|
return;
|
|
}
|
|
|
|
if (m_bTempAway && level.inttime >= m_iTempAwayTime) {
|
|
m_bTempAway = false;
|
|
m_Path.FindPath(origin, m_vTargetPos, this, 0, NULL, 0);
|
|
}
|
|
|
|
if (m_Path.CurrentNode()) {
|
|
m_Path.UpdatePos(origin, 8);
|
|
|
|
m_vCurrentGoal = origin;
|
|
VectorAdd2D(m_vCurrentGoal, m_Path.CurrentDelta(), m_vCurrentGoal);
|
|
|
|
if (m_Path.Complete(origin)) {
|
|
// Clear the path
|
|
m_Path.Clear();
|
|
}
|
|
} else if (!m_bTempAway) {
|
|
m_vCurrentGoal = origin;
|
|
}
|
|
|
|
if (ai_debugpath->integer) {
|
|
G_DebugLine(centroid, m_vCurrentGoal + Vector(0, 0, 36), 1, 1, 0, 1);
|
|
}
|
|
|
|
// Check if we're blocked
|
|
if (level.inttime >= m_iCheckPathTime) {
|
|
Vector end;
|
|
|
|
m_bDeltaMove = false;
|
|
|
|
m_iCheckPathTime = level.inttime + 2000;
|
|
|
|
if (m_Path.CurrentNode() && m_Path.CurrentNode() != m_Path.LastNode()) {
|
|
end = m_Path.NextNode()->point;
|
|
} else {
|
|
end = m_vCurrentGoal;
|
|
}
|
|
|
|
if (GetMoveResult() >= MOVERESULT_BLOCKED || velocity.lengthSquared() <= 8 * 8) {
|
|
gi.DPrintf2("Updating path for bot client %i\n", edict - g_entities);
|
|
|
|
m_bTempAway = true;
|
|
m_bDeltaMove = false;
|
|
m_iTempAwayTime = level.inttime + 750;
|
|
|
|
// Try to backward a little
|
|
//vDir = end - origin;
|
|
//VectorNormalizeFast( vDir );
|
|
//m_Path.FindPathAway( origin, origin + vDir * 16, vDir, this, 256, NULL, 0 );
|
|
//m_Path.FindPathAway( origin, origin + m_vLastValidDir * 16, -m_vLastValidDir, this, 256, NULL, 0 );
|
|
m_Path.Clear();
|
|
m_vCurrentGoal = m_vLastValidGoal - m_vLastValidDir * 256;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (ai_debugpath->integer) {
|
|
PathInfo *pos = m_Path.CurrentNode();
|
|
|
|
if (pos != NULL) {
|
|
while (pos != m_Path.LastNode()) {
|
|
Vector vStart = pos->point + Vector(0, 0, 32);
|
|
|
|
pos--;
|
|
|
|
Vector vEnd = pos->point + Vector(0, 0, 32);
|
|
|
|
G_DebugLine(vStart, vEnd, 1, 0, 0, 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((m_vTargetPos - origin).lengthSquared() <= 16 * 16) {
|
|
ClearMove();
|
|
}
|
|
|
|
//vDir = m_vCurrentGoal - centroid;
|
|
|
|
// Rotate the dir
|
|
vDir[0] = m_Path.CurrentDelta()[0];
|
|
vDir[1] = m_Path.CurrentDelta()[1];
|
|
vDir[2] = 0;
|
|
VectorNormalize(vDir);
|
|
vAngles = vDir.toAngles() - angles;
|
|
vAngles.AngleVectorsLeft(&vWishDir);
|
|
|
|
m_vLastValidDir = vWishDir;
|
|
m_vLastValidGoal = m_vCurrentGoal;
|
|
|
|
// Forward to the specified direction
|
|
float x = vWishDir.x * 255.0f;
|
|
float y = -vWishDir.y * 255.0f;
|
|
|
|
m_botCmd.forwardmove = (signed char)(x > 127 ? 127 : x < -127 ? -127 : x);
|
|
m_botCmd.rightmove = (signed char)(y > 127 ? 127 : y < -127 ? -127 : y);
|
|
CheckJump();
|
|
|
|
Weapon *pWeap = GetActiveWeapon(WEAPON_MAIN);
|
|
|
|
if (pWeap && !pWeap->ShouldReload()) {
|
|
m_RunLabel.Execute(this);
|
|
}
|
|
}
|
|
|
|
void PlayerBot::CheckJump(void)
|
|
{
|
|
Vector start;
|
|
Vector end;
|
|
Vector dir;
|
|
trace_t trace;
|
|
|
|
if (!m_Path.CurrentNode()) {
|
|
return;
|
|
}
|
|
|
|
dir = m_Path.CurrentDelta();
|
|
VectorNormalizeFast(dir);
|
|
|
|
if (ai_debugpath->integer) {
|
|
G_DebugLine(origin + Vector(0, 0, STEPSIZE), origin + Vector(0, 0, STEPSIZE) + dir * 32, 1, 0, 1, 1);
|
|
G_DebugLine(origin + Vector(0, 0, 56), origin + Vector(0, 0, 56) + dir * 32, 1, 0, 1, 1);
|
|
}
|
|
|
|
Vector vStart = origin + Vector(0, 0, STEPSIZE);
|
|
Vector vEnd = origin + Vector(0, 0, STEPSIZE) + dir * 4;
|
|
|
|
// Check if the bot needs to jump
|
|
trace = G_Trace(vStart, mins, maxs, vEnd, this, MASK_PLAYERSOLID, false, "PlayerBot::CheckJump");
|
|
|
|
// No need to jump
|
|
if (trace.fraction >= 0.95f) {
|
|
m_botCmd.upmove = 0;
|
|
return;
|
|
}
|
|
|
|
start = origin + Vector(0, 0, 56);
|
|
end = origin + Vector(0, 0, 56) + dir * 4;
|
|
|
|
// Check if the bot can jump
|
|
trace = G_Trace(start, mins, maxs, end, this, MASK_PLAYERSOLID, true, "PlayerBot::CheckJump");
|
|
|
|
// Make the bot climb walls
|
|
if (trace.fraction >= 0.95f) {
|
|
if (!m_botCmd.upmove) {
|
|
m_botCmd.upmove = 127;
|
|
} else {
|
|
m_botCmd.upmove = 0;
|
|
}
|
|
} else {
|
|
m_botCmd.upmove = 0;
|
|
}
|
|
}
|
|
|
|
void PlayerBot::CheckEndPos(void)
|
|
{
|
|
Vector start;
|
|
Vector end;
|
|
trace_t trace;
|
|
|
|
if (!m_Path.LastNode()) {
|
|
return;
|
|
}
|
|
|
|
start = m_Path.LastNode()->point;
|
|
end = m_vTargetPos;
|
|
|
|
trace = G_Trace(start, mins, maxs, end, this, MASK_TARGETPATH, true, "PlayerBot::CheckEndPos");
|
|
|
|
if (trace.fraction < 0.95f) {
|
|
m_vTargetPos = trace.endpos;
|
|
}
|
|
}
|
|
|
|
void PlayerBot::CheckUse(void)
|
|
{
|
|
Vector dir;
|
|
Vector start;
|
|
Vector end;
|
|
trace_t trace;
|
|
|
|
m_botCmd.buttons &= ~BUTTON_USE;
|
|
|
|
angles.AngleVectorsLeft(&dir);
|
|
|
|
start = origin + Vector(0, 0, viewheight);
|
|
end = origin + Vector(0, 0, viewheight) + dir * 32;
|
|
|
|
trace = G_Trace(start, vec_zero, vec_zero, end, this, MASK_USABLE, false, "PlayerBot::CheckUse");
|
|
|
|
// It may be a door
|
|
if ((trace.allsolid || trace.startsolid || trace.fraction != 1.0f) && trace.entityNum) {
|
|
m_botCmd.buttons |= BUTTON_USE;
|
|
}
|
|
}
|
|
|
|
void PlayerBot::GetUsercmd(usercmd_t *ucmd)
|
|
{
|
|
*ucmd = m_botCmd;
|
|
}
|
|
|
|
void PlayerBot::GetEyeInfo(usereyes_t *eyeinfo)
|
|
{
|
|
*eyeinfo = m_botEyes;
|
|
}
|
|
|
|
void PlayerBot::UpdateBotStates(void)
|
|
{
|
|
if (bot_manualmove->integer) {
|
|
memset(&m_botCmd, 0, sizeof(usercmd_t));
|
|
return;
|
|
}
|
|
|
|
if (IsDead()) {
|
|
// The bot should respawn
|
|
m_botCmd.buttons ^= BUTTON_ATTACKLEFT;
|
|
return;
|
|
}
|
|
|
|
m_botCmd.buttons |= BUTTON_RUN;
|
|
|
|
m_botEyes.ofs[0] = 0;
|
|
m_botEyes.ofs[1] = 0;
|
|
m_botEyes.ofs[2] = viewheight;
|
|
m_botEyes.angles[0] = 0;
|
|
m_botEyes.angles[1] = 0;
|
|
|
|
SetTargetAngles(Vector(0, 90, 0));
|
|
|
|
CheckStates();
|
|
|
|
MoveThink();
|
|
TurnThink();
|
|
CheckUse();
|
|
}
|
|
|
|
void PlayerBot::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;
|
|
}
|
|
|
|
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(edict);
|
|
|
|
while (1) {
|
|
com_token = COM_Parse(&data);
|
|
|
|
if (!com_token || !*com_token) {
|
|
break;
|
|
}
|
|
|
|
ev.AddString(com_token);
|
|
}
|
|
|
|
gi.Free(buffer);
|
|
|
|
try {
|
|
ProcessEvent(ev);
|
|
} catch (ScriptException& exc) {
|
|
gi.DPrintf("*** Bot Command Exception *** %s\n", exc.string.c_str());
|
|
}
|
|
}
|
|
|
|
void PlayerBot::setAngles(Vector ang)
|
|
{
|
|
Entity::setAngles(ang);
|
|
SetTargetAngles(angles);
|
|
}
|
|
|
|
void PlayerBot::updateOrigin(void)
|
|
{
|
|
Entity::updateOrigin();
|
|
|
|
if (origin == client->ps.origin) {
|
|
// no change because of Pmove
|
|
return;
|
|
}
|
|
|
|
m_pPrimaryAttract = NULL;
|
|
|
|
if (m_Path.CurrentNode()) {
|
|
// recalculate paths because of a new origin
|
|
m_Path.ReFindPath(origin, this);
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
SetTargetAngles
|
|
|
|
Set the bot's angle
|
|
====================
|
|
*/
|
|
void PlayerBot::SetTargetAngles(Vector vAngles)
|
|
{
|
|
m_vTargetAng = vAngles;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
AimAt
|
|
|
|
Make the bot face to the specified direction
|
|
====================
|
|
*/
|
|
void PlayerBot::AimAt(Vector vPos)
|
|
{
|
|
Vector vDelta = vPos - centroid;
|
|
|
|
VectorNormalize(vDelta);
|
|
vectoangles(vDelta, m_vTargetAng);
|
|
}
|
|
|
|
/*
|
|
====================
|
|
AimAtAimNode
|
|
|
|
Make the bot face toward the current path
|
|
====================
|
|
*/
|
|
void PlayerBot::AimAtAimNode(void)
|
|
{
|
|
if (m_Path.CurrentDelta()) {
|
|
AimAt(origin + m_Path.CurrentDelta());
|
|
} else {
|
|
AimAt(m_vCurrentGoal);
|
|
}
|
|
|
|
m_vTargetAng[PITCH] = 0;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
CheckReload
|
|
|
|
Make the bot reload if necessary
|
|
====================
|
|
*/
|
|
void PlayerBot::CheckReload(void)
|
|
{
|
|
Weapon *weap = GetActiveWeapon(WEAPON_MAIN);
|
|
|
|
if (weap && weap->CheckReload(FIRE_PRIMARY)) {
|
|
SendCommand("reload");
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
MoveTo
|
|
|
|
Move to the specified position
|
|
====================
|
|
*/
|
|
void PlayerBot::MoveTo(Vector vPos, float *vLeashHome, float fLeashRadius)
|
|
{
|
|
m_bPathing = true;
|
|
m_vTargetPos = vPos;
|
|
m_Path.FindPath(origin, vPos, this, 0, vLeashHome, fLeashRadius * fLeashRadius);
|
|
|
|
if (!m_Path.CurrentNode()) {
|
|
m_bPathing = false;
|
|
return;
|
|
}
|
|
|
|
CheckEndPos();
|
|
}
|
|
|
|
/*
|
|
====================
|
|
MoveTo
|
|
|
|
Move to the nearest attractive point with a minimum priority
|
|
Returns true if no attractive point was found
|
|
====================
|
|
*/
|
|
bool PlayerBot::MoveToBestAttractivePoint(int iMinPriority)
|
|
{
|
|
Container<AttractiveNode *> list;
|
|
AttractiveNode *bestNode;
|
|
float bestDistanceSquared;
|
|
int bestPriority;
|
|
|
|
if (m_pPrimaryAttract) {
|
|
MoveTo(m_pPrimaryAttract->origin);
|
|
|
|
if (!IsMoving()) {
|
|
m_pPrimaryAttract = NULL;
|
|
} else {
|
|
if (MoveDone()) {
|
|
if (!m_fAttractTime) {
|
|
m_fAttractTime = level.time + m_pPrimaryAttract->m_fMaxStayTime;
|
|
}
|
|
if (level.time > m_fAttractTime) {
|
|
nodeAttract_t *a = new nodeAttract_t;
|
|
a->m_fRespawnTime = level.time + m_pPrimaryAttract->m_fRespawnTime;
|
|
a->m_pNode = m_pPrimaryAttract;
|
|
|
|
m_pPrimaryAttract = NULL;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!attractiveNodes.NumObjects()) {
|
|
return false;
|
|
}
|
|
|
|
bestNode = NULL;
|
|
bestDistanceSquared = 99999999.0f;
|
|
bestPriority = iMinPriority;
|
|
|
|
for (int i = attractiveNodes.NumObjects(); i > 0; i--) {
|
|
AttractiveNode *node = attractiveNodes.ObjectAt(i);
|
|
float distSquared;
|
|
bool m_bRespawning = false;
|
|
|
|
for (int j = m_attractList.NumObjects(); j > 0; j--) {
|
|
AttractiveNode *node2 = m_attractList.ObjectAt(j)->m_pNode;
|
|
|
|
if (node2 == node) {
|
|
m_bRespawning = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (m_bRespawning) {
|
|
continue;
|
|
}
|
|
|
|
if (node->m_iPriority < bestPriority) {
|
|
continue;
|
|
}
|
|
|
|
if (!node->CheckTeam(this)) {
|
|
continue;
|
|
}
|
|
|
|
distSquared = VectorLengthSquared(origin - node->origin);
|
|
|
|
if (node->m_fMaxDistanceSquared >= 0 && distSquared > node->m_fMaxDistanceSquared) {
|
|
continue;
|
|
}
|
|
|
|
if (!CanMoveTo(node->origin)) {
|
|
continue;
|
|
}
|
|
|
|
if (distSquared < bestDistanceSquared) {
|
|
bestDistanceSquared = distSquared;
|
|
bestNode = node;
|
|
bestPriority = node->m_iPriority;
|
|
}
|
|
}
|
|
|
|
if (bestNode) {
|
|
m_pPrimaryAttract = bestNode;
|
|
m_fAttractTime = 0;
|
|
MoveTo(bestNode->origin);
|
|
return true;
|
|
} else {
|
|
// No attractive point found
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
CanMoveTo
|
|
|
|
Returns true if the bot has done moving
|
|
====================
|
|
*/
|
|
bool PlayerBot::CanMoveTo(Vector vPos)
|
|
{
|
|
return m_Path.DoesTheoreticPathExist(origin, vPos, NULL, 0, NULL, 0);
|
|
}
|
|
|
|
/*
|
|
====================
|
|
MoveDone
|
|
|
|
Returns true if the bot has done moving
|
|
====================
|
|
*/
|
|
bool PlayerBot::MoveDone(void)
|
|
{
|
|
return !m_Path.LastNode() || m_Path.Complete(origin);
|
|
}
|
|
|
|
/*
|
|
====================
|
|
IsMoving
|
|
|
|
Returns true if the bot has a current path
|
|
====================
|
|
*/
|
|
bool PlayerBot::IsMoving(void)
|
|
{
|
|
return m_bPathing;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
ClearMove
|
|
|
|
Stop the bot from moving
|
|
====================
|
|
*/
|
|
void PlayerBot::ClearMove(void)
|
|
{
|
|
m_Path.Clear();
|
|
m_bPathing = false;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
MoveNear
|
|
|
|
Move near the specified position within the radius
|
|
====================
|
|
*/
|
|
void PlayerBot::MoveNear(Vector vNear, float fRadius, float *vLeashHome, float fLeashRadius)
|
|
{
|
|
m_bPathing = true;
|
|
m_Path.FindPathNear(origin, vNear, this, 0, fRadius * fRadius, vLeashHome, fLeashRadius * fLeashRadius);
|
|
|
|
if (!m_Path.CurrentNode()) {
|
|
m_bPathing = false;
|
|
return;
|
|
}
|
|
|
|
m_vTargetPos = m_Path.LastNode()->point;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
AvoidPath
|
|
|
|
Avoid the specified position within the radius and start from a direction
|
|
====================
|
|
*/
|
|
void PlayerBot::AvoidPath(
|
|
Vector vAvoid, float fAvoidRadius, Vector vPreferredDir, float *vLeashHome, float fLeashRadius
|
|
)
|
|
{
|
|
Vector vDir;
|
|
|
|
if (vPreferredDir == vec_zero) {
|
|
vDir = origin - vAvoid;
|
|
VectorNormalizeFast(vDir);
|
|
} else {
|
|
vDir = vPreferredDir;
|
|
}
|
|
|
|
m_bPathing = true;
|
|
m_Path.FindPathAway(origin, vAvoid, vDir, this, fAvoidRadius, vLeashHome, fLeashRadius * fLeashRadius);
|
|
|
|
if (!m_Path.CurrentNode()) {
|
|
m_bPathing = false;
|
|
return;
|
|
}
|
|
|
|
m_vTargetPos = m_Path.LastNode()->point;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
NoticeEvent
|
|
|
|
Warn the bot of an event
|
|
====================
|
|
*/
|
|
void PlayerBot::NoticeEvent(Vector vPos, int iType, Entity *pEnt, float fDistanceSquared, float fRadiusSquared)
|
|
{
|
|
// Ignore teammates
|
|
if (pEnt->IsSubclassOfPlayer()) {
|
|
Player *p = (Player *)pEnt;
|
|
|
|
if (p->GetTeam() == GetTeam()) {
|
|
return;
|
|
}
|
|
} else if (pEnt->IsSubclassOfWeapon()) {
|
|
Weapon *pWeap = (Weapon *)pEnt;
|
|
|
|
if (pWeap->GetOwner()) {
|
|
Player *p = (Player *)pWeap->GetOwner();
|
|
|
|
if (p->IsSubclassOfPlayer() && p->GetTeam() == 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_vLastCuriousPos = vPos;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
ClearEnemy
|
|
|
|
Clear the bot's enemy
|
|
====================
|
|
*/
|
|
void PlayerBot::ClearEnemy(void)
|
|
{
|
|
m_iAttackTime = 0;
|
|
m_pEnemy = NULL;
|
|
m_vOldEnemyPos = vec_zero;
|
|
m_vLastEnemyPos = vec_zero;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Bot states
|
|
--------------------
|
|
____________________
|
|
--------------------
|
|
____________________
|
|
--------------------
|
|
____________________
|
|
--------------------
|
|
____________________
|
|
====================
|
|
*/
|
|
|
|
void PlayerBot::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 !!!", client->pers.netname);
|
|
State_Reset();
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Default state
|
|
|
|
|
|
====================
|
|
*/
|
|
void PlayerBot::State_DefaultBegin(void)
|
|
{
|
|
ClearMove();
|
|
}
|
|
|
|
void PlayerBot::State_DefaultEnd(void) {}
|
|
|
|
void PlayerBot::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;
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Idle state
|
|
|
|
Make the bot move to random directions
|
|
====================
|
|
*/
|
|
void PlayerBot::InitState_Idle(botfunc_t *func)
|
|
{
|
|
func->CheckCondition = &PlayerBot::CheckCondition_Idle;
|
|
func->ThinkState = &PlayerBot::State_Idle;
|
|
}
|
|
|
|
bool PlayerBot::CheckCondition_Idle(void)
|
|
{
|
|
if (m_iCuriousTime) {
|
|
return false;
|
|
}
|
|
|
|
if (m_iAttackTime) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PlayerBot::State_Idle(void)
|
|
{
|
|
AimAtAimNode();
|
|
CheckReload();
|
|
|
|
if (!MoveToBestAttractivePoint() && !IsMoving()) {
|
|
if (m_vLastDeathPos != vec_zero) {
|
|
MoveTo(m_vLastDeathPos);
|
|
|
|
if (MoveDone()) {
|
|
m_vLastDeathPos = vec_zero;
|
|
}
|
|
} else {
|
|
AvoidPath(origin - Vector(orientation[0]) * 128.0f, 1024);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Curious state
|
|
|
|
Forward to the last event position
|
|
====================
|
|
*/
|
|
void PlayerBot::InitState_Curious(botfunc_t *func)
|
|
{
|
|
func->CheckCondition = &PlayerBot::CheckCondition_Curious;
|
|
func->ThinkState = &PlayerBot::State_Curious;
|
|
}
|
|
|
|
bool PlayerBot::CheckCondition_Curious(void)
|
|
{
|
|
if (m_iAttackTime) {
|
|
m_iCuriousTime = 0;
|
|
return false;
|
|
}
|
|
|
|
if (level.inttime > m_iCuriousTime) {
|
|
if (m_iCuriousTime) {
|
|
ClearMove();
|
|
m_iCuriousTime = 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PlayerBot::State_Curious(void)
|
|
{
|
|
//AimAt( m_vLastCuriousPos );
|
|
AimAtAimNode();
|
|
|
|
if (!MoveToBestAttractivePoint(3) && !IsMoving()) {
|
|
MoveTo(m_vLastCuriousPos);
|
|
}
|
|
|
|
if (MoveDone()) {
|
|
m_iCuriousTime = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Attack state
|
|
|
|
Attack the enemy
|
|
====================
|
|
*/
|
|
void PlayerBot::InitState_Attack(botfunc_t *func)
|
|
{
|
|
func->CheckCondition = &PlayerBot::CheckCondition_Attack;
|
|
func->EndState = &PlayerBot::State_EndAttack;
|
|
func->ThinkState = &PlayerBot::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 PlayerBot::CheckCondition_Attack(void)
|
|
{
|
|
Container<Sentient *> sents = SentientList;
|
|
|
|
bot_origin = origin;
|
|
sents.Sort(sentients_compare);
|
|
|
|
for (int i = 1; i <= sents.NumObjects(); i++) {
|
|
Sentient *sent = sents.ObjectAt(i);
|
|
Player *player = (Player *)sent;
|
|
|
|
if (sent == this) {
|
|
continue;
|
|
}
|
|
|
|
if (sent->hidden()) {
|
|
continue;
|
|
}
|
|
|
|
if (sent->IsDead()) {
|
|
continue;
|
|
}
|
|
|
|
if (sent->IsSubclassOfPlayer()) {
|
|
if (g_gametype->integer >= GT_TEAM && player->GetTeam() == GetTeam()) {
|
|
continue;
|
|
}
|
|
} else {
|
|
if (sent->m_Team == m_Team) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (CanSee(sent, 80, world->m_fAIVisionDistance, false)) {
|
|
m_pEnemy = sent;
|
|
m_iAttackTime = level.inttime + 10000;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (level.inttime > m_iAttackTime) {
|
|
if (m_iAttackTime) {
|
|
ClearMove();
|
|
m_iAttackTime = 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void PlayerBot::State_EndAttack(void)
|
|
{
|
|
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
|
|
}
|
|
|
|
void PlayerBot::State_Attack(void)
|
|
{
|
|
Player *p = (Player *)m_pEnemy.Pointer();
|
|
bool bMelee = false;
|
|
float fMinDistance = 128;
|
|
float fMinDistanceSquared = fMinDistance * fMinDistance;
|
|
|
|
if (!m_pEnemy || m_pEnemy->IsDead() || (!p->IsSubclassOfPlayer() && m_pEnemy->m_Team == m_Team)
|
|
|| (g_gametype->integer >= GT_TEAM && p->GetTeam() == GetTeam())) {
|
|
m_iAttackTime = 0;
|
|
return;
|
|
}
|
|
|
|
float fDistanceSquared = (m_pEnemy->origin - origin).lengthSquared();
|
|
|
|
if (CanSee(m_pEnemy, 20, world->m_fAIVisionDistance, false)) {
|
|
Weapon *pWeap = GetActiveWeapon(WEAPON_MAIN);
|
|
|
|
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;
|
|
|
|
fMinDistance = fPrimaryBulletRange;
|
|
|
|
if (fMinDistance > 256) {
|
|
fMinDistance = 256;
|
|
}
|
|
|
|
fMinDistanceSquared = fMinDistance * fMinDistance;
|
|
|
|
if (client->ps.stats[STAT_AMMO] > 0 || client->ps.stats[STAT_CLIPAMMO] > 0) {
|
|
if (fDistanceSquared <= fPrimaryBulletRangeSquared) {
|
|
if (pWeap->IsSemiAuto()) {
|
|
m_botCmd.buttons ^= BUTTON_ATTACKLEFT;
|
|
} else {
|
|
m_botCmd.buttons |= BUTTON_ATTACKLEFT;
|
|
}
|
|
} else {
|
|
m_botCmd.buttons &= ~BUTTON_ATTACKLEFT;
|
|
}
|
|
} else if (pWeap->GetFireType(FIRE_SECONDARY) == FT_MELEE) {
|
|
bMelee = true;
|
|
|
|
if (fDistanceSquared <= fSecondaryBulletRangeSquared) {
|
|
m_botCmd.buttons ^= BUTTON_ATTACKRIGHT;
|
|
} else {
|
|
m_botCmd.buttons &= ~BUTTON_ATTACKRIGHT;
|
|
}
|
|
} else {
|
|
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
|
|
}
|
|
} else {
|
|
m_botCmd.buttons &= ~(BUTTON_ATTACKLEFT | BUTTON_ATTACKRIGHT);
|
|
fMinDistanceSquared = 0;
|
|
}
|
|
|
|
AimAt(m_vLastEnemyPos);
|
|
|
|
m_vOldEnemyPos = m_vLastEnemyPos;
|
|
m_vLastEnemyPos = m_pEnemy->centroid;
|
|
|
|
if ((!MoveToBestAttractivePoint(5) && !IsMoving()) || (m_vOldEnemyPos != m_vLastEnemyPos && !MoveDone())) {
|
|
if (!bMelee) {
|
|
if ((origin - m_vLastEnemyPos).lengthSquared() < fMinDistanceSquared) {
|
|
Vector vDir = origin - m_vLastEnemyPos;
|
|
VectorNormalizeFast(vDir);
|
|
|
|
AvoidPath(m_vLastEnemyPos, fMinDistance, vDir);
|
|
} else {
|
|
MoveNear(m_vLastEnemyPos, fMinDistance);
|
|
}
|
|
} else {
|
|
MoveTo(m_vLastEnemyPos);
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Grenade state
|
|
|
|
Avoid any grenades
|
|
====================
|
|
*/
|
|
void PlayerBot::InitState_Grenade(botfunc_t *func)
|
|
{
|
|
func->CheckCondition = &PlayerBot::CheckCondition_Grenade;
|
|
func->ThinkState = &PlayerBot::State_Grenade;
|
|
}
|
|
|
|
bool PlayerBot::CheckCondition_Grenade(void)
|
|
{
|
|
// FIXME: TODO
|
|
return false;
|
|
}
|
|
|
|
void PlayerBot::State_Grenade(void)
|
|
{
|
|
// FIXME: TODO
|
|
}
|
|
|
|
/*
|
|
====================
|
|
Weapon state
|
|
|
|
Change weapon when necessary
|
|
====================
|
|
*/
|
|
void PlayerBot::InitState_Weapon(botfunc_t *func)
|
|
{
|
|
func->CheckCondition = &PlayerBot::CheckCondition_Weapon;
|
|
func->BeginState = &PlayerBot::State_BeginWeapon;
|
|
}
|
|
|
|
bool PlayerBot::CheckCondition_Weapon(void)
|
|
{
|
|
return GetActiveWeapon(WEAPON_MAIN) != BestWeapon(NULL, false, WEAPON_CLASS_THROWABLE);
|
|
}
|
|
|
|
void PlayerBot::State_BeginWeapon(void)
|
|
{
|
|
Weapon *weap = BestWeapon(NULL, false, WEAPON_CLASS_THROWABLE);
|
|
|
|
if (weap == NULL) {
|
|
SendCommand("safeholster 1");
|
|
return;
|
|
}
|
|
|
|
SendCommand(va("use \"%s\"", weap->model.c_str()));
|
|
}
|
|
|
|
void PlayerBot::Spawned(void)
|
|
{
|
|
ClearEnemy();
|
|
m_iCuriousTime = 0;
|
|
|
|
Player::Spawned();
|
|
}
|
|
|
|
void PlayerBot::Killed(Event *ev)
|
|
{
|
|
Player::Killed(ev);
|
|
|
|
// 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;
|
|
|
|
m_vLastDeathPos = origin;
|
|
}
|
|
|
|
void PlayerBot::GotKill(Event *ev)
|
|
{
|
|
Player::GotKill(ev);
|
|
|
|
ClearEnemy();
|
|
m_iCuriousTime = 0;
|
|
}
|
|
|
|
void PlayerBot::EventStuffText(Event *ev)
|
|
{
|
|
SendCommand(ev->GetString(1));
|
|
}
|