Files
2026-05-23 20:50:12 -07:00

579 lines
15 KiB
C++

// Bot.cpp
//
#include "precompiled.h"
#include "../Game_local.h"
idCVar bot_pathdebug( "bot_pathdebug", "0", CVAR_BOOL | CVAR_CHEAT, "force the bot to path to player" );
idCVar bot_goaldist( "bot_goaldist", "20", CVAR_INTEGER | CVAR_CHEAT, "" );
idCVar bot_debugnav( "bot_debugnav", "0", CVAR_BOOL | CVAR_CHEAT, "draws navmesh paths for the bot" );
idCVar bot_showstate( "bot_showstate", "0", CVAR_BOOL | CVAR_CHEAT, "draws the bot state above the bot" );
idCVar bot_debug( "bot_debug", "0", CVAR_BOOL, "shows debug info for the bot" );
idCVar bot_skill("bot_skill", "4", CVAR_INTEGER, "");
static int BotSafeAmmoCount( const idInventory& inv, const char* ammoName ) {
const int ammoNum = idWeapon::GetAmmoNumForName( ammoName );
if( ammoNum < 0 || ammoNum >= AMMO_NUMTYPES ) {
return 0;
}
return inv.ammo[ammoNum];
}
static bool BotValidGameEntityNum( int entityNum ) {
return entityNum >= 0 && entityNum < gameLocal.num_entities && gameLocal.entities[entityNum] != NULL;
}
static bool BotDirectPathIsClear( int passEntityNum, const idVec3& from, const idVec3& to ) {
trace_t trace;
gameLocal.Trace( trace, from, to, CONTENTS_SOLID | CONTENTS_PLAYERCLIP, passEntityNum );
return trace.fraction >= 0.95f;
}
static idAngles BotTurnTowardAngles( const idAngles& currentAngles, const idAngles& desiredAngles, float maxTurn )
{
idAngles result = currentAngles;
for( int i = 0; i < 2; i++ )
{
float delta = idMath::AngleDelta( desiredAngles[i], currentAngles[i] );
delta = idMath::ClampFloat( -maxTurn, maxTurn, delta );
result[i] = idMath::AngleMod( currentAngles[i] + delta );
}
result[ROLL] = 0.0f;
return result;
}
static idVec3 bot_stuckOrigin[MAX_CLIENTS];
static float bot_stuckTime[MAX_CLIENTS];
static bool bot_stuckInit[MAX_CLIENTS];
/*
===================
rvmBot::rvmBot
===================
*/
rvmBot::rvmBot()
{
//bs.action = NULL;
hasSpawned = false;
//gameLocal.RegisterBot( this );
}
/*
===================
rvmBot::~rvmBot
===================
*/
rvmBot::~rvmBot()
{
//gameLocal.UnRegisterBot( this );
}
/*
==================
rvmBot::SetEnemy
==================
*/
void rvmBot::SetEnemy( idPlayer* player, idVec3 origin)
{
if( player == NULL || player == this || player->health <= 0 || player->spectating )
{
return;
}
const bool noEnemy = bs.enemy < 0 || !BotValidGameEntityNum( bs.enemy ) || EntityIsDead( gameLocal.entities[bs.enemy] );
const bool sameEnemy = bs.enemy == player->entityNumber;
if( noEnemy || sameEnemy )
{
bs.enemy = player->entityNumber;
bs.aggressiveAttackTime = gameLocal.SysScriptTime() + 2.0f;
bs.lastenemyorigin = origin;
bs.last_enemy_visible_position = origin;
stateThread.SetState("state_Attacked");
}
}
/*
==================
rvmBot::BotUpdateInventory
==================
*/
void rvmBot::BotUpdateInventory( void )
{
bs.inventory[INVENTORY_ARMOR] = inventory.armor;
bs.inventory[INVENTORY_GAUNTLET] = 1;
bs.inventory[INVENTORY_SHOTGUN] = HasWeapon( weapon_shotgun );
bs.inventory[INVENTORY_MACHINEGUN] = HasWeapon( weapon_machinegun );
bs.inventory[INVENTORY_GRENADELAUNCHER] = 0;
bs.inventory[INVENTORY_ROCKETLAUNCHER] = HasWeapon( weapon_rocketlauncher );
bs.inventory[INVENTORY_LIGHTNING] = 0;
bs.inventory[INVENTORY_RAILGUN] = 0;
bs.inventory[INVENTORY_PLASMAGUN] = HasWeapon( weapon_plasmagun );
bs.inventory[INVENTORY_BFG10K] = 0;
bs.inventory[INVENTORY_GRAPPLINGHOOK] = 0;
bs.inventory[INVENTORY_SHELLS] = BotSafeAmmoCount( inventory, "ammo_shells" );
bs.inventory[INVENTORY_BULLETS] = BotSafeAmmoCount( inventory, "ammo_clip" );
bs.inventory[INVENTORY_GRENADES] = 0;
bs.inventory[INVENTORY_CELLS] = BotSafeAmmoCount( inventory, "ammo_cells" );
bs.inventory[INVENTORY_LIGHTNINGAMMO] = 0;
bs.inventory[INVENTORY_ROCKETS] = BotSafeAmmoCount( inventory, "ammo_rockets" );
bs.inventory[INVENTORY_SLUGS] = 0;
bs.inventory[INVENTORY_BFGAMMO] = 0;
bs.inventory[INVENTORY_HEALTH] = health;
bs.inventory[INVENTORY_TELEPORTER] = 0;
bs.inventory[INVENTORY_MEDKIT] = 0;
bs.inventory[INVENTORY_QUAD] = 0;
bs.inventory[INVENTORY_ENVIRONMENTSUIT] = 0;
bs.inventory[INVENTORY_HASTE] = 0;
bs.inventory[INVENTORY_INVISIBILITY] = 0;
bs.inventory[INVENTORY_REGEN] = 0;
bs.inventory[INVENTORY_FLIGHT] = 0;
bs.inventory[INVENTORY_REDFLAG] = 0;
bs.inventory[INVENTORY_BLUEFLAG] = 0;
}
/*
===================
rvmBot::Spawn
===================
*/
void rvmBot::Spawn( void )
{
idStr botName;
char filename[256];
int errnum;
stateThread.SetOwner(this);
idPlayer::Spawn();
if( gameLocal.isServer )
{
weapon_machinegun = SlotForWeapon( "weapon_machinegun" );
weapon_shotgun = SlotForWeapon( "weapon_shotgun" );
weapon_plasmagun = SlotForWeapon( "weapon_plasmagun" );
weapon_rocketlauncher = SlotForWeapon( "weapon_rocketlauncher" );
WP_MACHINEGUN = weapon_machinegun;
WP_SHOTGUN = weapon_shotgun;
WP_PLASMAGUN = weapon_plasmagun;
WP_ROCKET_LAUNCHER = weapon_rocketlauncher;
botName = "dark";// spawnArgs.GetString("botname");
aas = gameLocal.GetBotNAV();
if( aas == NULL )
{
gameLocal.Error( "Missing NAV\n" );
return;
}
// Load in the bot character.
const int skill = idMath::ClampInt( 1, 5, bot_skill.GetInteger() );
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), skill );
if( bs.character == NULL && skill != 4 )
{
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), 4 );
}
if( bs.character == NULL && skill != 1 )
{
bs.character = botCharacterStatsManager.BotLoadCharacterFromFile( va( "bots/%s_c.c", botName.c_str() ), 1 );
}
if( !bs.character )
{
gameLocal.Error( "Failed to load character file for bot %s\n", botName.c_str() );
}
// Allocate the goal state.
bs.gs = botGoalManager.BotAllocGoalState( entityNumber );
// Get the bot items weights file name.
botCharacterStatsManager.Characteristic_String( bs.character, CHARACTERISTIC_ITEMWEIGHTS, filename, 256 );
errnum = botGoalManager.BotLoadItemWeights( bs.gs, filename );
if( errnum != BLERR_NOERROR )
{
gameLocal.Error( "Failed to load bot item weights!" );
botGoalManager.BotFreeGoalState( bs.gs );
return;
}
//allocate a weapon state
bs.ws = botWeaponInfoManager.BotAllocWeaponState();
//load the weapon weights
botCharacterStatsManager.Characteristic_String( bs.character, CHARACTERISTIC_WEAPONWEIGHTS, filename, 256 );
errnum = botWeaponInfoManager.BotLoadWeaponWeights( bs.ws, filename );
if( errnum != BLERR_NOERROR )
{
// trap_BotFreeGoalState(bs->gs);
botWeaponInfoManager.BotFreeWeaponState( bs.ws );
return;
}
bs.client = entityNumber;
bs.entitynum = entityNumber;
bs.setupcount = 4;
bs.entergame_time = Bot_Time();
bs.weaponnum = spawnArgs.GetInt( "current_weapon", "1" );
hasSpawned = true;
bs.botinput.respawn = true;
bs.botinput.weapon = bs.weaponnum;
bs.botinput.lastWeaponNum = -1;
stateThread.SetState("state_Respawn");
}
}
/*
===================
rvmBot::Think
===================
*/
void rvmBot::BotMoveToGoalOrigin(idVec3 goalOrigin)
{
idVec3 moveDir = goalOrigin - GetPhysics()->GetOrigin();
const float goalDist = moveDir.Length();
// Usercmd forward/right movement is horizontal. Feeding pitch or a view-origin
// z delta into the movement vector makes the bot under-steer, drift, or push
// into walls while looking up/down at a target.
moveDir[2] = 0.0f;
if( moveDir.LengthSqr() < Square( 4.0f ) )
{
bs.botinput.dir = idVec3( 0.0f, 0.0f, 0.0f );
bs.botinput.speed = 0.0f;
}
else
{
if( bs.enemy < 0 && goalDist > 96.0f )
{
if( bs.move_wander_time < Bot_Time() )
{
bs.move_wander_time = Bot_Time() + idMath::FRandRange( 0.75f, 1.8f );
bs.move_wander_side = rvmBotUtil::crandom();
if( idMath::Fabs( bs.move_wander_side ) < 0.35f )
{
bs.move_wander_side = bs.move_wander_side < 0.0f ? -0.35f : 0.35f;
}
}
idVec3 forwardDir = moveDir;
forwardDir.Normalize();
idVec3 sideDir( -forwardDir[1], forwardDir[0], 0.0f );
const float wanderScale = idMath::ClampFloat( 0.0f, 64.0f, goalDist * 0.18f );
idVec3 wanderedDir = moveDir + sideDir * ( wanderScale * bs.move_wander_side );
wanderedDir[2] = 0.0f;
idVec3 testDir = wanderedDir;
if( testDir.Normalize() > 0.0f )
{
idVec3 testEnd = GetPhysics()->GetOrigin() + testDir * 56.0f;
if( BotDirectPathIsClear( entityNumber, GetPhysics()->GetOrigin() + idVec3( 0.0f, 0.0f, 24.0f ), testEnd + idVec3( 0.0f, 0.0f, 24.0f ) ) )
{
moveDir = wanderedDir;
}
else
{
bs.move_wander_side = -bs.move_wander_side;
}
}
}
bs.botinput.dir = moveDir;
bs.botinput.dir.Normalize();
bs.botinput.speed = pm_runspeed.GetInteger();
}
if( bs.enemy >= 0 )
{
// Combat states set bs.viewangles through BotAimAtEnemy. Do not replace that
// with a simple eye-to-eye angle here, or prediction/ground-rocket aiming is lost.
bs.botinput.viewangles = bs.viewangles;
}
else if( moveDir.LengthSqr() > Square( 1.0f ) )
{
const idAngles desiredAngles( 0.0f, moveDir.ToYaw(), 0.0f );
bs.botinput.viewangles = BotTurnTowardAngles( viewAngles, desiredAngles, 22.0f );
bs.viewangles = bs.botinput.viewangles;
}
else
{
bs.botinput.viewangles = viewAngles;
bs.viewangles = viewAngles;
}
}
/*
===================
rvmBot::SpawnToPoint
===================
*/
void rvmBot::SpawnToPoint( const idVec3& spawn_origin, const idAngles& spawn_angles )
{
idPlayer::SpawnToPoint( spawn_origin, spawn_angles );
if( gameLocal.isServer )
{
bs.ltg_time = 0;
stateThread.SetState("state_SeekLTG");
}
}
/*
===================
rvmBot::StateThreadChanged
===================
*/
void rvmBot::StateThreadChanged(void) {
// Ensure if we are switching states, pop the last goal.
bs.ltg_time = 0;
}
/*
===================
rvmBot::ServerThink
===================
*/
void rvmBot::ServerThink( void )
{
bs.origin = GetPhysics()->GetOrigin();
bs.eye = GetEyePosition();
bs.thinktime = Bot_Time();
bs.botinput.actionflags = 0;
bs.botinput.dir = idVec3( 0.0f, 0.0f, 0.0f );
bs.botinput.speed = 0.0f;
bs.botinput.viewangles = viewAngles;
bs.viewangles = viewAngles;
BotUpdateInventory();
if( bot_pathdebug.IsModified() )
{
idPlayer* localPlayer = gameLocal.GetLocalPlayer();
if( bot_pathdebug.GetBool() && localPlayer != NULL )
{
bs.currentGoal.origin = localPlayer->GetPhysics()->GetOrigin();
bs.currentGoal.framenum = gameLocal.framenum;
}
bot_pathdebug.ClearModified();
bot_pathdebug.SetBool( false );
}
stateThread.Execute();
if( BotIsDead( &bs ) || spectating || aas == NULL )
{
bs.botinput.weapon = bs.weaponnum;
bs.attackerEntity = NULL;
return;
}
if( bs.enemy >= 0 && bs.botinput.speed > 0.0f )
{
bs.viewangles = bs.botinput.viewangles;
bs.useRandomPosition = false;
bs.attackerEntity = NULL;
bs.botinput.weapon = bs.weaponnum;
return;
}
idVec3 goalOrigin = bs.useRandomPosition ? bs.random_move_position : bs.currentGoal.origin;
idVec3 moveGoal = goalOrigin;
bool haveMoveGoal = false;
if( goalOrigin.LengthSqr() > Square( 1.0f ) )
{
aasPath_t path;
idVec3 org = bs.origin;
int curAreaNum = aas->AdjustPositionAndGetArea( org );
idVec3 adjustedGoal = goalOrigin;
int goalArea = aas->PointAreaNum( adjustedGoal );
if( goalArea <= 0 )
{
goalArea = aas->AdjustPositionAndGetArea( adjustedGoal );
}
if( bot_debug.GetBool() )
{
aas->ShowWalkPath( GetOrigin(), goalArea, adjustedGoal );
aas->ShowArea( GetOrigin() );
}
if( curAreaNum > 0 && goalArea > 0 && aas->WalkPathToGoal( path, curAreaNum, org, goalArea, adjustedGoal, TFL_WALK | TFL_AIR ) )
{
moveGoal = path.moveGoal;
haveMoveGoal = true;
}
else if( BotDirectPathIsClear( entityNumber, bs.eye, adjustedGoal + idVec3( 0.0f, 0.0f, 24.0f ) ) )
{
moveGoal = adjustedGoal;
haveMoveGoal = true;
}
}
// If the selected route is not producing movement, invalidate the current path
// and choose a small local NAV point. This prevents repeatedly pushing into the
// same wall or corner when the item/chase goal is stale.
if( haveMoveGoal )
{
const int stuckIndex = entityNumber >= 0 && entityNumber < MAX_CLIENTS ? entityNumber : 0;
if( !bot_stuckInit[stuckIndex] || ( bs.origin - bot_stuckOrigin[stuckIndex] ).LengthSqr() > Square( 8.0f ) )
{
bot_stuckInit[stuckIndex] = true;
bot_stuckOrigin[stuckIndex] = bs.origin;
bot_stuckTime[stuckIndex] = Bot_Time();
}
else if( Bot_Time() - bot_stuckTime[stuckIndex] > 1.25f )
{
bs.ltg_time = 0;
bs.nbg_time = 0;
BotGetRandomPointNearPosition( bs.origin, bs.random_move_position, 96.0f );
bs.useRandomPosition = true;
moveGoal = bs.random_move_position;
bot_stuckOrigin[stuckIndex] = bs.origin;
bot_stuckTime[stuckIndex] = Bot_Time();
}
BotMoveToGoalOrigin( moveGoal );
}
else
{
bs.botinput.dir = idVec3( 0.0f, 0.0f, 0.0f );
bs.botinput.speed = 0.0f;
bs.botinput.viewangles = bs.viewangles;
}
bs.viewangles = bs.botinput.viewangles;
bs.useRandomPosition = false;
bs.attackerEntity = NULL;
bs.botinput.weapon = bs.weaponnum;
}
/*
=======================
rvmBot::Damage
=======================
*/
void rvmBot::Damage( idEntity* inflictor, idEntity* attacker, const idVec3& dir, const char* damageDefName, const float damageScale, const int location )
{
idPlayer::Damage( inflictor, attacker, dir, damageDefName, damageScale, location );
idPlayer* player = attacker != NULL ? attacker->Cast<idPlayer>() : NULL;
if( health <= 0 )
{
if( player != NULL )
{
BotSendChatMessage( DEATH, player->netname );
}
return;
}
if( player == NULL )
{
return;
}
bs.attackerEntity = player;
SetEnemy( player, player->GetOrigin() );
}
/*
=======================
rvmBot::InflictedDamageEvent
=======================
*/
void rvmBot::InflictedDamageEvent(idEntity* target) {
idPlayer* player = target != NULL ? target->Cast<idPlayer>() : NULL;
// Don't flood the chat with death and insults.
if( player != NULL && !player->IsBot() && player->health <= 0 )
{
BotSendChatMessage(KILL, player->netname);
}
}
/*
=======================
rvmBot::PresenceTypeBoundingBox
=======================
*/
void rvmBot::PresenceTypeBoundingBox( int presencetype, idVec3& mins, idVec3& maxs )
{
int index;
//bounding box size for each presence type
//idVec3 boxmins[3] = { {0, 0, 0}, {-15, -15, -24}, {-15, -15, -24} };
//idVec3 boxmaxs[3] = { {0, 0, 0}, { 15, 15, 32}, { 15, 15, 8} };
idVec3 boxmins[3];
idVec3 boxmaxs[3];
boxmins[0] = idVec3( 0, 0, 0 );
boxmins[1] = idVec3( -15, -15, -24 );
boxmins[2] = idVec3( -15, -15, -24 );
boxmaxs[0] = idVec3( 0, 0, 0 );
boxmaxs[1] = idVec3( 15, 15, 32 );
boxmaxs[2] = idVec3( 15, 15, 8 );
if( presencetype == PRESENCE_NORMAL )
{
index = 1;
}
else if( presencetype == PRESENCE_CROUCH )
{
index = 2;
}
else
{
//botimport.Print(PRT_FATAL, "NAV_PresenceTypeBoundingBox: unknown presence type\n");
index = 2;
}
mins = boxmins[index];
maxs = boxmaxs[index];
}
/*
===================
rvmBot::Think
===================
*/
void rvmBot::Think( void )
{
if( !hasSpawned )
{
return;
}
if( gameLocal.isServer )
{
ServerThink();
if( bot_debug.GetBool() )
{
idVec4 color;
color = idVec4(1, 1, 1, 1);
if (bs.enemy >= 0)
color = idVec4(1, 0, 0, 1);
idBounds bounds = idBounds( idVec3( -10, -10, -10 ), idVec3( 10, 10, 10 ) );
gameRenderWorld->DebugBounds( color, bounds, GetOrigin() );
idMat3 axis = viewAngles.ToMat3();
gameRenderWorld->DrawTextA(stateThread.GetState()->state.c_str(), GetOrigin(), 1.0f, color, axis);
}
}
deltaViewAngles.Zero();
idPlayer::Think();
}