mirror of
https://github.com/openmoh/openmohaa.git
synced 2025-04-28 13:47:58 +03:00
1619 lines
27 KiB
C++
1619 lines
27 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 2015 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 |= EF_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;
|
|
}
|
|
|
|
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 );
|
|
|
|
if( m_bDeltaMove )
|
|
m_vCurrentGoal = origin + m_Path.CurrentDelta();
|
|
else
|
|
m_vCurrentGoal = m_Path.CurrentNode()->point;
|
|
|
|
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.DPrintf( "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 = m_Path.CurrentDelta();
|
|
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();
|
|
|
|
Event params;
|
|
|
|
Weapon *pWeap = GetActiveWeapon( WEAPON_MAIN );
|
|
|
|
if( pWeap && !pWeap->ShouldReload() )
|
|
{
|
|
ExecuteThread( "global/bot_run.scr", "", params );
|
|
}
|
|
}
|
|
|
|
void PlayerBot::CheckJump
|
|
(
|
|
void
|
|
)
|
|
{
|
|
Vector start;
|
|
Vector end;
|
|
Vector dir;
|
|
trace_t trace;
|
|
|
|
if( !m_Path.CurrentNode() )
|
|
return;
|
|
|
|
dir = m_Path.CurrentDelta();
|
|
VectorNormalizeFast( dir );
|
|
|
|
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.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 ) )
|
|
{
|
|
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 ) )
|
|
{
|
|
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 ) );
|
|
}
|