For the graph files that have many close-placed nodes bots will switch his view constantly to new one when going on path, making it too shaky while moving. This fix makes bots to look at next node instead of currently reaching one, so aiming is a bit clearer now.
5822 lines
187 KiB
C++
5822 lines
187 KiB
C++
//
|
|
// Yet Another POD-Bot, based on PODBot by Markus Klinge ("CountFloyd").
|
|
// Copyright (c) YaPB Development Team.
|
|
//
|
|
// This software is licensed under the BSD-style license.
|
|
// Additional exceptions apply. For full license details, see LICENSE.txt or visit:
|
|
// https://yapb.ru/license
|
|
//
|
|
|
|
#include <yapb.h>
|
|
|
|
ConVar yb_debug ("yb_debug", "0", "Enables or disables useful messages about bot states. Not required for end users", true, 0.0f, 4.0f);
|
|
ConVar yb_debug_goal ("yb_debug_goal", "-1", "Forces all alive bots to build path and go to the specified here graph node.", true, -1.0f, kMaxNodes);
|
|
ConVar yb_user_follow_percent ("yb_user_follow_percent", "20", "Specifies the percent of bots, than can follow leader on each round start.", true, 0.0f, 100.0f);
|
|
ConVar yb_user_max_followers ("yb_user_max_followers", "1", "Specifies how many bots can follow a single user.", true, 0.0f, static_cast <float> (kGameMaxPlayers / 2));
|
|
|
|
ConVar yb_jasonmode ("yb_jasonmode", "0", "If enabled, all bots will be forced only the knife, skipping weapon buying routines.");
|
|
ConVar yb_radio_mode ("yb_radio_mode", "2", "Allows bots to use radio or chattter.\nAllowed values: '0', '1', '2'.\nIf '0', radio and chatter is disabled.\nIf '1', only radio allowed.\nIf '2' chatter and radio allowed.", true, 0.0f, 2.0f);
|
|
|
|
ConVar yb_economics_rounds ("yb_economics_rounds", "1", "Specifies whether bots able to use team economics, like do not buy any weapons for whole team to keep money for better guns.");
|
|
ConVar yb_walking_allowed ("yb_walking_allowed", "1", "Sepcifies whether bots able to use 'shift' if they thinks that enemy is near.");
|
|
ConVar yb_camping_allowed ("yb_camping_allowed", "1", "Allows or disallows bots to camp. Doesn't affects bomb/hostage defending tasks");
|
|
|
|
ConVar yb_tkpunish ("yb_tkpunish", "1", "Allows or disallows bots to take revenge of teamkillers / team attacks.");
|
|
ConVar yb_freeze_bots ("yb_freeze_bots", "0", "If enables bots think function is disabled, so bots will not move anywhere from their spawn spots.");
|
|
ConVar yb_spraypaints ("yb_spraypaints", "1", "Allows or disallows the use of spay paints.");
|
|
ConVar yb_botbuy ("yb_botbuy", "1", "Allows or disallows bots weapon buying routines.");
|
|
ConVar yb_destroy_breakables_around ("yb_destroy_breakables_around", "1", "Allows bots to destroy breakables around him, even without touching with them.");
|
|
|
|
ConVar yb_chatter_path ("yb_chatter_path", "sound/radio/bot", "Specifies the paths for the bot chatter sound files.", false);
|
|
ConVar yb_restricted_weapons ("yb_restricted_weapons", "", "Specifies semicolon separated list of weapons that are not allowed to buy / pickup.", false);
|
|
|
|
// game console variables
|
|
ConVar mp_c4timer ("mp_c4timer", nullptr, Var::NoRegister);
|
|
ConVar mp_flashlight ("mp_flashlight", nullptr, Var::NoRegister);
|
|
ConVar mp_buytime ("mp_buytime", nullptr, Var::NoRegister, true, "1");
|
|
ConVar mp_startmoney ("mp_startmoney", nullptr, Var::NoRegister, true, "800");
|
|
ConVar mp_footsteps ("mp_footsteps", nullptr, Var::NoRegister);
|
|
|
|
ConVar sv_gravity ("sv_gravity", nullptr, Var::NoRegister);
|
|
|
|
int Bot::getMsgQueue () {
|
|
// this function get the current message from the bots message queue
|
|
|
|
int message = m_messageQueue[m_actMessageIndex++];
|
|
m_actMessageIndex &= 0x1f; // wraparound
|
|
|
|
return message;
|
|
}
|
|
|
|
void Bot::pushMsgQueue (int message) {
|
|
// this function put a message into the bot message queue
|
|
|
|
if (message == BotMsg::Say) {
|
|
// notify other bots of the spoken text otherwise, bots won't respond to other bots (network messages aren't sent from bots)
|
|
int entityIndex = index ();
|
|
|
|
for (const auto &other : bots) {
|
|
if (other->pev != pev) {
|
|
if (m_notKilled == other->m_notKilled) {
|
|
other->m_sayTextBuffer.entityIndex = entityIndex;
|
|
other->m_sayTextBuffer.sayText = m_chatBuffer;
|
|
}
|
|
other->m_sayTextBuffer.timeNextChat = game.time () + other->m_sayTextBuffer.chatDelay;
|
|
}
|
|
}
|
|
}
|
|
m_messageQueue[m_pushMessageIndex++] = message;
|
|
m_pushMessageIndex &= 0x1f; // wraparound
|
|
}
|
|
|
|
float Bot::isInFOV (const Vector &destination) {
|
|
float entityAngle = cr::modAngles (destination.yaw ()); // find yaw angle from source to destination...
|
|
float viewAngle = cr::modAngles (pev->v_angle.y); // get bot's current view angle...
|
|
|
|
// return the absolute value of angle to destination entity
|
|
// zero degrees means straight ahead, 45 degrees to the left or
|
|
// 45 degrees to the right is the limit of the normal view angle
|
|
float absoluteAngle = cr::abs (viewAngle - entityAngle);
|
|
|
|
if (absoluteAngle > 180.0f) {
|
|
absoluteAngle = 360.0f - absoluteAngle;
|
|
}
|
|
return absoluteAngle;
|
|
}
|
|
|
|
bool Bot::isInViewCone (const Vector &origin) {
|
|
// this function returns true if the spatial vector location origin is located inside
|
|
// the field of view cone of the bot entity, false otherwise. It is assumed that entities
|
|
// have a human-like field of view, that is, about 90 degrees.
|
|
|
|
return util.isInViewCone (origin, ent ());
|
|
}
|
|
|
|
bool Bot::seesItem (const Vector &destination, const char *itemName) {
|
|
TraceResult tr;
|
|
|
|
// trace a line from bot's eyes to destination..
|
|
game.testLine (getEyesPos (), destination, TraceIgnore::Monsters, ent (), &tr);
|
|
|
|
// check if line of sight to object is not blocked (i.e. visible)
|
|
if (tr.flFraction != 1.0f) {
|
|
return strcmp (STRING (tr.pHit->v.classname), itemName) == 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Bot::seesEntity (const Vector &dest, bool fromBody) {
|
|
TraceResult tr;
|
|
|
|
// trace a line from bot's eyes to destination...
|
|
game.testLine (fromBody ? pev->origin : getEyesPos (), dest, TraceIgnore::Everything, ent (), &tr);
|
|
|
|
// check if line of sight to object is not blocked (i.e. visible)
|
|
return tr.flFraction >= 1.0f;
|
|
}
|
|
|
|
void Bot::checkGrenadesThrow () {
|
|
|
|
// do not check cancel if we have grenade in out hands
|
|
bool checkTasks = getCurrentTaskId () == Task::PlantBomb || getCurrentTaskId () == Task::DefuseBomb;
|
|
|
|
auto clearThrowStates = [] (uint32 &states) {
|
|
states &= ~(Sense::ThrowExplosive | Sense::ThrowFlashbang | Sense::ThrowSmoke);
|
|
};
|
|
|
|
// check if throwing a grenade is a good thing to do...
|
|
if (checkTasks || yb_ignore_enemies.bool_ () || m_isUsingGrenade || m_grenadeRequested || m_isReloading || yb_jasonmode.bool_ () || m_grenadeCheckTime >= game.time ()) {
|
|
clearThrowStates (m_states);
|
|
return;
|
|
}
|
|
|
|
// check again in some seconds
|
|
m_grenadeCheckTime = game.time () + 0.5f;
|
|
|
|
if (!util.isAlive (m_lastEnemy) || !(m_states & (Sense::SuspectEnemy | Sense::HearingEnemy))) {
|
|
clearThrowStates (m_states);
|
|
return;
|
|
}
|
|
|
|
// check if we have grenades to throw
|
|
int grenadeToThrow = bestGrenadeCarried ();
|
|
|
|
// if we don't have grenades no need to check it this round again
|
|
if (grenadeToThrow == -1) {
|
|
m_grenadeCheckTime = game.time () + 15.0f; // changed since, conzero can drop grens from dead players
|
|
|
|
clearThrowStates (m_states);
|
|
return;
|
|
}
|
|
else {
|
|
int cancelProb = 20;
|
|
|
|
if (grenadeToThrow == Weapon::Flashbang) {
|
|
cancelProb = 25;
|
|
}
|
|
else if (grenadeToThrow == Weapon::Smoke) {
|
|
cancelProb = 35;
|
|
}
|
|
if (rg.chance (cancelProb)) {
|
|
clearThrowStates (m_states);
|
|
return;
|
|
}
|
|
}
|
|
float distance = (m_lastEnemyOrigin - pev->origin).length2d ();
|
|
|
|
// don't throw grenades at anything that isn't on the ground!
|
|
if (!(m_lastEnemy->v.flags & FL_ONGROUND) && !m_lastEnemy->v.waterlevel && m_lastEnemyOrigin.z > pev->absmax.z) {
|
|
distance = kInfiniteDistance;
|
|
}
|
|
|
|
// too high to throw?
|
|
if (m_lastEnemy->v.origin.z > pev->origin.z + 500.0f) {
|
|
distance = kInfiniteDistance;
|
|
}
|
|
|
|
// enemy within a good throw distance?
|
|
if (!m_lastEnemyOrigin.empty () && distance > (grenadeToThrow == Weapon::Smoke ? 200.0f : 400.0f) && distance < 1200.0f) {
|
|
bool allowThrowing = true;
|
|
|
|
// care about different grenades
|
|
switch (grenadeToThrow) {
|
|
case Weapon::Explosive:
|
|
if (numFriendsNear (m_lastEnemy->v.origin, 256.0f) > 0) {
|
|
allowThrowing = false;
|
|
}
|
|
else {
|
|
float radius = m_lastEnemy->v.velocity.length2d ();
|
|
const Vector &pos = (m_lastEnemy->v.velocity * 0.5f).get2d () + m_lastEnemy->v.origin;
|
|
|
|
if (radius < 164.0f) {
|
|
radius = 164.0f;
|
|
}
|
|
auto predicted = graph.searchRadius (radius, pos, 12);
|
|
|
|
if (predicted.empty ()) {
|
|
m_states &= ~Sense::ThrowExplosive;
|
|
break;
|
|
}
|
|
|
|
for (const auto predict : predicted) {
|
|
allowThrowing = true;
|
|
|
|
if (!graph.exists (predict)) {
|
|
allowThrowing = false;
|
|
continue;
|
|
}
|
|
|
|
m_throw = graph[predict].origin;
|
|
|
|
auto throwPos = calcThrow (getEyesPos (), m_throw);
|
|
|
|
if (throwPos.lengthSq () < 100.0f) {
|
|
throwPos = calcToss (getEyesPos (), m_throw);
|
|
}
|
|
|
|
if (throwPos.empty ()) {
|
|
allowThrowing = false;
|
|
}
|
|
else {
|
|
m_throw.z += 110.0f;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allowThrowing) {
|
|
m_states |= Sense::ThrowExplosive;
|
|
}
|
|
else {
|
|
m_states &= ~Sense::ThrowExplosive;
|
|
}
|
|
break;
|
|
|
|
case Weapon::Flashbang: {
|
|
int nearest = graph.getNearest ((m_lastEnemy->v.velocity * 0.5f).get2d () + m_lastEnemy->v.origin);
|
|
|
|
if (nearest != kInvalidNodeIndex) {
|
|
m_throw = graph[nearest].origin;
|
|
|
|
if (numFriendsNear (m_throw, 256.0f) > 0) {
|
|
allowThrowing = false;
|
|
}
|
|
}
|
|
else {
|
|
allowThrowing = false;
|
|
}
|
|
|
|
if (allowThrowing) {
|
|
auto throwPos = calcThrow (getEyesPos (), m_throw);
|
|
|
|
if (throwPos.lengthSq () < 100.0f) {
|
|
throwPos = calcToss (getEyesPos (), m_throw);
|
|
}
|
|
|
|
if (throwPos.empty ()) {
|
|
allowThrowing = false;
|
|
}
|
|
else {
|
|
m_throw.z += 110.0f;
|
|
}
|
|
}
|
|
|
|
if (allowThrowing) {
|
|
m_states |= Sense::ThrowFlashbang;
|
|
}
|
|
else {
|
|
m_states &= ~Sense::ThrowFlashbang;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case Weapon::Smoke:
|
|
if (allowThrowing && !game.isNullEntity (m_lastEnemy)) {
|
|
if (util.getShootingCone (m_lastEnemy, pev->origin) >= 0.9f) {
|
|
allowThrowing = false;
|
|
}
|
|
}
|
|
|
|
if (allowThrowing) {
|
|
m_states |= Sense::ThrowSmoke;
|
|
}
|
|
else {
|
|
m_states &= ~Sense::ThrowSmoke;
|
|
}
|
|
break;
|
|
}
|
|
const float MaxThrowTime = game.time () + 0.3f;
|
|
|
|
if (m_states & Sense::ThrowExplosive) {
|
|
startTask (Task::ThrowExplosive, TaskPri::Throw, kInvalidNodeIndex, MaxThrowTime, false);
|
|
}
|
|
else if (m_states & Sense::ThrowFlashbang) {
|
|
startTask (Task::ThrowFlashbang, TaskPri::Throw, kInvalidNodeIndex, MaxThrowTime, false);
|
|
}
|
|
else if (m_states & Sense::ThrowSmoke) {
|
|
startTask (Task::ThrowSmoke, TaskPri::Throw, kInvalidNodeIndex, MaxThrowTime, false);
|
|
}
|
|
}
|
|
else {
|
|
clearThrowStates (m_states);
|
|
}
|
|
}
|
|
|
|
void Bot::avoidGrenades () {
|
|
// checks if bot 'sees' a grenade, and avoid it
|
|
|
|
if (!bots.hasActiveGrenades ()) {
|
|
return;
|
|
}
|
|
|
|
// check if old pointers to grenade is invalid
|
|
if (game.isNullEntity (m_avoidGrenade)) {
|
|
m_avoidGrenade = nullptr;
|
|
m_needAvoidGrenade = 0;
|
|
}
|
|
else if ((m_avoidGrenade->v.flags & FL_ONGROUND) || (m_avoidGrenade->v.effects & EF_NODRAW)) {
|
|
m_avoidGrenade = nullptr;
|
|
m_needAvoidGrenade = 0;
|
|
}
|
|
auto &activeGrenades = bots.searchActiveGrenades ();
|
|
|
|
// find all grenades on the map
|
|
for (auto pent : activeGrenades) {
|
|
if (pent->v.effects & EF_NODRAW) {
|
|
continue;
|
|
}
|
|
|
|
// check if visible to the bot
|
|
if (!seesEntity (pent->v.origin) && isInFOV (pent->v.origin - getEyesPos ()) > pev->fov * 0.5f) {
|
|
continue;
|
|
}
|
|
auto model = STRING (pent->v.model) + 9;
|
|
|
|
if (m_preventFlashing < game.time () && m_personality == Personality::Rusher && m_difficulty == 4 && strcmp (model, "flashbang.mdl") == 0) {
|
|
// don't look at flash bang
|
|
if (!(m_states & Sense::SeeingEnemy)) {
|
|
pev->v_angle.y = cr::normalizeAngles ((game.getEntityWorldOrigin (pent) - getEyesPos ()).angles ().y + 180.0f);
|
|
|
|
m_canChooseAimDirection = false;
|
|
m_preventFlashing = game.time () + rg.float_ (1.0f, 2.0f);
|
|
}
|
|
}
|
|
else if (strcmp (model, "hegrenade.mdl") == 0) {
|
|
if (!game.isNullEntity (m_avoidGrenade)) {
|
|
return;
|
|
}
|
|
|
|
if (game.getTeam (pent->v.owner) == m_team || pent->v.owner == ent ()) {
|
|
return;
|
|
}
|
|
|
|
if (!(pent->v.flags & FL_ONGROUND)) {
|
|
float distance = (pent->v.origin - pev->origin).lengthSq ();
|
|
float distanceMoved = ((pent->v.origin + pent->v.velocity * getFrameInterval ()) - pev->origin).lengthSq ();
|
|
|
|
if (distanceMoved < distance && distance < cr::square (500.0f)) {
|
|
const auto &dirToPoint = (pev->origin - pent->v.origin).normalize2d ();
|
|
const auto &rightSide = pev->v_angle.right ().normalize2d ();
|
|
|
|
if ((dirToPoint | rightSide) > 0.0f) {
|
|
m_needAvoidGrenade = -1;
|
|
}
|
|
else {
|
|
m_needAvoidGrenade = 1;
|
|
}
|
|
m_avoidGrenade = pent;
|
|
}
|
|
}
|
|
}
|
|
else if ((pent->v.flags & FL_ONGROUND) && strcmp (model, "smokegrenade.mdl") == 0) {
|
|
if (isInFOV (pent->v.origin - getEyesPos ()) < pev->fov - 7.0f) {
|
|
float distance = (pent->v.origin - pev->origin).length ();
|
|
|
|
// shrink bot's viewing distance to smoke grenade's distance
|
|
if (m_viewDistance > distance) {
|
|
m_viewDistance = distance;
|
|
|
|
if (rg.chance (45)) {
|
|
pushChatterMessage (Chatter::BehindSmoke);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::checkBreakable (edict_t *touch) {
|
|
if (!game.isShootableBreakable (touch)) {
|
|
return;
|
|
}
|
|
m_breakableEntity = lookupBreakable ();
|
|
|
|
if (game.isNullEntity (m_breakableEntity)) {
|
|
return;
|
|
}
|
|
m_campButtons = pev->button & IN_DUCK;
|
|
startTask (Task::ShootBreakable, TaskPri::ShootBreakable, kInvalidNodeIndex, 0.0f, false);
|
|
}
|
|
|
|
void Bot::checkBreakablesAround () {
|
|
if (!yb_destroy_breakables_around.bool_ () || m_currentWeapon == Weapon::Knife || rg.chance (25) || !game.hasBreakables () || m_seeEnemyTime + 4.0f > game.time () || !game.isNullEntity (m_enemy) || !hasPrimaryWeapon ()) {
|
|
return;
|
|
}
|
|
|
|
// check if we're have some breakbles in 450 units range
|
|
for (const auto &breakable : game.getBreakables ()) {
|
|
if (!game.isShootableBreakable (breakable)) {
|
|
continue;
|
|
}
|
|
const auto &origin = game.getEntityWorldOrigin (breakable);
|
|
|
|
if ((origin - pev->origin).lengthSq () > cr::square (450.0f)) {
|
|
continue;
|
|
}
|
|
|
|
if (isInFOV (origin - getEyesPos ()) < pev->fov && seesEntity (origin)) {
|
|
m_breakableOrigin = origin;
|
|
m_breakableEntity = breakable;
|
|
m_campButtons = pev->button & IN_DUCK;
|
|
|
|
startTask (Task::ShootBreakable, TaskPri::ShootBreakable, kInvalidNodeIndex, 0.0f, false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
edict_t *Bot::lookupBreakable () {
|
|
// this function checks if bot is blocked by a shoot able breakable in his moving direction
|
|
|
|
TraceResult tr;
|
|
game.testLine (pev->origin, pev->origin + (m_destOrigin - pev->origin).normalize () * 72.0f, TraceIgnore::None, ent (), &tr);
|
|
|
|
if (tr.flFraction != 1.0f) {
|
|
auto ent = tr.pHit;
|
|
|
|
// check if this isn't a triggered (bomb) breakable and if it takes damage. if true, shoot the crap!
|
|
if (game.isShootableBreakable (ent)) {
|
|
m_breakableOrigin = game.getEntityWorldOrigin (ent);
|
|
return ent;
|
|
}
|
|
}
|
|
game.testLine (getEyesPos (), getEyesPos () + (m_destOrigin - getEyesPos ()).normalize () * 72.0f, TraceIgnore::None, ent (), &tr);
|
|
|
|
if (tr.flFraction != 1.0f) {
|
|
auto ent = tr.pHit;
|
|
|
|
if (game.isShootableBreakable (ent)) {
|
|
m_breakableOrigin = game.getEntityWorldOrigin (ent);
|
|
return ent;
|
|
}
|
|
}
|
|
m_breakableEntity = nullptr;
|
|
m_breakableOrigin = nullptr;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void Bot::setIdealReactionTimers (bool actual) {
|
|
static struct ReactionTime {
|
|
float min;
|
|
float max;
|
|
} reactionTimers[] = {{0.8f, 1.0f}, {0.4f, 0.6f}, {0.2f, 0.4f}, {0.1f, 0.3f}, {0.0f, 0.1f}};
|
|
|
|
const ReactionTime &reaction = reactionTimers[m_difficulty];
|
|
|
|
if (actual) {
|
|
m_idealReactionTime = reaction.min;
|
|
m_actualReactionTime = reaction.min;
|
|
|
|
return;
|
|
}
|
|
m_idealReactionTime = rg.float_ (reaction.min, reaction.max);
|
|
}
|
|
|
|
void Bot::updatePickups () {
|
|
// this function finds Items to collect or use in the near of a bot
|
|
|
|
// don't try to pickup anything while on ladder or trying to escape from bomb...
|
|
if (isOnLadder () || getCurrentTaskId () == Task::EscapeFromBomb || yb_jasonmode.bool_ () || !bots.hasIntrestingEntities ()) {
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
|
|
return;
|
|
}
|
|
|
|
auto &intresting = bots.searchIntrestingEntities ();
|
|
const float radius = cr::square (320.0f);
|
|
|
|
if (!game.isNullEntity (m_pickupItem)) {
|
|
bool itemExists = false;
|
|
auto pickupItem = m_pickupItem;
|
|
|
|
for (auto &ent : intresting) {
|
|
const Vector &origin = game.getEntityWorldOrigin (ent);
|
|
|
|
// too far from us ?
|
|
if ((pev->origin - origin).lengthSq () > radius) {
|
|
continue;
|
|
}
|
|
|
|
if (ent == pickupItem) {
|
|
if (seesItem (origin, STRING (ent->v.classname))) {
|
|
itemExists = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (itemExists) {
|
|
return;
|
|
}
|
|
else {
|
|
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
}
|
|
}
|
|
|
|
edict_t *pickupItem = nullptr;
|
|
Pickup pickupType = Pickup::None;
|
|
Vector pickupPos = nullptr;
|
|
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
|
|
for (const auto &ent : intresting) {
|
|
bool allowPickup = false; // assume can't use it until known otherwise
|
|
|
|
if (ent == m_itemIgnore) {
|
|
continue; // someone owns this weapon or it hasn't respawned yet
|
|
}
|
|
const Vector &origin = game.getEntityWorldOrigin (ent);
|
|
|
|
// too far from us ?
|
|
if ((pev->origin - origin).lengthSq () > radius) {
|
|
continue;
|
|
}
|
|
|
|
auto classname = STRING (ent->v.classname);
|
|
auto model = STRING (ent->v.model) + 9;
|
|
|
|
// check if line of sight to object is not blocked (i.e. visible)
|
|
if (seesItem (origin, classname)) {
|
|
if (strncmp ("hostage_entity", classname, 14) == 0) {
|
|
allowPickup = true;
|
|
pickupType = Pickup::Hostage;
|
|
}
|
|
else if (strncmp ("weaponbox", classname, 9) == 0 && strcmp (model, "backpack.mdl") == 0) {
|
|
allowPickup = true;
|
|
pickupType = Pickup::DroppedC4;
|
|
}
|
|
else if ((strncmp ("weaponbox", classname, 9) == 0 || strncmp ("armoury_entity", classname, 14) == 0 || strncmp ("csdm", classname, 4) == 0) && !m_isUsingGrenade) {
|
|
allowPickup = true;
|
|
pickupType = Pickup::Weapon;
|
|
}
|
|
else if (strncmp ("weapon_shield", classname, 13) == 0 && !m_isUsingGrenade) {
|
|
allowPickup = true;
|
|
pickupType = Pickup::Shield;
|
|
}
|
|
else if (strncmp ("item_thighpack", classname, 14) == 0 && m_team == Team::CT && !m_hasDefuser) {
|
|
allowPickup = true;
|
|
pickupType = Pickup::DefusalKit;
|
|
}
|
|
else if (strncmp ("grenade", classname, 7) == 0 && strcmp (model, "c4.mdl") == 0) {
|
|
allowPickup = true;
|
|
pickupType = Pickup::PlantedC4;
|
|
}
|
|
}
|
|
|
|
// if the bot found something it can pickup...
|
|
if (allowPickup) {
|
|
|
|
// found weapon on ground?
|
|
if (pickupType == Pickup::Weapon) {
|
|
int primaryWeaponCarried = bestPrimaryCarried ();
|
|
int secondaryWeaponCarried = bestSecondaryCarried ();
|
|
|
|
const auto &config = conf.getWeapons ();
|
|
const auto &primary = config[primaryWeaponCarried];
|
|
const auto &secondary = config[secondaryWeaponCarried];
|
|
const auto &primaryProp = conf.getWeaponProp (primary.id);
|
|
const auto &secondaryProp = conf.getWeaponProp (secondary.id);
|
|
|
|
if (secondaryWeaponCarried < 7 && (m_ammo[secondary.id] > 0.3 * secondaryProp.ammo1Max) && strcmp (model, "w_357ammobox.mdl") == 0) {
|
|
allowPickup = false;
|
|
}
|
|
else if (!m_isVIP && primaryWeaponCarried >= 7 && (m_ammo[primary.id] > 0.3 * primaryProp.ammo1Max) && strncmp (model, "w_", 2) == 0) {
|
|
|
|
const bool isSniperRifle = primaryWeaponCarried == Weapon::AWP || primaryWeaponCarried == Weapon::G3SG1 || primaryWeaponCarried == Weapon::SG550;
|
|
const bool isSubmachine = primaryWeaponCarried == Weapon::MP5 || primaryWeaponCarried == Weapon::TMP || primaryWeaponCarried == Weapon::P90 || primaryWeaponCarried == Weapon::MAC10 || primaryWeaponCarried == Weapon::UMP45;
|
|
const bool isShotgun = primaryWeaponCarried == Weapon::M3;
|
|
const bool isRifle = primaryWeaponCarried == Weapon::Famas || primaryWeaponCarried == Weapon::AK47 || primaryWeaponCarried == Weapon::M4A1 || primaryWeaponCarried == Weapon::Galil || primaryWeaponCarried == Weapon::AUG || primaryWeaponCarried == Weapon::SG552;
|
|
|
|
if (strcmp (model, "w_9mmarclip.mdl") == 0 && !isRifle) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "w_shotbox.mdl") == 0 && !isShotgun) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "w_9mmclip.mdl") == 0 && !isSubmachine) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "w_crossbow_clip.mdl") == 0 && !isSniperRifle) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "w_chainammo.mdl") == 0 && primaryWeaponCarried != Weapon::M249) {
|
|
allowPickup = false;
|
|
}
|
|
}
|
|
else if (m_isVIP || !rateGroundWeapon (ent)) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "medkit.mdl") == 0 && pev->health >= 100.0f) {
|
|
allowPickup = false;
|
|
}
|
|
else if ((strcmp (model, "kevlar.mdl") == 0 || strcmp (model, "battery.mdl") == 0) && pev->armorvalue >= 100.0f) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "flashbang.mdl") == 0 && (pev->weapons & cr::bit (Weapon::Flashbang))) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "hegrenade.mdl") == 0 && (pev->weapons & cr::bit (Weapon::Explosive))) {
|
|
allowPickup = false;
|
|
}
|
|
else if (strcmp (model, "smokegrenade.mdl") == 0 && (pev->weapons & cr::bit (Weapon::Smoke))) {
|
|
allowPickup = false;
|
|
}
|
|
}
|
|
else if (pickupType == Pickup::Shield) // found a shield on ground?
|
|
{
|
|
if ((pev->weapons & cr::bit (Weapon::Elite)) || hasShield () || m_isVIP || (hasPrimaryWeapon () && !rateGroundWeapon (ent))) {
|
|
allowPickup = false;
|
|
}
|
|
}
|
|
else if (m_team == Team::Terrorist) // terrorist team specific
|
|
{
|
|
if (pickupType == Pickup::DroppedC4) {
|
|
allowPickup = true;
|
|
m_destOrigin = origin; // ensure we reached dropped bomb
|
|
|
|
pushChatterMessage (Chatter::FoundC4); // play info about that
|
|
clearSearchNodes ();
|
|
}
|
|
else if (pickupType == Pickup::Hostage) {
|
|
m_itemIgnore = ent;
|
|
allowPickup = false;
|
|
|
|
if (!m_defendHostage && m_difficulty > 2 && rg.chance (30) && m_timeCamping + 15.0f < game.time ()) {
|
|
int index = findDefendNode (origin);
|
|
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.float_ (30.0f, 60.0f), true); // push camp task on to stack
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.float_ (3.0f, 6.0f), true); // push move command
|
|
|
|
if (graph[index].vis.crouch <= graph[index].vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
m_defendHostage = true;
|
|
|
|
pushChatterMessage (Chatter::GoingToGuardHostages); // play info about that
|
|
return;
|
|
}
|
|
}
|
|
else if (pickupType == Pickup::PlantedC4) {
|
|
allowPickup = false;
|
|
|
|
if (!m_defendedBomb) {
|
|
m_defendedBomb = true;
|
|
|
|
int index = findDefendNode (origin);
|
|
const Path &path = graph[index];
|
|
|
|
float bombTimer = mp_c4timer.float_ ();
|
|
float timeMidBlowup = bots.getTimeBombPlanted () + (bombTimer * 0.5f + bombTimer * 0.25f) - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
|
|
|
|
if (timeMidBlowup > game.time ()) {
|
|
clearTask (Task::MoveToPosition); // remove any move tasks
|
|
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, timeMidBlowup, true); // push camp task on to stack
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, timeMidBlowup, true); // push move command
|
|
|
|
if (path.vis.crouch <= path.vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
if (rg.chance (90)) {
|
|
pushChatterMessage (Chatter::DefendingBombsite);
|
|
}
|
|
}
|
|
else {
|
|
pushRadioMessage (Radio::ShesGonnaBlow); // issue an additional radio message
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (m_team == Team::CT) {
|
|
if (pickupType == Pickup::Hostage) {
|
|
if (game.isNullEntity (ent) || ent->v.health <= 0) {
|
|
allowPickup = false; // never pickup dead hostage
|
|
}
|
|
else {
|
|
for (const auto &other : bots) {
|
|
if (other->m_notKilled) {
|
|
for (const auto &hostage : other->m_hostages) {
|
|
if (hostage == ent) {
|
|
allowPickup = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (pickupType == Pickup::PlantedC4) {
|
|
if (util.isAlive (m_enemy)) {
|
|
allowPickup = false;
|
|
return;
|
|
}
|
|
|
|
if (isOutOfBombTimer ()) {
|
|
completeTask ();
|
|
|
|
// then start escape from bomb immediate
|
|
startTask (Task::EscapeFromBomb, TaskPri::EscapeFromBomb, kInvalidNodeIndex, 0.0f, true);
|
|
|
|
// and no pickup
|
|
allowPickup = false;
|
|
return;
|
|
}
|
|
|
|
if (rg.chance (70)) {
|
|
pushChatterMessage (Chatter::FoundC4Plant);
|
|
}
|
|
|
|
allowPickup = !isBombDefusing (origin) || m_hasProgressBar;
|
|
pickupType = Pickup::PlantedC4;
|
|
|
|
if (!m_defendedBomb && !allowPickup) {
|
|
m_defendedBomb = true;
|
|
|
|
int index = findDefendNode (origin);
|
|
const Path &path = graph[index];
|
|
|
|
float timeToExplode = bots.getTimeBombPlanted () + mp_c4timer.float_ () - graph.calculateTravelTime (pev->maxspeed, pev->origin, path.origin);
|
|
|
|
clearTask (Task::MoveToPosition); // remove any move tasks
|
|
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, timeToExplode, true); // push camp task on to stack
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, timeToExplode, true); // push move command
|
|
|
|
if (path.vis.crouch <= path.vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
|
|
if (rg.chance (85)) {
|
|
pushChatterMessage (Chatter::DefendingBombsite);
|
|
}
|
|
}
|
|
}
|
|
else if (pickupType == Pickup::DroppedC4) {
|
|
m_itemIgnore = ent;
|
|
allowPickup = false;
|
|
|
|
if (!m_defendedBomb && m_difficulty > 2 && rg.chance (75) && pev->health < 80) {
|
|
int index = findDefendNode (origin);
|
|
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.float_ (30.0f, 70.0f), true); // push camp task on to stack
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.float_ (10.0f, 30.0f), true); // push move command
|
|
|
|
if (graph[index].vis.crouch <= graph[index].vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
m_defendedBomb = true;
|
|
|
|
pushChatterMessage (Chatter::GoingToGuardDroppedC4); // play info about that
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// if condition valid
|
|
if (allowPickup) {
|
|
pickupPos = origin; // remember location of entity
|
|
pickupItem = ent; // remember this entity
|
|
|
|
m_pickupType = pickupType;
|
|
break;
|
|
}
|
|
else {
|
|
pickupType = Pickup::None;
|
|
}
|
|
}
|
|
} // end of the while loop
|
|
|
|
if (!game.isNullEntity (pickupItem)) {
|
|
for (const auto &other : bots) {
|
|
if (other->m_notKilled && other->m_pickupItem == pickupItem) {
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// check if item is too high to reach, check if getting the item would hurt bot
|
|
if (pickupPos.z > getEyesPos ().z + (m_pickupType == Pickup::Hostage ? 50.0f : 20.0f) || isDeadlyMove (pickupPos)) {
|
|
m_itemIgnore = m_pickupItem;
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
|
|
return;
|
|
}
|
|
m_pickupItem = pickupItem; // save pointer of picking up entity
|
|
}
|
|
}
|
|
|
|
void Bot::getCampDirection (Vector *dest) {
|
|
// this function check if view on last enemy position is blocked - replace with better vector then
|
|
// mostly used for getting a good camping direction vector if not camping on a camp waypoint
|
|
|
|
TraceResult tr;
|
|
const Vector &src = getEyesPos ();
|
|
|
|
game.testLine (src, *dest, TraceIgnore::Monsters, ent (), &tr);
|
|
|
|
// check if the trace hit something...
|
|
if (tr.flFraction < 1.0f) {
|
|
float length = (tr.vecEndPos - src).lengthSq ();
|
|
|
|
if (length > 10000.0f) {
|
|
return;
|
|
}
|
|
|
|
int enemyIndex = graph.getNearest (*dest);
|
|
int tempIndex = graph.getNearest (pev->origin);
|
|
|
|
if (tempIndex == kInvalidNodeIndex || enemyIndex == kInvalidNodeIndex) {
|
|
return;
|
|
}
|
|
float minDistance = kInfiniteDistance;
|
|
|
|
int lookAtWaypoint = kInvalidNodeIndex;
|
|
const Path &path = graph[tempIndex];
|
|
|
|
for (auto &link : path.links) {
|
|
if (link.index == kInvalidNodeIndex) {
|
|
continue;
|
|
}
|
|
auto distance = static_cast <float> (graph.getPathDist (link.index, enemyIndex));
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
lookAtWaypoint = link.index;
|
|
}
|
|
}
|
|
|
|
if (graph.exists (lookAtWaypoint)) {
|
|
*dest = graph[lookAtWaypoint].origin;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::showChaterIcon (bool show) {
|
|
// this function depending on show boolen, shows/remove chatter, icon, on the head of bot.
|
|
|
|
if (!game.is (GameFlags::HasBotVoice) || yb_radio_mode.int_ () != 2) {
|
|
return;
|
|
}
|
|
|
|
auto sendBotVoice = [](bool show, edict_t *ent, int ownId) {
|
|
MessageWriter (MSG_ONE, msgs.id (NetMsg::BotVoice), nullptr, ent) // begin message
|
|
.writeByte (show) // switch on/off
|
|
.writeByte (ownId);
|
|
};
|
|
|
|
int ownIndex = index ();
|
|
|
|
for (auto &client : util.getClients ()) {
|
|
if (!(client.flags & ClientFlags::Used) || (client.ent->v.flags & FL_FAKECLIENT) || client.team != m_team) {
|
|
continue;
|
|
}
|
|
|
|
if (!show && (client.iconFlags[ownIndex] & ClientFlags::Icon) && client.iconTimestamp[ownIndex] < game.time ()) {
|
|
sendBotVoice (false, client.ent, entindex ());
|
|
|
|
client.iconTimestamp[ownIndex] = 0.0f;
|
|
client.iconFlags[ownIndex] &= ~ClientFlags::Icon;
|
|
}
|
|
else if (show && !(client.iconFlags[ownIndex] & ClientFlags::Icon)) {
|
|
sendBotVoice (true, client.ent, entindex ());
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::instantChatter (int type) {
|
|
// this function sends instant chatter messages.
|
|
if (!game.is (GameFlags::HasBotVoice) || yb_radio_mode.int_ () != 2 || !conf.hasChatterBank (type) || !conf.hasChatterBank (Chatter::DiePain)) {
|
|
return;
|
|
}
|
|
|
|
auto playbackSound = conf.pickRandomFromChatterBank (type);
|
|
auto painSound = conf.pickRandomFromChatterBank (Chatter::DiePain);
|
|
|
|
if (m_notKilled) {
|
|
showChaterIcon (true);
|
|
}
|
|
MessageWriter msg;
|
|
int ownIndex = index ();
|
|
|
|
for (auto &client : util.getClients ()) {
|
|
if (!(client.flags & ClientFlags::Used) || (client.ent->v.flags & FL_FAKECLIENT) || client.team != m_team) {
|
|
continue;
|
|
}
|
|
msg.start (MSG_ONE, msgs.id (NetMsg::SendAudio), nullptr, client.ent); // begin message
|
|
msg.writeByte (ownIndex);
|
|
|
|
if (pev->deadflag & DEAD_DYING) {
|
|
client.iconTimestamp[ownIndex] = game.time () + painSound.duration;
|
|
msg.writeString (strings.format ("%s/%s.wav", yb_chatter_path.str (), painSound.name.chars ()));
|
|
}
|
|
else if (!(pev->deadflag & DEAD_DEAD)) {
|
|
client.iconTimestamp[ownIndex] = game.time () + playbackSound.duration;
|
|
msg.writeString (strings.format ("%s/%s.wav", yb_chatter_path.str (), playbackSound.name.chars ()));
|
|
}
|
|
msg.writeShort (m_voicePitch).end ();
|
|
client.iconFlags[ownIndex] |= ClientFlags::Icon;
|
|
}
|
|
}
|
|
|
|
void Bot::pushRadioMessage (int message) {
|
|
// this function inserts the radio message into the message queue
|
|
|
|
if (yb_radio_mode.int_ () == 0 || m_numFriendsLeft == 0) {
|
|
return;
|
|
}
|
|
m_forceRadio = !game.is (GameFlags::HasBotVoice) || !conf.hasChatterBank (message) || yb_radio_mode.int_ () != 2; // use radio instead voice
|
|
|
|
m_radioSelect = message;
|
|
pushMsgQueue (BotMsg::Radio);
|
|
}
|
|
|
|
void Bot::pushChatterMessage (int message) {
|
|
// this function inserts the voice message into the message queue (mostly same as above)
|
|
|
|
if (!game.is (GameFlags::HasBotVoice) || yb_radio_mode.int_ () != 2 || !conf.hasChatterBank (message) || m_numFriendsLeft == 0) {
|
|
return;
|
|
}
|
|
bool sendMessage = false;
|
|
|
|
const float messageRepeat = conf.getChatterMessageRepeatInterval (message);
|
|
float &messageTimer = m_chatterTimes[message];
|
|
|
|
if (messageTimer < game.time () || cr::fequal (messageTimer, kMaxChatterRepeatInteval)) {
|
|
if (!cr::fequal (messageTimer, kMaxChatterRepeatInteval) && !cr::fequal (messageRepeat, kMaxChatterRepeatInteval)) {
|
|
messageTimer = game.time () + messageRepeat;
|
|
}
|
|
sendMessage = true;
|
|
}
|
|
|
|
if (!sendMessage) {
|
|
m_radioSelect = -1;
|
|
return;
|
|
}
|
|
m_radioSelect = message;
|
|
pushMsgQueue (BotMsg::Radio);
|
|
}
|
|
|
|
void Bot::checkMsgQueue () {
|
|
// this function checks and executes pending messages
|
|
|
|
extern ConVar mp_freezetime;
|
|
|
|
// no new message?
|
|
if (m_actMessageIndex == m_pushMessageIndex) {
|
|
return;
|
|
}
|
|
// get message from stack
|
|
int state = getMsgQueue ();
|
|
|
|
// nothing to do?
|
|
if (state == BotMsg::None || (state == BotMsg::Radio && game.is (GameFlags::FreeForAll))) {
|
|
return;
|
|
}
|
|
|
|
switch (state) {
|
|
case BotMsg::Buy: // general buy message
|
|
|
|
// buy weapon
|
|
if (m_nextBuyTime > game.time ()) {
|
|
// keep sending message
|
|
pushMsgQueue (BotMsg::Buy);
|
|
return;
|
|
}
|
|
|
|
if (!m_inBuyZone || game.is (GameFlags::CSDM)) {
|
|
m_buyPending = true;
|
|
m_buyingFinished = true;
|
|
|
|
break;
|
|
}
|
|
|
|
m_buyPending = false;
|
|
m_nextBuyTime = game.time () + rg.float_ (0.5f, 1.3f);
|
|
|
|
// if freezetime is very low do not delay the buy process
|
|
if (mp_freezetime.float_ () <= 1.0f) {
|
|
m_nextBuyTime = game.time ();
|
|
m_ignoreBuyDelay = true;
|
|
}
|
|
|
|
// if bot buying is off then no need to buy
|
|
if (!yb_botbuy.bool_ ()) {
|
|
m_buyState = BuyState::Done;
|
|
}
|
|
|
|
// if fun-mode no need to buy
|
|
if (yb_jasonmode.bool_ ()) {
|
|
m_buyState = BuyState::Done;
|
|
selectWeaponByName ("weapon_knife");
|
|
}
|
|
|
|
// prevent vip from buying
|
|
if (m_isVIP) {
|
|
m_buyState = BuyState::Done;
|
|
m_pathType = FindPath::Fast;
|
|
}
|
|
|
|
// prevent terrorists from buying on es maps
|
|
if (game.mapIs (MapFlags::Escape) && m_team == Team::Terrorist) {
|
|
m_buyState = 6;
|
|
}
|
|
|
|
// prevent teams from buying on fun maps
|
|
if (game.mapIs (MapFlags::KnifeArena | MapFlags::Fun)) {
|
|
m_buyState = BuyState::Done;
|
|
|
|
if (game.mapIs (MapFlags::KnifeArena)) {
|
|
yb_jasonmode.set (1);
|
|
}
|
|
}
|
|
|
|
if (m_buyState > BuyState::Done - 1) {
|
|
m_buyingFinished = true;
|
|
return;
|
|
}
|
|
|
|
pushMsgQueue (BotMsg::None);
|
|
buyStuff ();
|
|
|
|
break;
|
|
|
|
case BotMsg::Radio:
|
|
// if last bot radio command (global) happened just a 3 seconds ago, delay response
|
|
if (bots.getLastRadioTimestamp (m_team) + 3.0f < game.time ()) {
|
|
// if same message like previous just do a yes/no
|
|
if (m_radioSelect != Radio::RogerThat && m_radioSelect != Radio::Negative) {
|
|
if (m_radioSelect == bots.getLastRadio (m_team) && bots.getLastRadioTimestamp (m_team) + 1.5f > game.time ()) {
|
|
m_radioSelect = -1;
|
|
}
|
|
else {
|
|
if (m_radioSelect != Radio::ReportingIn) {
|
|
bots.setLastRadio (m_team, m_radioSelect);
|
|
}
|
|
else {
|
|
bots.setLastRadio (m_team, -1);
|
|
}
|
|
|
|
for (const auto &bot : bots) {
|
|
if (pev != bot->pev && bot->m_team == m_team) {
|
|
bot->m_radioOrder = m_radioSelect;
|
|
bot->m_radioEntity = ent ();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_radioSelect != -1) {
|
|
if ((m_radioSelect != Radio::ReportingIn && m_forceRadio) || yb_radio_mode.int_ () != 2 || !conf.hasChatterBank (m_radioSelect) || !game.is (GameFlags::HasBotVoice)) {
|
|
if (m_radioSelect < Radio::GoGoGo) {
|
|
game.botCommand (ent (), "radio1");
|
|
}
|
|
else if (m_radioSelect < Radio::RogerThat) {
|
|
m_radioSelect -= Radio::GoGoGo - 1;
|
|
game.botCommand (ent (), "radio2");
|
|
}
|
|
else {
|
|
m_radioSelect -= Radio::RogerThat - 1;
|
|
game.botCommand (ent (), "radio3");
|
|
}
|
|
|
|
// select correct menu item for this radio message
|
|
game.botCommand (ent (), "menuselect %d", m_radioSelect);
|
|
}
|
|
else if (m_radioSelect != Radio::ReportingIn) {
|
|
instantChatter (m_radioSelect);
|
|
}
|
|
}
|
|
m_forceRadio = false; // reset radio to voice
|
|
bots.setLastRadioTimestamp (m_team, game.time ()); // store last radio usage
|
|
}
|
|
else {
|
|
pushMsgQueue (BotMsg::Radio);
|
|
}
|
|
break;
|
|
|
|
// team independent saytext
|
|
case BotMsg::Say:
|
|
say (m_chatBuffer.chars ());
|
|
break;
|
|
|
|
// team dependent saytext
|
|
case BotMsg::SayTeam:
|
|
sayTeam (m_chatBuffer.chars ());
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
bool Bot::isWeaponRestricted (int weaponIndex) {
|
|
// this function checks for weapon restrictions.
|
|
|
|
if (strings.isEmpty (yb_restricted_weapons.str ())) {
|
|
return isWeaponRestrictedAMX (weaponIndex); // no banned weapons
|
|
}
|
|
auto bannedWeapons = String (yb_restricted_weapons.str ()).split (";");
|
|
|
|
for (auto &ban : bannedWeapons) {
|
|
const char *banned = STRING (util.getWeaponAlias (true, nullptr, weaponIndex));
|
|
|
|
// check is this weapon is banned
|
|
if (strncmp (ban.chars (), banned, ban.length ()) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return isWeaponRestrictedAMX (weaponIndex);
|
|
}
|
|
|
|
bool Bot::isWeaponRestrictedAMX (int weaponIndex) {
|
|
// this function checks restriction set by AMX Mod, this function code is courtesy of KWo.
|
|
|
|
// check for weapon restrictions
|
|
if (cr::bit (weaponIndex) & (kPrimaryWeaponMask | kSecondaryWeaponMask | Weapon::Shield)) {
|
|
const char *restrictedWeapons = engfuncs.pfnCVarGetString ("amx_restrweapons");
|
|
|
|
if (strings.isEmpty (restrictedWeapons)) {
|
|
return false;
|
|
}
|
|
int indices[] = {4, 25, 20, -1, 8, -1, 12, 19, -1, 5, 6, 13, 23, 17, 18, 1, 2, 21, 9, 24, 7, 16, 10, 22, -1, 3, 15, 14, 0, 11};
|
|
|
|
// find the weapon index
|
|
int index = indices[weaponIndex - 1];
|
|
|
|
// validate index range
|
|
if (index < 0 || index >= static_cast <int> (strlen (restrictedWeapons))) {
|
|
return false;
|
|
}
|
|
return restrictedWeapons[index] != '0';
|
|
}
|
|
|
|
// check for equipment restrictions
|
|
else {
|
|
const char *restrictedEquipment = engfuncs.pfnCVarGetString ("amx_restrequipammo");
|
|
|
|
if (strings.isEmpty (restrictedEquipment)) {
|
|
return false;
|
|
}
|
|
int indices[] = {-1, -1, -1, 3, -1, -1, -1, -1, 4, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 2, -1, -1, -1, -1, -1, 0, 1, 5};
|
|
|
|
// find the weapon index
|
|
int index = indices[weaponIndex - 1];
|
|
|
|
// validate index range
|
|
if (index < 0 || index >= static_cast <int> (strlen (restrictedEquipment))) {
|
|
return false;
|
|
}
|
|
return restrictedEquipment[index] != '0';
|
|
}
|
|
}
|
|
|
|
bool Bot::canReplaceWeapon () {
|
|
// this function determines currently owned primary weapon, and checks if bot has
|
|
// enough money to buy more powerful weapon.
|
|
|
|
auto tab = conf.getRawWeapons ();
|
|
|
|
// if bot is not rich enough or non-standard weapon mode enabled return false
|
|
if (tab[25].teamStandard != 1 || m_moneyAmount < 4000) {
|
|
return false;
|
|
}
|
|
|
|
if (!strings.isEmpty (yb_restricted_weapons.str ())) {
|
|
auto bannedWeapons = String (yb_restricted_weapons.str ()).split (";");
|
|
|
|
// check if its banned
|
|
for (auto &ban : bannedWeapons) {
|
|
if (m_currentWeapon == util.getWeaponAlias (false, ban.chars ())) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_currentWeapon == Weapon::Scout && m_moneyAmount > 5000) {
|
|
return true;
|
|
}
|
|
else if (m_currentWeapon == Weapon::MP5 && m_moneyAmount > 6000) {
|
|
return true;
|
|
}
|
|
else if ((m_currentWeapon == Weapon::M3 || m_currentWeapon == Weapon::XM1014) && m_moneyAmount > 4000) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int Bot::pickBestWeapon (int *vec, int count, int moneySave) {
|
|
// this function picks best available weapon from random choice with money save
|
|
|
|
bool needMoreRandomWeapon = (m_personality == Personality::Careful) || (rg.chance (25) && m_personality == Personality::Normal);
|
|
|
|
if (needMoreRandomWeapon) {
|
|
auto pick = [] (const float factor) -> float {
|
|
union {
|
|
unsigned int u;
|
|
float f;
|
|
} cast;
|
|
cast.f = factor;
|
|
|
|
return (static_cast <int> ((cast.u >> 23) & 0xff) - 127) * 0.3010299956639812f;
|
|
};
|
|
|
|
float buyFactor = (m_moneyAmount - static_cast <float> (moneySave)) / (16000.0f - static_cast <float> (moneySave)) * 3.0f;
|
|
|
|
if (buyFactor < 1.0f) {
|
|
buyFactor = 1.0f;
|
|
}
|
|
|
|
// swap array values
|
|
for (int *begin = vec, *end = vec + count - 1; begin < end; ++begin, --end) {
|
|
cr::swap (*end, *begin);
|
|
}
|
|
return vec[static_cast <int> (static_cast <float> (count - 1) * pick (rg.float_ (1.0f, cr::powf (10.0f, buyFactor))) / buyFactor + 0.5f)];
|
|
}
|
|
|
|
int chance = 95;
|
|
|
|
// high skilled bots almost always prefer best weapon
|
|
if (m_difficulty < 4) {
|
|
if (m_personality == Personality::Normal) {
|
|
chance = 50;
|
|
}
|
|
else if (m_personality == Personality::Careful) {
|
|
chance = 75;
|
|
}
|
|
}
|
|
auto &info = conf.getWeapons ();
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
auto &weapon = info[vec[i]];
|
|
|
|
// if wea have enough money for weapon buy it
|
|
if (weapon.price + moneySave < m_moneyAmount + rg.int_ (50, 200) && rg.chance (chance)) {
|
|
return vec[i];
|
|
}
|
|
}
|
|
return vec[rg.int_ (0, count - 1)];
|
|
}
|
|
|
|
void Bot::buyStuff () {
|
|
// this function does all the work in selecting correct buy menus for most weapons/items
|
|
|
|
WeaponInfo *selectedWeapon = nullptr;
|
|
m_nextBuyTime = game.time ();
|
|
|
|
if (!m_ignoreBuyDelay) {
|
|
m_nextBuyTime += rg.float_ (0.3f, 0.5f);
|
|
}
|
|
|
|
int count = 0, weaponCount = 0;
|
|
int choices[kNumWeapons];
|
|
|
|
// select the priority tab for this personality
|
|
const int *pref = conf.getWeaponPrefs (m_personality) + kNumWeapons;
|
|
auto tab = conf.getRawWeapons ();
|
|
|
|
const bool isPistolMode = tab[25].teamStandard == -1 && tab[3].teamStandard == 2;
|
|
const bool teamEcoValid = bots.checkTeamEco (m_team);
|
|
|
|
// do this, because xash engine is not capable to run all the features goldsrc, but we have cs 1.6 on it, so buy table must be the same
|
|
const bool isOldGame = game.is (GameFlags::Legacy) && !game.is (GameFlags::Xash3D);
|
|
|
|
const bool hasDefaultPistols = (pev->weapons & (cr::bit (Weapon::USP) | cr::bit (Weapon::Glock18)));
|
|
const bool isFirstRound = m_moneyAmount == mp_startmoney.int_ ();
|
|
|
|
switch (m_buyState) {
|
|
case BuyState::PrimaryWeapon: // if no primary weapon and bot has some money, buy a primary weapon
|
|
if ((!hasShield () && !hasPrimaryWeapon () && teamEcoValid) || (teamEcoValid && canReplaceWeapon ())) {
|
|
int moneySave = 0;
|
|
|
|
do {
|
|
bool ignoreWeapon = false;
|
|
|
|
pref--;
|
|
|
|
assert (*pref > -1);
|
|
assert (*pref < kNumWeapons);
|
|
|
|
selectedWeapon = &tab[*pref];
|
|
++count;
|
|
|
|
if (selectedWeapon->buyGroup == 1) {
|
|
continue;
|
|
}
|
|
|
|
// weapon available for every team?
|
|
if (game.mapIs (MapFlags::Assassination) && selectedWeapon->teamAS != 2 && selectedWeapon->teamAS != m_team) {
|
|
continue;
|
|
}
|
|
|
|
// ignore weapon if this weapon not supported by currently running cs version...
|
|
if (isOldGame && selectedWeapon->buySelect == -1) {
|
|
continue;
|
|
}
|
|
|
|
// ignore weapon if this weapon is not targeted to out team....
|
|
if (selectedWeapon->teamStandard != 2 && selectedWeapon->teamStandard != m_team) {
|
|
continue;
|
|
}
|
|
|
|
// ignore weapon if this weapon is restricted
|
|
if (isWeaponRestricted (selectedWeapon->id)) {
|
|
continue;
|
|
}
|
|
|
|
const int *limit = conf.getEconLimit ();
|
|
int prostock = 0;
|
|
|
|
// filter out weapons with bot economics
|
|
switch (m_personality) {
|
|
case Personality::Rusher:
|
|
prostock = limit[EcoLimit::ProstockRusher];
|
|
break;
|
|
|
|
case Personality::Careful:
|
|
prostock = limit[EcoLimit::ProstockCareful];
|
|
break;
|
|
|
|
case Personality::Normal:
|
|
default:
|
|
prostock = limit[EcoLimit::ProstockNormal];
|
|
break;
|
|
}
|
|
|
|
if (m_team == Team::CT) {
|
|
switch (selectedWeapon->id) {
|
|
case Weapon::TMP:
|
|
case Weapon::UMP45:
|
|
case Weapon::P90:
|
|
case Weapon::MP5:
|
|
if (m_moneyAmount > limit[EcoLimit::SmgCTGreater] + prostock) {
|
|
ignoreWeapon = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (selectedWeapon->id == Weapon::Shield && m_moneyAmount > limit[EcoLimit::ShieldGreater]) {
|
|
ignoreWeapon = true;
|
|
}
|
|
}
|
|
else if (m_team == Team::Terrorist) {
|
|
switch (selectedWeapon->id) {
|
|
case Weapon::UMP45:
|
|
case Weapon::MAC10:
|
|
case Weapon::P90:
|
|
case Weapon::MP5:
|
|
case Weapon::Scout:
|
|
if (m_moneyAmount > limit[EcoLimit::SmgTEGreater] + prostock) {
|
|
ignoreWeapon = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (selectedWeapon->id) {
|
|
case Weapon::XM1014:
|
|
case Weapon::M3:
|
|
if (m_moneyAmount < limit[EcoLimit::ShotgunLess]) {
|
|
ignoreWeapon = true;
|
|
}
|
|
|
|
if (m_moneyAmount >= limit[EcoLimit::ShotgunGreater]) {
|
|
ignoreWeapon = false;
|
|
|
|
}
|
|
break;
|
|
}
|
|
|
|
switch (selectedWeapon->id) {
|
|
case Weapon::SG550:
|
|
case Weapon::G3SG1:
|
|
case Weapon::AWP:
|
|
case Weapon::M249:
|
|
if (m_moneyAmount < limit[EcoLimit::HeavyLess]) {
|
|
ignoreWeapon = true;
|
|
|
|
}
|
|
|
|
if (m_moneyAmount >= limit[EcoLimit::HeavyGreater]) {
|
|
ignoreWeapon = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (ignoreWeapon && tab[25].teamStandard == 1 && yb_economics_rounds.bool_ ()) {
|
|
continue;
|
|
}
|
|
|
|
// save money for grenade for example?
|
|
moneySave = rg.int_ (500, 1000);
|
|
|
|
if (bots.getLastWinner () == m_team) {
|
|
moneySave = 0;
|
|
}
|
|
|
|
if (selectedWeapon->price <= (m_moneyAmount - moneySave)) {
|
|
choices[weaponCount++] = *pref;
|
|
}
|
|
|
|
} while (count < kNumWeapons && weaponCount < 4);
|
|
|
|
// found a desired weapon?
|
|
if (weaponCount > 0) {
|
|
int chosenWeapon;
|
|
|
|
// choose randomly from the best ones...
|
|
if (weaponCount > 1) {
|
|
chosenWeapon = pickBestWeapon (choices, weaponCount, moneySave);
|
|
}
|
|
else {
|
|
chosenWeapon = choices[weaponCount - 1];
|
|
}
|
|
selectedWeapon = &tab[chosenWeapon];
|
|
}
|
|
else {
|
|
selectedWeapon = nullptr;
|
|
}
|
|
|
|
if (selectedWeapon != nullptr) {
|
|
game.botCommand (ent (), "buy;menuselect %d", selectedWeapon->buyGroup);
|
|
|
|
if (isOldGame) {
|
|
game.botCommand (ent (), "menuselect %d", selectedWeapon->buySelect);
|
|
}
|
|
else {
|
|
if (m_team == Team::Terrorist) {
|
|
game.botCommand (ent (), "menuselect %d", selectedWeapon->buySelectT);
|
|
}
|
|
else {
|
|
game.botCommand (ent (), "menuselect %d", selectedWeapon->buySelectCT);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (hasPrimaryWeapon () && !hasShield ()) {
|
|
m_reloadState = Reload::Primary;
|
|
break;
|
|
}
|
|
else if ((hasSecondaryWeapon () && !hasShield ()) || hasShield ()) {
|
|
m_reloadState = Reload::Secondary;
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case BuyState::ArmorVestHelm: // if armor is damaged and bot has some money, buy some armor
|
|
if (pev->armorvalue < rg.int_ (50, 80) && (isPistolMode || (teamEcoValid && hasPrimaryWeapon ()))) {
|
|
// if bot is rich, buy kevlar + helmet, else buy a single kevlar
|
|
if (m_moneyAmount > 1500 && !isWeaponRestricted (Weapon::ArmorHelm)) {
|
|
game.botCommand (ent (), "buyequip;menuselect 2");
|
|
}
|
|
else if (!isWeaponRestricted (Weapon::Armor)) {
|
|
game.botCommand (ent (), "buyequip;menuselect 1");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BuyState::SecondaryWeapon: // if bot has still some money, buy a better secondary weapon
|
|
if (isPistolMode || (isFirstRound && hasDefaultPistols) || (hasDefaultPistols && bots.getLastWinner () != m_team) || (hasPrimaryWeapon () && hasDefaultPistols && m_moneyAmount > rg.int_ (7500, 9000))) {
|
|
do {
|
|
pref--;
|
|
|
|
assert (*pref > -1);
|
|
assert (*pref < kNumWeapons);
|
|
|
|
selectedWeapon = &tab[*pref];
|
|
++count;
|
|
|
|
if (selectedWeapon->buyGroup != 1) {
|
|
continue;
|
|
}
|
|
|
|
// ignore weapon if this weapon is restricted
|
|
if (isWeaponRestricted (selectedWeapon->id)) {
|
|
continue;
|
|
}
|
|
|
|
// weapon available for every team?
|
|
if (game.mapIs (MapFlags::Assassination) && selectedWeapon->teamAS != 2 && selectedWeapon->teamAS != m_team) {
|
|
continue;
|
|
}
|
|
|
|
if (isOldGame && selectedWeapon->buySelect == -1) {
|
|
continue;
|
|
}
|
|
|
|
if (selectedWeapon->teamStandard != 2 && selectedWeapon->teamStandard != m_team) {
|
|
continue;
|
|
}
|
|
|
|
if (selectedWeapon->price <= (m_moneyAmount - rg.int_ (100, 200))) {
|
|
choices[weaponCount++] = *pref;
|
|
}
|
|
|
|
} while (count < kNumWeapons && weaponCount < 4);
|
|
|
|
// found a desired weapon?
|
|
if (weaponCount > 0) {
|
|
int chosenWeapon;
|
|
|
|
// choose randomly from the best ones...
|
|
if (weaponCount > 1) {
|
|
chosenWeapon = pickBestWeapon (choices, weaponCount, rg.int_ (100, 200));
|
|
}
|
|
else {
|
|
chosenWeapon = choices[weaponCount - 1];
|
|
}
|
|
selectedWeapon = &tab[chosenWeapon];
|
|
}
|
|
else {
|
|
selectedWeapon = nullptr;
|
|
}
|
|
|
|
if (selectedWeapon != nullptr) {
|
|
game.botCommand (ent (), "buy;menuselect %d", selectedWeapon->buyGroup);
|
|
|
|
if (isOldGame) {
|
|
game.botCommand (ent (), "menuselect %d", selectedWeapon->buySelect);
|
|
}
|
|
else {
|
|
if (m_team == Team::Terrorist) {
|
|
game.botCommand (ent (), "menuselect %d", selectedWeapon->buySelectT);
|
|
}
|
|
else {
|
|
game.botCommand (ent (), "menuselect %d", selectedWeapon->buySelectCT);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BuyState::Grenades: // if bot has still some money, choose if bot should buy a grenade or not
|
|
|
|
// buy a he grenade
|
|
if (conf.chanceToBuyGrenade (0) && m_moneyAmount >= 400 && !isWeaponRestricted (Weapon::Explosive)) {
|
|
game.botCommand (ent (), "buyequip");
|
|
game.botCommand (ent (), "menuselect 4");
|
|
}
|
|
|
|
// buy a concussion grenade, i.e., 'flashbang'
|
|
if (conf.chanceToBuyGrenade (1) && m_moneyAmount >= 300 && teamEcoValid && !isWeaponRestricted (Weapon::Flashbang)) {
|
|
game.botCommand (ent (), "buyequip");
|
|
game.botCommand (ent (), "menuselect 3");
|
|
}
|
|
|
|
// buy a smoke grenade
|
|
if (conf.chanceToBuyGrenade (2) && m_moneyAmount >= 400 && teamEcoValid && !isWeaponRestricted (Weapon::Smoke)) {
|
|
game.botCommand (ent (), "buyequip");
|
|
game.botCommand (ent (), "menuselect 5");
|
|
}
|
|
break;
|
|
|
|
case BuyState::DefusalKit: // if bot is CT and we're on a bomb map, randomly buy the defuse kit
|
|
if (game.mapIs (MapFlags::Demolition) && m_team == Team::CT && rg.chance (80) && m_moneyAmount > 200 && !isWeaponRestricted (Weapon::Defuser)) {
|
|
if (isOldGame) {
|
|
game.botCommand (ent (), "buyequip;menuselect 6");
|
|
}
|
|
else {
|
|
game.botCommand (ent (), "defuser"); // use alias in steamcs
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BuyState::NightVision:
|
|
if (m_moneyAmount > 2500 && !m_hasNVG && rg.chance (30) && m_path) {
|
|
float skyColor = illum.getSkyColor ();
|
|
float lightLevel = m_path->light;
|
|
|
|
// if it's somewhat darkm do buy nightvision goggles
|
|
if ((skyColor >= 50.0f && lightLevel <= 15.0f) || (skyColor < 50.0f && lightLevel < 40.0f)) {
|
|
if (isOldGame) {
|
|
game.botCommand (ent (), "buyequip;menuselect 7");
|
|
}
|
|
else {
|
|
game.botCommand (ent (), "nvgs"); // use alias in steamcs
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case BuyState::Ammo: // buy enough primary & secondary ammo (do not check for money here)
|
|
for (int i = 0; i <= 5; ++i) {
|
|
game.botCommand (ent (), "buyammo%d", rg.int_ (1, 2)); // simulate human
|
|
}
|
|
|
|
// buy enough secondary ammo
|
|
if (hasPrimaryWeapon ()) {
|
|
game.botCommand (ent (), "buy;menuselect 7");
|
|
}
|
|
|
|
// buy enough primary ammo
|
|
game.botCommand (ent (), "buy;menuselect 6");
|
|
|
|
// try to reload secondary weapon
|
|
if (m_reloadState != Reload::Primary) {
|
|
m_reloadState = Reload::Secondary;
|
|
}
|
|
m_ignoreBuyDelay = false;
|
|
break;
|
|
}
|
|
|
|
++m_buyState;
|
|
pushMsgQueue (BotMsg::Buy);
|
|
}
|
|
|
|
void Bot::updateEmotions () {
|
|
// slowly increase/decrease dynamic emotions back to their base level
|
|
if (m_nextEmotionUpdate > game.time ()) {
|
|
return;
|
|
}
|
|
|
|
if (m_agressionLevel > m_baseAgressionLevel) {
|
|
m_agressionLevel -= 0.10f;
|
|
}
|
|
else {
|
|
m_agressionLevel += 0.10f;
|
|
}
|
|
|
|
if (m_fearLevel > m_baseFearLevel) {
|
|
m_fearLevel -= 0.05f;
|
|
}
|
|
else {
|
|
m_fearLevel += 0.05f;
|
|
}
|
|
|
|
if (m_agressionLevel < 0.0f) {
|
|
m_agressionLevel = 0.0f;
|
|
}
|
|
|
|
if (m_fearLevel < 0.0f) {
|
|
m_fearLevel = 0.0f;
|
|
}
|
|
m_nextEmotionUpdate = game.time () + 1.0f;
|
|
}
|
|
|
|
void Bot::overrideConditions () {
|
|
|
|
if (m_currentWeapon != Weapon::Knife && m_difficulty > 2 && ((m_aimFlags & AimFlags::Enemy) || (m_states & Sense::SeeingEnemy)) && !yb_jasonmode.bool_ () && getCurrentTaskId () != Task::Camp && getCurrentTaskId () != Task::SeekCover && !isOnLadder ()) {
|
|
m_moveToGoal = false; // don't move to goal
|
|
m_navTimeset = game.time ();
|
|
|
|
if (util.isPlayer (m_enemy)) {
|
|
attackMovement ();
|
|
}
|
|
}
|
|
|
|
// check if we need to escape from bomb
|
|
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_notKilled && getCurrentTaskId () != Task::EscapeFromBomb && getCurrentTaskId () != Task::Camp && isOutOfBombTimer ()) {
|
|
completeTask (); // complete current task
|
|
|
|
// then start escape from bomb immediate
|
|
startTask (Task::EscapeFromBomb, TaskPri::EscapeFromBomb, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
|
|
// special handling, if we have a knife in our hands
|
|
if ((bots.getRoundStartTime () + 6.0f > game.time () || !hasAnyWeapons ()) && m_currentWeapon == Weapon::Knife && util.isPlayer (m_enemy)) {
|
|
float length = (pev->origin - m_enemy->v.origin).length2d ();
|
|
|
|
// do waypoint movement if enemy is not reachable with a knife
|
|
if (length > 100.0f && (m_states & Sense::SeeingEnemy)) {
|
|
int nearestToEnemyPoint = graph.getNearest (m_enemy->v.origin);
|
|
|
|
if (nearestToEnemyPoint != kInvalidNodeIndex && nearestToEnemyPoint != m_currentNodeIndex && cr::abs (graph[nearestToEnemyPoint].origin.z - m_enemy->v.origin.z) < 16.0f) {
|
|
float taskTime = game.time () + length / pev->maxspeed * 0.5f;
|
|
|
|
if (getCurrentTaskId () != Task::MoveToPosition && getTask ()->desire != TaskPri::Hide) {
|
|
startTask (Task::MoveToPosition, TaskPri::Hide, nearestToEnemyPoint, taskTime, true);
|
|
}
|
|
m_isEnemyReachable = false;
|
|
m_enemy = nullptr;
|
|
|
|
m_enemyIgnoreTimer = taskTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
// special handling for sniping
|
|
if (usesSniper () && (m_states & (Sense::SeeingEnemy | Sense::SuspectEnemy)) && m_sniperStopTime > game.time () && getCurrentTaskId () != Task::SeekCover) {
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
m_navTimeset = game.time ();
|
|
}
|
|
|
|
// special handling for reloading
|
|
if (m_reloadState != Reload::None && m_isReloading && ((pev->button | m_oldButtons) & IN_RELOAD)) {
|
|
if (m_seeEnemyTime + 4.0f < game.time () && (m_states & Sense::SuspectEnemy)) {
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
m_navTimeset = game.time ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::setConditions () {
|
|
// this function carried out each frame. does all of the sensing, calculates emotions and finally sets the desired
|
|
// action after applying all of the Filters
|
|
|
|
m_aimFlags = 0;
|
|
updateEmotions ();
|
|
|
|
// does bot see an enemy?
|
|
trackEnemies ();
|
|
|
|
// did bot just kill an enemy?
|
|
if (!game.isNullEntity (m_lastVictim)) {
|
|
if (game.getTeam (m_lastVictim) != m_team) {
|
|
// add some aggression because we just killed somebody
|
|
m_agressionLevel += 0.1f;
|
|
|
|
if (m_agressionLevel > 1.0f) {
|
|
m_agressionLevel = 1.0f;
|
|
}
|
|
|
|
if (rg.chance (10)) {
|
|
pushChatMessage (Chat::Kill);
|
|
}
|
|
|
|
if (rg.chance (10)) {
|
|
pushRadioMessage (Radio::EnemyDown);
|
|
}
|
|
else if (rg.chance (60)) {
|
|
if ((m_lastVictim->v.weapons & cr::bit (Weapon::AWP)) || (m_lastVictim->v.weapons & cr::bit (Weapon::Scout)) || (m_lastVictim->v.weapons & cr::bit (Weapon::G3SG1)) || (m_lastVictim->v.weapons & cr::bit (Weapon::SG550))) {
|
|
pushChatterMessage (Chatter::SniperKilled);
|
|
}
|
|
else {
|
|
switch (numEnemiesNear (pev->origin, kInfiniteDistance)) {
|
|
case 0:
|
|
if (rg.chance (50)) {
|
|
pushChatterMessage (Chatter::NoEnemiesLeft);
|
|
}
|
|
else {
|
|
pushChatterMessage (Chatter::EnemyDown);
|
|
}
|
|
break;
|
|
|
|
case 1:
|
|
pushChatterMessage (Chatter::OneEnemyLeft);
|
|
break;
|
|
|
|
case 2:
|
|
pushChatterMessage (Chatter::TwoEnemiesLeft);
|
|
break;
|
|
|
|
case 3:
|
|
pushChatterMessage (Chatter::ThreeEnemiesLeft);
|
|
break;
|
|
|
|
default:
|
|
pushChatterMessage (Chatter::EnemyDown);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if no more enemies found AND bomb planted, switch to knife to get to bombplace faster
|
|
if (m_team == Team::CT && m_currentWeapon != Weapon::Knife && m_numEnemiesLeft == 0 && bots.isBombPlanted ()) {
|
|
selectWeaponByName ("weapon_knife");
|
|
m_plantedBombNodeIndex = getNearestToPlantedBomb ();
|
|
|
|
if (isOccupiedNode (m_plantedBombNodeIndex)) {
|
|
pushChatterMessage (Chatter::BombsiteSecured);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
pushChatMessage (Chat::TeamKill, true);
|
|
pushChatterMessage (Chatter::FriendlyFire);
|
|
}
|
|
m_lastVictim = nullptr;
|
|
}
|
|
|
|
// check if our current enemy is still valid
|
|
if (!game.isNullEntity (m_lastEnemy)) {
|
|
if (!util.isAlive (m_lastEnemy) && m_shootAtDeadTime < game.time ()) {
|
|
m_lastEnemyOrigin = nullptr;
|
|
m_lastEnemy = nullptr;
|
|
}
|
|
}
|
|
else {
|
|
m_lastEnemyOrigin = nullptr;
|
|
m_lastEnemy = nullptr;
|
|
}
|
|
|
|
// don't listen if seeing enemy, just checked for sounds or being blinded (because its inhuman)
|
|
if (!yb_ignore_enemies.bool_ () && m_soundUpdateTime < game.time () && m_blindTime < game.time () && m_seeEnemyTime + 1.0f < game.time ()) {
|
|
updateHearing ();
|
|
m_soundUpdateTime = game.time () + 0.25f;
|
|
}
|
|
else if (m_heardSoundTime < game.time ()) {
|
|
m_states &= ~Sense::HearingEnemy;
|
|
}
|
|
|
|
if (game.isNullEntity (m_enemy) && !game.isNullEntity (m_lastEnemy) && !m_lastEnemyOrigin.empty ()) {
|
|
m_aimFlags |= AimFlags::PredictPath;
|
|
|
|
if (seesEntity (m_lastEnemyOrigin, true)) {
|
|
m_aimFlags |= AimFlags::LastEnemy;
|
|
}
|
|
}
|
|
|
|
// check for grenades depending on difficulty
|
|
if (rg.chance (cr::max (25, m_difficulty * 25))) {
|
|
checkGrenadesThrow ();
|
|
}
|
|
|
|
// check if there are items needing to be used/collected
|
|
if (m_itemCheckTime < game.time () || !game.isNullEntity (m_pickupItem)) {
|
|
|
|
updatePickups ();
|
|
m_itemCheckTime = game.time () + 0.5f;
|
|
}
|
|
filterTasks ();
|
|
}
|
|
|
|
void Bot::filterTasks () {
|
|
// initialize & calculate the desire for all actions based on distances, emotions and other stuff
|
|
getTask ();
|
|
|
|
float tempFear = m_fearLevel;
|
|
float tempAgression = m_agressionLevel;
|
|
|
|
// decrease fear if teammates near
|
|
int friendlyNum = 0;
|
|
|
|
if (!m_lastEnemyOrigin.empty ()) {
|
|
friendlyNum = numFriendsNear (pev->origin, 500.0f) - numEnemiesNear (m_lastEnemyOrigin, 500.0f);
|
|
}
|
|
|
|
if (friendlyNum > 0) {
|
|
tempFear = tempFear * 0.5f;
|
|
}
|
|
|
|
// increase/decrease fear/aggression if bot uses a sniping weapon to be more careful
|
|
if (usesSniper ()) {
|
|
tempFear = tempFear * 1.2f;
|
|
tempAgression = tempAgression * 0.6f;
|
|
}
|
|
auto &filter = bots.getFilters ();
|
|
|
|
// bot found some item to use?
|
|
if (!game.isNullEntity (m_pickupItem) && getCurrentTaskId () != Task::EscapeFromBomb) {
|
|
m_states |= Sense::PickupItem;
|
|
|
|
if (m_pickupType == Pickup::Button) {
|
|
filter[Task::PickupItem].desire = 50.0f; // always pickup button
|
|
}
|
|
else {
|
|
float distance = (500.0f - (game.getEntityWorldOrigin (m_pickupItem) - pev->origin).length ()) * 0.2f;
|
|
|
|
if (distance > 50.0f) {
|
|
distance = 50.0f;
|
|
}
|
|
filter[Task::PickupItem].desire = distance;
|
|
}
|
|
}
|
|
else {
|
|
m_states &= ~Sense::PickupItem;
|
|
filter[Task::PickupItem].desire = 0.0f;
|
|
}
|
|
|
|
// calculate desire to attack
|
|
if ((m_states & Sense::SeeingEnemy) && reactOnEnemy ()) {
|
|
filter[Task::Attack].desire = TaskPri::Attack;
|
|
}
|
|
else {
|
|
filter[Task::Attack].desire = 0.0f;
|
|
}
|
|
float &seekCoverDesire = filter[Task::SeekCover].desire;
|
|
float &huntEnemyDesire = filter[Task::Hunt].desire;
|
|
float &blindedDesire = filter[Task::Blind].desire;
|
|
|
|
// calculate desires to seek cover or hunt
|
|
if (util.isPlayer (m_lastEnemy) && !m_lastEnemyOrigin.empty () && !m_hasC4) {
|
|
float retreatLevel = (100.0f - (pev->health > 50.0f ? 100.0f : pev->health)) * tempFear; // retreat level depends on bot health
|
|
|
|
if (m_numEnemiesLeft > m_numFriendsLeft * 0.5f && m_retreatTime < game.time () && m_seeEnemyTime - rg.float_ (2.0f, 4.0f) < game.time ()) {
|
|
|
|
float timeSeen = m_seeEnemyTime - game.time ();
|
|
float timeHeard = m_heardSoundTime - game.time ();
|
|
float ratio = 0.0f;
|
|
|
|
m_retreatTime = game.time () + rg.float_ (3.0f, 15.0f);
|
|
|
|
if (timeSeen > timeHeard) {
|
|
timeSeen += 10.0f;
|
|
ratio = timeSeen * 0.1f;
|
|
}
|
|
else {
|
|
timeHeard += 10.0f;
|
|
ratio = timeHeard * 0.1f;
|
|
}
|
|
bool lowAmmo = m_ammoInClip[m_currentWeapon] < conf.findWeaponById (m_currentWeapon).maxClip * 0.18f;
|
|
|
|
if (bots.isBombPlanted () || m_isStuck || m_currentWeapon == Weapon::Knife) {
|
|
ratio /= 3.0f; // reduce the seek cover desire if bomb is planted
|
|
}
|
|
else if (m_isVIP || m_isReloading || (lowAmmo && usesSniper ())) {
|
|
ratio *= 3.0f; // triple the seek cover desire if bot is VIP or reloading
|
|
}
|
|
else {
|
|
ratio /= 2.0f; // reduce seek cover otherwise
|
|
}
|
|
seekCoverDesire = retreatLevel * ratio;
|
|
}
|
|
else {
|
|
seekCoverDesire = 0.0f;
|
|
}
|
|
|
|
// if half of the round is over, allow hunting
|
|
if (getCurrentTaskId () != Task::EscapeFromBomb && game.isNullEntity (m_enemy) && bots.getRoundMidTime () < game.time () && !m_isUsingGrenade && m_currentNodeIndex != graph.getNearest (m_lastEnemyOrigin) && m_personality != Personality::Careful && !yb_ignore_enemies.bool_ ()) {
|
|
float desireLevel = 4096.0f - ((1.0f - tempAgression) * (m_lastEnemyOrigin - pev->origin).length ());
|
|
|
|
desireLevel = (100.0f * desireLevel) / 4096.0f;
|
|
desireLevel -= retreatLevel;
|
|
|
|
if (desireLevel > 89.0f) {
|
|
desireLevel = 89.0f;
|
|
}
|
|
huntEnemyDesire = desireLevel;
|
|
}
|
|
else {
|
|
huntEnemyDesire = 0.0f;
|
|
}
|
|
}
|
|
else {
|
|
huntEnemyDesire = 0.0f;
|
|
seekCoverDesire = 0.0f;
|
|
}
|
|
|
|
// blinded behavior
|
|
blindedDesire = m_blindTime > game.time () ? TaskPri::Blind : 0.0f;
|
|
|
|
// now we've initialized all the desires go through the hard work
|
|
// of filtering all actions against each other to pick the most
|
|
// rewarding one to the bot.
|
|
|
|
// FIXME: instead of going through all of the actions it might be
|
|
// better to use some kind of decision tree to sort out impossible
|
|
// actions.
|
|
|
|
// most of the values were found out by trial-and-error and a helper
|
|
// utility i wrote so there could still be some weird behaviors, it's
|
|
// hard to check them all out.
|
|
|
|
// this function returns the behavior having the higher activation level
|
|
auto maxDesire = [] (BotTask *first, BotTask *second) {
|
|
if (first->desire > second->desire) {
|
|
return first;
|
|
}
|
|
return second;
|
|
};
|
|
|
|
// this function returns the first behavior if its activation level is anything higher than zero
|
|
auto subsumeDesire = [] (BotTask *first, BotTask *second) {
|
|
if (first->desire > 0) {
|
|
return first;
|
|
}
|
|
return second;
|
|
};
|
|
|
|
// this function returns the input behavior if it's activation level exceeds the threshold, or some default behavior otherwise
|
|
auto thresholdDesire = [] (BotTask *first, float threshold, float desire) {
|
|
if (first->desire < threshold) {
|
|
first->desire = desire;
|
|
}
|
|
return first;
|
|
};
|
|
|
|
// this function clamp the inputs to be the last known value outside the [min, max] range.
|
|
auto hysteresisDesire = [] (float cur, float min, float max, float old) {
|
|
if (cur <= min || cur >= max) {
|
|
old = cur;
|
|
}
|
|
return old;
|
|
};
|
|
|
|
m_oldCombatDesire = hysteresisDesire (filter[Task::Attack].desire, 40.0f, 90.0f, m_oldCombatDesire);
|
|
filter[Task::Attack].desire = m_oldCombatDesire;
|
|
|
|
auto offensive = &filter[Task::Attack];
|
|
auto pickup = &filter[Task::PickupItem];
|
|
|
|
// calc survive (cover/hide)
|
|
auto survive = thresholdDesire (&filter[Task::SeekCover], 40.0f, 0.0f);
|
|
survive = subsumeDesire (&filter[Task::Hide], survive);
|
|
|
|
auto def = thresholdDesire (&filter[Task::Hunt], 41.0f, 0.0f); // don't allow hunting if desires 60<
|
|
offensive = subsumeDesire (offensive, pickup); // if offensive task, don't allow picking up stuff
|
|
|
|
auto sub = maxDesire (offensive, def); // default normal & careful tasks against offensive actions
|
|
auto final = subsumeDesire (&filter[Task::Blind], maxDesire (survive, sub)); // reason about fleeing instead
|
|
|
|
if (!m_tasks.empty ()) {
|
|
final = maxDesire (final, getTask ());
|
|
startTask (final->id, final->desire, final->data, final->time, final->resume); // push the final behavior in our task stack to carry out
|
|
}
|
|
}
|
|
|
|
void Bot::clearTasks () {
|
|
// this function resets bot tasks stack, by removing all entries from the stack.
|
|
|
|
m_tasks.clear ();
|
|
}
|
|
|
|
void Bot::startTask (Task id, float desire, int data, float time, bool resume) {
|
|
for (auto &task : m_tasks) {
|
|
if (task.id == id) {
|
|
if (!cr::fequal (task.desire, desire)) {
|
|
task.desire = desire;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
m_tasks.emplace (id, desire, data, time, resume);
|
|
|
|
clearSearchNodes ();
|
|
ignoreCollision ();
|
|
|
|
int tid = getCurrentTaskId ();
|
|
|
|
// leader bot?
|
|
if (m_isLeader && tid == Task::SeekCover) {
|
|
updateTeamCommands (); // reorganize team if fleeing
|
|
}
|
|
|
|
if (tid == Task::Camp) {
|
|
selectBestWeapon ();
|
|
}
|
|
|
|
// this is best place to handle some voice commands report team some info
|
|
if (rg.chance (90)) {
|
|
if (tid == Task::Blind) {
|
|
pushChatterMessage (Chatter::Blind);
|
|
}
|
|
else if (tid == Task::PlantBomb) {
|
|
pushChatterMessage (Chatter::PlantingBomb);
|
|
}
|
|
}
|
|
|
|
if (rg.chance (25) && tid == Task::Camp) {
|
|
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) {
|
|
pushChatterMessage (Chatter::GuardingDroppedC4);
|
|
}
|
|
else {
|
|
pushChatterMessage (Chatter::GoingToCamp);
|
|
}
|
|
}
|
|
|
|
if (yb_debug_goal.int_ () != kInvalidNodeIndex) {
|
|
m_chosenGoalIndex = yb_debug_goal.int_ ();
|
|
}
|
|
else {
|
|
m_chosenGoalIndex = getTask ()->data;
|
|
}
|
|
|
|
if (rg.chance (75) && tid == Task::Camp && m_team == Team::Terrorist && m_inVIPZone) {
|
|
pushChatterMessage (Chatter::GoingToGuardVIPSafety);
|
|
}
|
|
}
|
|
|
|
BotTask *Bot::getTask () {
|
|
if (m_tasks.empty ()) {
|
|
m_tasks.emplace (Task::Normal, TaskPri::Normal, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
return &m_tasks.last ();
|
|
}
|
|
|
|
void Bot::clearTask (Task id) {
|
|
// this function removes one task from the bot task stack.
|
|
|
|
if (m_tasks.empty () || getCurrentTaskId () == Task::Normal) {
|
|
return; // since normal task can be only once on the stack, don't remove it...
|
|
}
|
|
|
|
if (getCurrentTaskId () == id) {
|
|
clearSearchNodes ();
|
|
ignoreCollision ();
|
|
|
|
m_tasks.pop ();
|
|
return;
|
|
}
|
|
|
|
for (auto &task : m_tasks) {
|
|
if (task.id == id) {
|
|
m_tasks.remove (task);
|
|
}
|
|
}
|
|
|
|
ignoreCollision ();
|
|
clearSearchNodes ();
|
|
}
|
|
|
|
void Bot::completeTask () {
|
|
// this function called whenever a task is completed.
|
|
|
|
ignoreCollision ();
|
|
|
|
if (m_tasks.empty ()) {
|
|
return;
|
|
}
|
|
|
|
do {
|
|
m_tasks.pop ();
|
|
} while (!m_tasks.empty () && !m_tasks.last ().resume);
|
|
|
|
clearSearchNodes ();
|
|
}
|
|
|
|
bool Bot::isEnemyThreat () {
|
|
if (game.isNullEntity (m_enemy) || getCurrentTaskId () == Task::SeekCover) {
|
|
return false;
|
|
}
|
|
|
|
// if bot is camping, he should be firing anyway and not leaving his position
|
|
if (getCurrentTaskId () == Task::Camp) {
|
|
return false;
|
|
}
|
|
|
|
// if enemy is near or facing us directly
|
|
if ((m_enemy->v.origin - pev->origin).lengthSq () < cr::square (256.0f) || isInViewCone (m_enemy->v.origin)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Bot::reactOnEnemy () {
|
|
// the purpose of this function is check if task has to be interrupted because an enemy is near (run attack actions then)
|
|
|
|
if (!isEnemyThreat ()) {
|
|
return false;
|
|
}
|
|
|
|
if (m_enemyReachableTimer < game.time ()) {
|
|
int ownIndex = m_currentNodeIndex;
|
|
|
|
if (ownIndex == kInvalidNodeIndex) {
|
|
ownIndex = findNearestNode ();
|
|
}
|
|
int enemyIndex = graph.getNearest (m_enemy->v.origin);
|
|
|
|
auto lineDist = (m_enemy->v.origin - pev->origin).length ();
|
|
auto pathDist = static_cast <float> (graph.getPathDist (ownIndex, enemyIndex));
|
|
|
|
if (pathDist - lineDist > 112.0f) {
|
|
m_isEnemyReachable = false;
|
|
}
|
|
else {
|
|
m_isEnemyReachable = true;
|
|
}
|
|
m_enemyReachableTimer = game.time () + 1.0f;
|
|
}
|
|
|
|
if (m_isEnemyReachable) {
|
|
m_navTimeset = game.time (); // override existing movement by attack movement
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Bot::lastEnemyShootable () {
|
|
// don't allow shooting through walls
|
|
if (!(m_aimFlags & AimFlags::LastEnemy) || m_lastEnemyOrigin.empty () || game.isNullEntity (m_lastEnemy)) {
|
|
return false;
|
|
}
|
|
return util.getShootingCone (ent (), m_lastEnemyOrigin) >= 0.90f && isPenetrableObstacle (m_lastEnemyOrigin);
|
|
}
|
|
|
|
void Bot::checkRadioQueue () {
|
|
// this function handling radio and reacting to it
|
|
|
|
|
|
// don't allow bot listen you if bot is busy
|
|
if (getCurrentTaskId () == Task::DefuseBomb || getCurrentTaskId () == Task::PlantBomb || hasHostage () || m_hasC4) {
|
|
m_radioOrder = 0;
|
|
return;
|
|
}
|
|
float distance = (m_radioEntity->v.origin - pev->origin).length ();
|
|
|
|
switch (m_radioOrder) {
|
|
case Radio::CoverMe:
|
|
case Radio::FollowMe:
|
|
case Radio::StickTogetherTeam:
|
|
case Chatter::GoingToPlantBomb:
|
|
case Chatter::CoverMe:
|
|
// check if line of sight to object is not blocked (i.e. visible)
|
|
if (seesEntity (m_radioEntity->v.origin) || m_radioOrder == Radio::StickTogetherTeam) {
|
|
if (game.isNullEntity (m_targetEntity) && game.isNullEntity (m_enemy) && rg.chance (m_personality == Personality::Careful ? 80 : 20)) {
|
|
int numFollowers = 0;
|
|
|
|
// check if no more followers are allowed
|
|
for (const auto &bot : bots) {
|
|
if (bot->m_notKilled) {
|
|
if (bot->m_targetEntity == m_radioEntity) {
|
|
++numFollowers;
|
|
}
|
|
}
|
|
}
|
|
int allowedFollowers = yb_user_max_followers.int_ ();
|
|
|
|
if (m_radioEntity->v.weapons & cr::bit (Weapon::C4)) {
|
|
allowedFollowers = 1;
|
|
}
|
|
|
|
if (numFollowers < allowedFollowers) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
m_targetEntity = m_radioEntity;
|
|
|
|
// don't pause/camp/follow anymore
|
|
Task taskID = getCurrentTaskId ();
|
|
|
|
if (taskID == Task::Pause || taskID == Task::Camp) {
|
|
getTask ()->time = game.time ();
|
|
}
|
|
startTask (Task::FollowUser, TaskPri::FollowUser, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
else if (numFollowers > allowedFollowers) {
|
|
for (int i = 0; (i < game.maxClients () && numFollowers > allowedFollowers); ++i) {
|
|
auto bot = bots[i];
|
|
|
|
if (bot != nullptr) {
|
|
if (bot->m_notKilled) {
|
|
if (bot->m_targetEntity == m_radioEntity) {
|
|
bot->m_targetEntity = nullptr;
|
|
numFollowers--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (m_radioOrder != Chatter::GoingToPlantBomb && rg.chance (15)) {
|
|
pushRadioMessage (Radio::Negative);
|
|
}
|
|
}
|
|
else if (m_radioOrder != Chatter::GoingToPlantBomb && rg.chance (25)) {
|
|
pushRadioMessage (Radio::Negative);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Radio::HoldThisPosition:
|
|
if (!game.isNullEntity (m_targetEntity)) {
|
|
if (m_targetEntity == m_radioEntity) {
|
|
m_targetEntity = nullptr;
|
|
pushRadioMessage (Radio::RogerThat);
|
|
|
|
m_campButtons = 0;
|
|
startTask (Task::Pause, TaskPri::Pause, kInvalidNodeIndex, game.time () + rg.float_ (30.0f, 60.0f), false);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Chatter::NewRound:
|
|
pushChatterMessage (Chatter::YouHeardTheMan);
|
|
break;
|
|
|
|
case Radio::TakingFireNeedAssistance:
|
|
if (game.isNullEntity (m_targetEntity)) {
|
|
if (game.isNullEntity (m_enemy) && m_seeEnemyTime + 4.0f < game.time ()) {
|
|
// decrease fear levels to lower probability of bot seeking cover again
|
|
m_fearLevel -= 0.2f;
|
|
|
|
if (m_fearLevel < 0.0f) {
|
|
m_fearLevel = 0.0f;
|
|
}
|
|
|
|
if (rg.chance (45) && yb_radio_mode.int_ () == 2) {
|
|
pushChatterMessage (Chatter::OnMyWay);
|
|
}
|
|
else if (m_radioOrder == Radio::NeedBackup && yb_radio_mode.int_ () != 2) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
tryHeadTowardRadioMessage ();
|
|
}
|
|
else if (rg.chance (25)) {
|
|
pushRadioMessage (Radio::Negative);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Radio::YouTakeThePoint:
|
|
if (seesEntity (m_radioEntity->v.origin) && m_isLeader) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
break;
|
|
|
|
case Radio::EnemySpotted:
|
|
case Radio::NeedBackup:
|
|
case Chatter::ScaredEmotion:
|
|
case Chatter::PinnedDown:
|
|
if (((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 2048.0f || !m_moveToC4) && rg.chance (50) && m_seeEnemyTime + 4.0f < game.time ()) {
|
|
m_fearLevel -= 0.1f;
|
|
|
|
if (m_fearLevel < 0.0f) {
|
|
m_fearLevel = 0.0f;
|
|
}
|
|
|
|
if (rg.chance (45) && yb_radio_mode.int_ () == 2) {
|
|
pushChatterMessage (Chatter::OnMyWay);
|
|
}
|
|
else if (m_radioOrder == Radio::NeedBackup && yb_radio_mode.int_ () != 2 && rg.chance (50)) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
tryHeadTowardRadioMessage ();
|
|
}
|
|
else if (rg.chance (30) && m_radioOrder == Radio::NeedBackup) {
|
|
pushRadioMessage (Radio::Negative);
|
|
}
|
|
break;
|
|
|
|
case Radio::GoGoGo:
|
|
if (m_radioEntity == m_targetEntity) {
|
|
if (rg.chance (45) && yb_radio_mode.int_ () == 2) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
else if (m_radioOrder == Radio::NeedBackup && yb_radio_mode.int_ () != 2) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
|
|
m_targetEntity = nullptr;
|
|
m_fearLevel -= 0.2f;
|
|
|
|
if (m_fearLevel < 0.0f) {
|
|
m_fearLevel = 0.0f;
|
|
}
|
|
}
|
|
else if ((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 2048.0f) {
|
|
Task taskID = getCurrentTaskId ();
|
|
|
|
if (taskID == Task::Pause || taskID == Task::Camp) {
|
|
m_fearLevel -= 0.2f;
|
|
|
|
if (m_fearLevel < 0.0f) {
|
|
m_fearLevel = 0.0f;
|
|
}
|
|
|
|
pushRadioMessage (Radio::RogerThat);
|
|
// don't pause/camp anymore
|
|
getTask ()->time = game.time ();
|
|
|
|
m_targetEntity = nullptr;
|
|
m_position = m_radioEntity->v.origin + m_radioEntity->v.v_angle.forward () * rg.float_ (1024.0f, 2048.0f);
|
|
|
|
clearSearchNodes ();
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
}
|
|
else if (!game.isNullEntity (m_doubleJumpEntity)) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
resetDoubleJump ();
|
|
}
|
|
else if (rg.chance (35)) {
|
|
pushRadioMessage (Radio::Negative);
|
|
}
|
|
break;
|
|
|
|
case Radio::ShesGonnaBlow:
|
|
if (game.isNullEntity (m_enemy) && distance < 2048.0f && bots.isBombPlanted () && m_team == Team::Terrorist) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
|
|
if (getCurrentTaskId () == Task::Camp) {
|
|
clearTask (Task::Camp);
|
|
}
|
|
m_targetEntity = nullptr;
|
|
startTask (Task::EscapeFromBomb, TaskPri::EscapeFromBomb, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
else if (rg.chance (35)) {
|
|
pushRadioMessage (Radio::Negative);
|
|
}
|
|
break;
|
|
|
|
case Radio::RegroupTeam:
|
|
// if no more enemies found AND bomb planted, switch to knife to get to bombplace faster
|
|
if (m_team == Team::CT && m_currentWeapon != Weapon::Knife && m_numEnemiesLeft == 0 && bots.isBombPlanted () && getCurrentTaskId () != Task::DefuseBomb) {
|
|
selectWeaponByName ("weapon_knife");
|
|
|
|
clearSearchNodes ();
|
|
|
|
m_position = graph.getBombOrigin ();
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
|
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
break;
|
|
|
|
case Radio::StormTheFront:
|
|
if (((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f) && rg.chance (50)) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
|
|
// don't pause/camp anymore
|
|
Task taskID = getCurrentTaskId ();
|
|
|
|
if (taskID == Task::Pause || taskID == Task::Camp) {
|
|
getTask ()->time = game.time ();
|
|
}
|
|
m_targetEntity = nullptr;
|
|
m_position = m_radioEntity->v.origin + m_radioEntity->v.v_angle.forward () * rg.float_ (1024.0f, 2048.0f);
|
|
|
|
clearSearchNodes ();
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
|
|
|
m_fearLevel -= 0.3f;
|
|
|
|
if (m_fearLevel < 0.0f) {
|
|
m_fearLevel = 0.0f;
|
|
}
|
|
m_agressionLevel += 0.3f;
|
|
|
|
if (m_agressionLevel > 1.0f) {
|
|
m_agressionLevel = 1.0f;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Radio::TeamFallback:
|
|
if ((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f) {
|
|
m_fearLevel += 0.5f;
|
|
|
|
if (m_fearLevel > 1.0f) {
|
|
m_fearLevel = 1.0f;
|
|
}
|
|
m_agressionLevel -= 0.5f;
|
|
|
|
if (m_agressionLevel < 0.0f) {
|
|
m_agressionLevel = 0.0f;
|
|
}
|
|
if (getCurrentTaskId () == Task::Camp) {
|
|
getTask ()->time += rg.float_ (10.0f, 15.0f);
|
|
}
|
|
else {
|
|
// don't pause/camp anymore
|
|
Task taskID = getCurrentTaskId ();
|
|
|
|
if (taskID == Task::Pause) {
|
|
getTask ()->time = game.time ();
|
|
}
|
|
m_targetEntity = nullptr;
|
|
m_seeEnemyTime = game.time ();
|
|
|
|
// if bot has no enemy
|
|
if (m_lastEnemyOrigin.empty ()) {
|
|
float nearestDistance = kInfiniteDistance;
|
|
|
|
// take nearest enemy to ordering player
|
|
for (const auto &client : util.getClients ()) {
|
|
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team == m_team) {
|
|
continue;
|
|
}
|
|
|
|
auto enemy = client.ent;
|
|
float curDist = (m_radioEntity->v.origin - enemy->v.origin).lengthSq ();
|
|
|
|
if (curDist < nearestDistance) {
|
|
nearestDistance = curDist;
|
|
|
|
m_lastEnemy = enemy;
|
|
m_lastEnemyOrigin = enemy->v.origin;
|
|
}
|
|
}
|
|
}
|
|
clearSearchNodes ();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Radio::ReportInTeam:
|
|
switch (getCurrentTaskId ()) {
|
|
case Task::Normal:
|
|
if (getTask ()->data != kInvalidNodeIndex && rg.chance (70)) {
|
|
const Path &path = graph[getTask ()->data];
|
|
|
|
if (path.flags & NodeFlag::Goal) {
|
|
if (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && m_hasC4) {
|
|
pushChatterMessage (Chatter::GoingToPlantBomb);
|
|
}
|
|
else {
|
|
pushChatterMessage (Chatter::Nothing);
|
|
}
|
|
}
|
|
else if (path.flags & NodeFlag::Rescue) {
|
|
pushChatterMessage (Chatter::RescuingHostages);
|
|
}
|
|
else if ((path.flags & NodeFlag::Camp) && rg.chance (75)) {
|
|
pushChatterMessage (Chatter::GoingToCamp);
|
|
}
|
|
else {
|
|
pushChatterMessage (Chatter::HeardNoise);
|
|
}
|
|
}
|
|
else if (rg.chance (30)) {
|
|
pushChatterMessage (Chatter::ReportingIn);
|
|
}
|
|
break;
|
|
|
|
case Task::MoveToPosition:
|
|
if (rg.chance (2)) {
|
|
pushChatterMessage (Chatter::GoingToCamp);
|
|
}
|
|
break;
|
|
|
|
case Task::Camp:
|
|
if (rg.chance (40)) {
|
|
if (bots.isBombPlanted () && m_team == Team::Terrorist) {
|
|
pushChatterMessage (Chatter::GuardingDroppedC4);
|
|
}
|
|
else if (m_inVIPZone && m_team == Team::Terrorist) {
|
|
pushChatterMessage (Chatter::GuardingVIPSafety);
|
|
}
|
|
else {
|
|
pushChatterMessage (Chatter::Camping);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Task::PlantBomb:
|
|
pushChatterMessage (Chatter::PlantingBomb);
|
|
break;
|
|
|
|
case Task::DefuseBomb:
|
|
pushChatterMessage (Chatter::DefusingBomb);
|
|
break;
|
|
|
|
case Task::Attack:
|
|
pushChatterMessage (Chatter::InCombat);
|
|
break;
|
|
|
|
case Task::Hide:
|
|
case Task::SeekCover:
|
|
pushChatterMessage (Chatter::SeekingEnemies);
|
|
break;
|
|
|
|
default:
|
|
if (rg.chance (50)) {
|
|
pushChatterMessage (Chatter::Nothing);
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case Radio::SectorClear:
|
|
// is bomb planted and it's a ct
|
|
if (!bots.isBombPlanted ()) {
|
|
break;
|
|
}
|
|
|
|
// check if it's a ct command
|
|
if (game.getTeam (m_radioEntity) == Team::CT && m_team == Team::CT && util.isFakeClient (m_radioEntity) && bots.getPlantedBombSearchTimestamp () < game.time ()) {
|
|
float minDistance = kInfiniteDistance;
|
|
int bombPoint = kInvalidNodeIndex;
|
|
|
|
// find nearest bomb waypoint to player
|
|
for (auto &point : graph.m_goalPoints) {
|
|
distance = (graph[point].origin - m_radioEntity->v.origin).lengthSq ();
|
|
|
|
if (distance < minDistance) {
|
|
minDistance = distance;
|
|
bombPoint = point;
|
|
}
|
|
}
|
|
|
|
// mark this waypoint as restricted point
|
|
if (bombPoint != kInvalidNodeIndex && !graph.isVisited (bombPoint)) {
|
|
// does this bot want to defuse?
|
|
if (getCurrentTaskId () == Task::Normal) {
|
|
// is he approaching this goal?
|
|
if (getTask ()->data == bombPoint) {
|
|
getTask ()->data = kInvalidNodeIndex;
|
|
pushRadioMessage (Radio::RogerThat);
|
|
}
|
|
}
|
|
graph.setVisited (bombPoint);
|
|
}
|
|
bots.setPlantedBombSearchTimestamp (game.time () + 0.5f);
|
|
}
|
|
break;
|
|
|
|
case Radio::GetInPositionAndWaitForGo:
|
|
if ((game.isNullEntity (m_enemy) && seesEntity (m_radioEntity->v.origin)) || distance < 1024.0f) {
|
|
pushRadioMessage (Radio::RogerThat);
|
|
|
|
if (getCurrentTaskId () == Task::Camp) {
|
|
getTask ()->time = game.time () + rg.float_ (30.0f, 60.0f);
|
|
}
|
|
else {
|
|
// don't pause anymore
|
|
Task taskID = getCurrentTaskId ();
|
|
|
|
if (taskID == Task::Pause) {
|
|
getTask ()->time = game.time ();
|
|
}
|
|
|
|
m_targetEntity = nullptr;
|
|
m_seeEnemyTime = game.time ();
|
|
|
|
// if bot has no enemy
|
|
if (m_lastEnemyOrigin.empty ()) {
|
|
float nearestDistance = kInfiniteDistance;
|
|
|
|
// take nearest enemy to ordering player
|
|
for (const auto &client : util.getClients ()) {
|
|
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team == m_team) {
|
|
continue;
|
|
}
|
|
|
|
auto enemy = client.ent;
|
|
float dist = (m_radioEntity->v.origin - enemy->v.origin).lengthSq ();
|
|
|
|
if (dist < nearestDistance) {
|
|
nearestDistance = dist;
|
|
m_lastEnemy = enemy;
|
|
m_lastEnemyOrigin = enemy->v.origin;
|
|
}
|
|
}
|
|
}
|
|
clearSearchNodes ();
|
|
|
|
int index = findDefendNode (m_radioEntity->v.origin);
|
|
|
|
// push camp task on to stack
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.float_ (30.0f, 60.0f), true);
|
|
// push move command
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.float_ (30.0f, 60.0f), true);
|
|
|
|
if (graph[index].vis.crouch <= graph[index].vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
m_radioOrder = 0; // radio command has been handled, reset
|
|
}
|
|
|
|
void Bot::tryHeadTowardRadioMessage () {
|
|
Task taskID = getCurrentTaskId ();
|
|
|
|
if (taskID == Task::MoveToPosition || m_headedTime + 15.0f < game.time () || !util.isAlive (m_radioEntity) || m_hasC4) {
|
|
return;
|
|
}
|
|
|
|
if ((util.isFakeClient (m_radioEntity) && rg.chance (25) && m_personality == Personality::Normal) || !(m_radioEntity->v.flags & FL_FAKECLIENT)) {
|
|
if (taskID == Task::Pause || taskID == Task::Camp) {
|
|
getTask ()->time = game.time ();
|
|
}
|
|
m_headedTime = game.time ();
|
|
m_position = m_radioEntity->v.origin;
|
|
|
|
clearSearchNodes ();
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, kInvalidNodeIndex, 0.0f, true);
|
|
}
|
|
}
|
|
|
|
void Bot::updateAimDir () {
|
|
uint32 flags = m_aimFlags;
|
|
|
|
// don't allow bot to look at danger positions under certain circumstances
|
|
if (!(flags & (AimFlags::Grenade | AimFlags::Enemy | AimFlags::Entity))) {
|
|
if (isOnLadder () || isInWater () || (m_pathFlags & NodeFlag::Ladder) || (m_currentTravelFlags & PathFlag::Jump)) {
|
|
flags &= ~(AimFlags::LastEnemy | AimFlags::PredictPath);
|
|
m_canChooseAimDirection = false;
|
|
}
|
|
}
|
|
|
|
if (flags & AimFlags::Override) {
|
|
m_lookAt = m_camp;
|
|
}
|
|
else if (flags & AimFlags::Grenade) {
|
|
m_lookAt = m_throw;
|
|
|
|
float throwDistance = (m_throw - pev->origin).length ();
|
|
float coordCorrection = 0.0f;
|
|
|
|
if (throwDistance > 100.0f && throwDistance < 800.0f) {
|
|
coordCorrection = 0.25f * (m_throw.z - pev->origin.z);
|
|
}
|
|
else if (throwDistance >= 800.0f) {
|
|
float angleCorrection = 37.0f * (throwDistance - 800.0f) / 800.0f;
|
|
|
|
if (angleCorrection > 45.0f) {
|
|
angleCorrection = 45.0f;
|
|
}
|
|
coordCorrection = throwDistance * cr::tanf (cr::degreesToRadians (angleCorrection)) + 0.25f * (m_throw.z - pev->origin.z);
|
|
}
|
|
m_lookAt.z += coordCorrection * 0.5f;
|
|
}
|
|
else if (flags & AimFlags::Enemy) {
|
|
focusEnemy ();
|
|
}
|
|
else if (flags & AimFlags::Entity) {
|
|
m_lookAt = m_entity;
|
|
}
|
|
else if (flags & AimFlags::LastEnemy) {
|
|
m_lookAt = m_lastEnemyOrigin;
|
|
|
|
// did bot just see enemy and is quite aggressive?
|
|
if (m_seeEnemyTime + 1.0f - m_actualReactionTime + m_baseAgressionLevel > game.time ()) {
|
|
|
|
// feel free to fire if shootable
|
|
if (!usesSniper () && lastEnemyShootable ()) {
|
|
m_wantsToFire = true;
|
|
}
|
|
}
|
|
}
|
|
else if (flags & AimFlags::PredictPath) {
|
|
bool changePredictedEnemy = true;
|
|
|
|
if (m_timeNextTracking > game.time () && m_trackingEdict == m_lastEnemy && util.isAlive (m_lastEnemy)) {
|
|
changePredictedEnemy = false;
|
|
}
|
|
|
|
if (changePredictedEnemy) {
|
|
int aimPoint = findAimingNode (m_lastEnemyOrigin);
|
|
|
|
if (aimPoint != kInvalidNodeIndex) {
|
|
m_lookAt = graph[aimPoint].origin;
|
|
m_camp = m_lookAt;
|
|
|
|
m_timeNextTracking = game.time () + 0.5f;
|
|
m_trackingEdict = m_lastEnemy;
|
|
}
|
|
else {
|
|
m_aimFlags &= ~AimFlags::PredictPath;
|
|
|
|
if (!m_camp.empty ()) {
|
|
m_lookAt = m_camp;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
m_lookAt = m_camp;
|
|
}
|
|
}
|
|
else if (flags & AimFlags::Camp) {
|
|
m_lookAt = m_camp;
|
|
}
|
|
else if (flags & AimFlags::Nav) {
|
|
if (m_moveToGoal && !m_isStuck && m_currentNodeIndex != kInvalidNodeIndex && !(m_path->flags & NodeFlag::Ladder) && m_pathWalk.hasNext () && (pev->origin - m_destOrigin).lengthSq () < cr::square (52.0f)) {
|
|
m_lookAt = graph[m_pathWalk.next ()].origin + pev->view_ofs;
|
|
}
|
|
else {
|
|
m_lookAt = m_destOrigin;
|
|
}
|
|
|
|
if (m_canChooseAimDirection && m_currentNodeIndex != kInvalidNodeIndex && !(m_path->flags & NodeFlag::Ladder)) {
|
|
int dangerIndex = graph.getDangerIndex (m_team, m_currentNodeIndex, m_currentNodeIndex);
|
|
|
|
if (graph.exists (dangerIndex) && graph.isVisible (m_currentNodeIndex, dangerIndex)) {
|
|
m_lookAt = graph[dangerIndex].origin + pev->view_ofs;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_lookAt.empty ()) {
|
|
m_lookAt = m_destOrigin;
|
|
}
|
|
}
|
|
|
|
void Bot::checkDarkness () {
|
|
|
|
// do not check for darkness at the start of the round
|
|
if (m_spawnTime + 5.0f > game.time () || !graph.exists (m_currentNodeIndex) || cr::fzero (m_path->light)) {
|
|
return;
|
|
}
|
|
|
|
// do not check every frame
|
|
if (m_checkDarkTime + 5.0f > game.time ()) {
|
|
return;
|
|
}
|
|
auto skyColor = illum.getSkyColor ();
|
|
|
|
if (mp_flashlight.bool_ () && !m_hasNVG) {
|
|
auto task = Task ();
|
|
|
|
if (!(pev->effects & EF_DIMLIGHT) && task != Task::Camp && task != Task::Attack && m_heardSoundTime + 3.0f < game.time () && m_flashLevel > 30.0f && ((skyColor > 50.0f && m_path->light < 10.0f) || (skyColor <= 50.0f && m_path->light < 40.0f))) {
|
|
pev->impulse = 100;
|
|
}
|
|
else if ((pev->effects & EF_DIMLIGHT) && (((m_path->light > 15.0f && skyColor > 50.0f) || (m_path->light > 45.0f && skyColor <= 50.0f)) || task == Task::Camp || task == Task::Attack || m_flashLevel <= 0 || m_heardSoundTime + 3.0f >= game.time ()))
|
|
{
|
|
pev->impulse = 100;
|
|
}
|
|
}
|
|
else if (m_hasNVG) {
|
|
if (pev->effects & EF_DIMLIGHT) {
|
|
pev->impulse = 100;
|
|
}
|
|
else if (!m_usesNVG && ((skyColor > 50.0f && m_path->light < 15.0f) || (skyColor <= 50.0f && m_path->light < 40.0f))) {
|
|
game.botCommand (ent (), "nightvision");
|
|
}
|
|
else if (m_usesNVG && ((m_path->light > 20.0f && skyColor > 50.0f) || (m_path->light > 45.0f && skyColor <= 50.0f))) {
|
|
game.botCommand (ent (), "nightvision");
|
|
}
|
|
}
|
|
m_checkDarkTime = game.time ();
|
|
}
|
|
|
|
void Bot::checkParachute () {
|
|
static auto parachute = engfuncs.pfnCVarGetPointer ("sv_parachute");
|
|
|
|
// if no cvar or it's not enabled do not bother
|
|
if (parachute && parachute->value > 0.0f) {
|
|
if (isOnLadder () || pev->velocity.z > -50.0f || isOnFloor ()) {
|
|
m_fallDownTime = 0.0f;
|
|
}
|
|
else if (cr::fzero (m_fallDownTime)) {
|
|
m_fallDownTime = game.time ();
|
|
}
|
|
|
|
// press use anyway
|
|
if (!cr::fzero (m_fallDownTime) && m_fallDownTime + 0.35f < game.time ()) {
|
|
pev->button |= IN_USE;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::frame () {
|
|
if (m_updateTime <= game.time ()) {
|
|
update ();
|
|
}
|
|
else if (m_notKilled) {
|
|
updateLookAngles ();
|
|
}
|
|
|
|
if (m_slowFrameTimestamp > game.time ()) {
|
|
return;
|
|
}
|
|
m_numFriendsLeft = numFriendsNear (pev->origin, kInfiniteDistance);
|
|
m_numEnemiesLeft = numEnemiesNear (pev->origin, kInfiniteDistance);
|
|
|
|
if (bots.isBombPlanted () && m_team == Team::CT && m_notKilled) {
|
|
const Vector &bombPosition = graph.getBombOrigin ();
|
|
|
|
if (!m_hasProgressBar && getCurrentTaskId () != Task::EscapeFromBomb && (pev->origin - bombPosition).lengthSq () < cr::square (1540.0f) && !isBombDefusing (bombPosition)) {
|
|
m_itemIgnore = nullptr;
|
|
m_itemCheckTime = game.time ();
|
|
|
|
clearTask (getCurrentTaskId ());
|
|
}
|
|
}
|
|
|
|
checkSpawnConditions ();
|
|
checkForChat ();
|
|
checkBreakablesAround ();
|
|
|
|
if (game.is (GameFlags::HasBotVoice)) {
|
|
showChaterIcon (false); // end voice feedback
|
|
}
|
|
|
|
// clear enemy far away
|
|
if (!m_lastEnemyOrigin.empty () && !game.isNullEntity (m_lastEnemy) && (pev->origin - m_lastEnemyOrigin).lengthSq () >= cr::square (1600.0f)) {
|
|
m_lastEnemy = nullptr;
|
|
m_lastEnemyOrigin = nullptr;
|
|
}
|
|
m_slowFrameTimestamp = game.time () + 0.5f;
|
|
}
|
|
|
|
void Bot::update () {
|
|
pev->button = 0;
|
|
pev->flags |= FL_FAKECLIENT; // restore fake client bit, if it were removed by some evil action =)
|
|
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
m_moveAngles = nullptr;
|
|
|
|
m_canChooseAimDirection = true;
|
|
m_notKilled = util.isAlive (ent ());
|
|
m_team = game.getTeam (ent ());
|
|
|
|
if (game.mapIs (MapFlags::Assassination) && !m_isVIP) {
|
|
m_isVIP = util.isPlayerVIP (ent ());
|
|
}
|
|
|
|
if (m_team == Team::Terrorist && game.mapIs (MapFlags::Demolition)) {
|
|
m_hasC4 = !!(pev->weapons & cr::bit (Weapon::C4));
|
|
}
|
|
|
|
// is bot movement enabled
|
|
bool botMovement = false;
|
|
|
|
// if the bot hasn't selected stuff to start the game yet, go do that...
|
|
if (m_notStarted) {
|
|
updateTeamJoin (); // select team & class
|
|
}
|
|
else if (!m_notKilled) {
|
|
// we got a teamkiller? vote him away...
|
|
if (m_voteKickIndex != m_lastVoteKick && yb_tkpunish.bool_ ()) {
|
|
game.botCommand (ent (), "vote %d", m_voteKickIndex);
|
|
m_lastVoteKick = m_voteKickIndex;
|
|
|
|
// if bot tk punishment is enabled slay the tk
|
|
if (yb_tkpunish.int_ () != 2 || util.isFakeClient (game.entityOfIndex (m_voteKickIndex))) {
|
|
return;
|
|
}
|
|
edict_t *killer = game.entityOfIndex (m_lastVoteKick);
|
|
|
|
++killer->v.frags;
|
|
MDLL_ClientKill (killer);
|
|
}
|
|
|
|
// host wants us to kick someone
|
|
else if (m_voteMap != 0) {
|
|
game.botCommand (ent (), "votemap %d", m_voteMap);
|
|
m_voteMap = 0;
|
|
}
|
|
}
|
|
else if (m_buyingFinished && !(pev->maxspeed < 10.0f && getCurrentTaskId () != Task::PlantBomb && getCurrentTaskId () != Task::DefuseBomb) && !yb_freeze_bots.bool_ () && !graph.hasChanged ()) {
|
|
botMovement = true;
|
|
}
|
|
checkMsgQueue (); // check for pending messages
|
|
|
|
if (botMovement) {
|
|
logic (); // execute main code
|
|
}
|
|
runMovement ();
|
|
|
|
// delay next execution
|
|
m_updateTime = game.time () + m_updateInterval;
|
|
}
|
|
|
|
void Bot::normal_ () {
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
int debugGoal = yb_debug_goal.int_ ();
|
|
|
|
// user forced a waypoint as a goal?
|
|
if (debugGoal != kInvalidNodeIndex && getTask ()->data != debugGoal) {
|
|
clearSearchNodes ();
|
|
getTask ()->data = debugGoal;
|
|
}
|
|
|
|
// stand still if reached debug goal
|
|
else if (m_currentNodeIndex == debugGoal) {
|
|
pev->button = 0;
|
|
ignoreCollision ();
|
|
|
|
m_moveSpeed = 0.0;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
return;
|
|
}
|
|
|
|
// bots rushing with knife, when have no enemy (thanks for idea to nicebot project)
|
|
if (m_currentWeapon == Weapon::Knife && (game.isNullEntity (m_lastEnemy) || !util.isAlive (m_lastEnemy)) && game.isNullEntity (m_enemy) && m_knifeAttackTime < game.time () && !hasShield () && numFriendsNear (pev->origin, 96.0f) == 0) {
|
|
if (rg.chance (40)) {
|
|
pev->button |= IN_ATTACK;
|
|
}
|
|
else {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
m_knifeAttackTime = game.time () + rg.float_ (2.5f, 6.0f);
|
|
}
|
|
const auto &prop = conf.getWeaponProp (m_currentWeapon);
|
|
|
|
if (m_reloadState == Reload::None && getAmmo () != 0 && getAmmoInClip () < 5 && prop.ammo1 != -1) {
|
|
m_reloadState = Reload::Primary;
|
|
}
|
|
|
|
// if bomb planted and it's a CT calculate new path to bomb point if he's not already heading for
|
|
if (!m_bombSearchOverridden && bots.isBombPlanted () && m_team == Team::CT && getTask ()->data != kInvalidNodeIndex && !(graph[getTask ()->data].flags & NodeFlag::Goal) && getCurrentTaskId () != Task::EscapeFromBomb) {
|
|
clearSearchNodes ();
|
|
getTask ()->data = kInvalidNodeIndex;
|
|
}
|
|
|
|
// reached the destination (goal) waypoint?
|
|
if (updateNavigation ()) {
|
|
|
|
// if we're reached the goal, and there is not enemies, notify the team
|
|
if (!bots.isBombPlanted () && m_currentNodeIndex != kInvalidNodeIndex && (m_path->flags & NodeFlag::Goal) && rg.chance (15) && numEnemiesNear (pev->origin, 650.0f) == 0) {
|
|
pushRadioMessage (Radio::SectorClear);
|
|
}
|
|
|
|
completeTask ();
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
|
|
// spray logo sometimes if allowed to do so
|
|
if (m_timeLogoSpray < game.time () && yb_spraypaints.bool_ () && rg.chance (60) && m_moveSpeed > getShiftSpeed () && game.isNullEntity (m_pickupItem)) {
|
|
if (!(game.mapIs (MapFlags::Demolition) && bots.isBombPlanted () && m_team == Team::CT)) {
|
|
startTask (Task::Spraypaint, TaskPri::Spraypaint, kInvalidNodeIndex, game.time () + 1.0f, false);
|
|
}
|
|
}
|
|
|
|
// reached waypoint is a camp waypoint
|
|
if ((m_path->flags & NodeFlag::Camp) && !game.is (GameFlags::CSDM) && yb_camping_allowed.bool_ ()) {
|
|
|
|
// check if bot has got a primary weapon and hasn't camped before
|
|
if (hasPrimaryWeapon () && m_timeCamping + 10.0f < game.time () && !hasHostage ()) {
|
|
bool campingAllowed = true;
|
|
|
|
// Check if it's not allowed for this team to camp here
|
|
if (m_team == Team::Terrorist) {
|
|
if (m_path->flags & NodeFlag::CTOnly) {
|
|
campingAllowed = false;
|
|
}
|
|
}
|
|
else {
|
|
if (m_path->flags & NodeFlag::TerroristOnly) {
|
|
campingAllowed = false;
|
|
}
|
|
}
|
|
|
|
// don't allow vip on as_ maps to camp + don't allow terrorist carrying c4 to camp
|
|
if (campingAllowed && (m_isVIP || (game.mapIs (MapFlags::Demolition) && m_team == Team::Terrorist && !bots.isBombPlanted () && m_hasC4))) {
|
|
campingAllowed = false;
|
|
}
|
|
|
|
// check if another bot is already camping here
|
|
if (campingAllowed && isOccupiedNode (m_currentNodeIndex)) {
|
|
campingAllowed = false;
|
|
}
|
|
|
|
if (campingAllowed) {
|
|
// crouched camping here?
|
|
if (m_path->flags & NodeFlag::Crouch) {
|
|
m_campButtons = IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons = 0;
|
|
}
|
|
selectBestWeapon ();
|
|
|
|
if (!(m_states & (Sense::SeeingEnemy | Sense::HearingEnemy)) && !m_reloadState) {
|
|
m_reloadState = Reload::Primary;
|
|
}
|
|
m_timeCamping = game.time () + rg.float_ (10.0f, 25.0f);
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, m_timeCamping, true);
|
|
|
|
m_camp = m_path->origin + m_path->start.forward () * 500.0f;;
|
|
m_aimFlags |= AimFlags::Camp;
|
|
m_campDirection = 0;
|
|
|
|
// tell the world we're camping
|
|
if (rg.chance (40)) {
|
|
pushRadioMessage (Radio::ImInPosition);
|
|
}
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// some goal waypoints are map dependant so check it out...
|
|
if (game.mapIs (MapFlags::HostageRescue)) {
|
|
// CT Bot has some hostages following?
|
|
if (m_team == Team::CT && hasHostage ()) {
|
|
// and reached a Rescue Point?
|
|
if (m_path->flags & NodeFlag::Rescue) {
|
|
m_hostages.clear ();
|
|
}
|
|
}
|
|
else if (m_team == Team::Terrorist && rg.chance (75)) {
|
|
int index = findDefendNode (m_path->origin);
|
|
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.float_ (60.0f, 120.0f), true); // push camp task on to stack
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.float_ (5.0f, 10.0f), true); // push move command
|
|
|
|
auto &path = graph[index];
|
|
|
|
// decide to duck or not to duck
|
|
if (path.vis.crouch <= path.vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
pushChatterMessage (Chatter::GoingToGuardVIPSafety); // play info about that
|
|
}
|
|
}
|
|
else if (game.mapIs (MapFlags::Demolition) && ((m_path->flags & NodeFlag::Goal) || m_inBombZone)) {
|
|
// is it a terrorist carrying the bomb?
|
|
if (m_hasC4) {
|
|
if ((m_states & Sense::SeeingEnemy) && numFriendsNear (pev->origin, 768.0f) == 0) {
|
|
// request an help also
|
|
pushRadioMessage (Radio::NeedBackup);
|
|
pushChatterMessage (Chatter::ScaredEmotion);
|
|
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + rg.float_ (4.0f, 8.0f), true);
|
|
}
|
|
else {
|
|
startTask (Task::PlantBomb, TaskPri::PlantBomb, kInvalidNodeIndex, 0.0f, false);
|
|
}
|
|
}
|
|
else if (m_team == Team::CT) {
|
|
if (!bots.isBombPlanted () && numFriendsNear (pev->origin, 210.0f) < 4) {
|
|
int index = findDefendNode (m_path->origin);
|
|
|
|
float campTime = rg.float_ (25.0f, 40.f);
|
|
|
|
// rusher bots don't like to camp too much
|
|
if (m_personality == Personality::Rusher) {
|
|
campTime *= 0.5f;
|
|
}
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + campTime, true); // push camp task on to stack
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + rg.float_ (5.0f, 11.0f), true); // push move command
|
|
|
|
auto &path = graph[index];
|
|
|
|
// decide to duck or not to duck
|
|
if (path.vis.crouch <= path.vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
pushChatterMessage (Chatter::DefendingBombsite); // play info about that
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// no more nodes to follow - search new ones (or we have a bomb)
|
|
else if (!hasActiveGoal ()) {
|
|
m_moveSpeed = pev->maxspeed;
|
|
|
|
clearSearchNodes ();
|
|
ignoreCollision ();
|
|
|
|
// did we already decide about a goal before?
|
|
int destIndex = getTask ()->data != kInvalidNodeIndex ? getTask ()->data : findBestGoal ();
|
|
|
|
m_prevGoalIndex = destIndex;
|
|
|
|
// remember index
|
|
getTask ()->data = destIndex;
|
|
|
|
auto pathSearchType = m_pathType;
|
|
|
|
// override with fast path
|
|
if (game.mapIs (MapFlags::Demolition) && bots.isBombPlanted ()) {
|
|
pathSearchType = FindPath::Fast;
|
|
}
|
|
|
|
// do pathfinding if it's not the current waypoint
|
|
if (destIndex != m_currentNodeIndex) {
|
|
findPath (m_currentNodeIndex, destIndex, pathSearchType);
|
|
}
|
|
}
|
|
else {
|
|
if (!(pev->flags & FL_DUCKING) && m_minSpeed != pev->maxspeed && m_minSpeed > 1.0f) {
|
|
m_moveSpeed = m_minSpeed;
|
|
}
|
|
}
|
|
float shiftSpeed = getShiftSpeed ();
|
|
|
|
if ((!cr::fzero (m_moveSpeed) && m_moveSpeed > shiftSpeed) && (yb_walking_allowed.bool_ () && mp_footsteps.bool_ ()) && m_difficulty > 2 && !(m_aimFlags & AimFlags::Enemy) && (m_heardSoundTime + 6.0f >= game.time () || (m_states & Sense::SuspectEnemy)) && numEnemiesNear (pev->origin, 768.0f) >= 1 && !yb_jasonmode.bool_ () && !bots.isBombPlanted ()) {
|
|
m_moveSpeed = shiftSpeed;
|
|
}
|
|
|
|
// bot hasn't seen anything in a long time and is asking his teammates to report in
|
|
if (yb_radio_mode.int_ () > 1 && m_seeEnemyTime + rg.float_ (45.0f, 80.0f) < game.time () && bots.getLastRadio (m_team) != Radio::ReportInTeam && rg.chance (15) && bots.getRoundStartTime () + 20.0f < game.time () && m_askCheckTime < game.time () && numFriendsNear (pev->origin, 1024.0f) == 0) {
|
|
pushRadioMessage (Radio::ReportInTeam);
|
|
|
|
m_askCheckTime = game.time () + rg.float_ (45.0f, 80.0f);
|
|
|
|
// make sure everyone else will not ask next few moments
|
|
for (const auto &bot : bots) {
|
|
if (bot->m_notKilled) {
|
|
bot->m_askCheckTime = game.time () + rg.float_ (5.0f, 30.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::spraypaint_ () {
|
|
m_aimFlags |= AimFlags::Entity;
|
|
|
|
// bot didn't spray this round?
|
|
if (m_timeLogoSpray < game.time () && getTask ()->time > game.time ()) {
|
|
const auto &forward = pev->v_angle.forward ();
|
|
Vector sprayOrigin = getEyesPos () + forward * 128.0f;
|
|
|
|
TraceResult tr;
|
|
game.testLine (getEyesPos (), sprayOrigin, TraceIgnore::Monsters, ent (), &tr);
|
|
|
|
// no wall in front?
|
|
if (tr.flFraction >= 1.0f) {
|
|
sprayOrigin.z -= 128.0f;
|
|
}
|
|
m_entity = sprayOrigin;
|
|
|
|
if (getTask ()->time - 0.5f < game.time ()) {
|
|
// emit spraycan sound
|
|
engfuncs.pfnEmitSound (ent (), CHAN_VOICE, "player/sprayer.wav", 1.0f, ATTN_NORM, 0, 100);
|
|
game.testLine (getEyesPos (), getEyesPos () + forward * 128.0f, TraceIgnore::Monsters, ent (), &tr);
|
|
|
|
// paint the actual logo decal
|
|
util.traceDecals (pev, &tr, m_logotypeIndex);
|
|
m_timeLogoSpray = game.time () + rg.float_ (60.0f, 90.0f);
|
|
}
|
|
}
|
|
else {
|
|
completeTask ();
|
|
}
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_navTimeset = game.time ();
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
ignoreCollision ();
|
|
}
|
|
|
|
void Bot::huntEnemy_ () {
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
// if we've got new enemy...
|
|
if (!game.isNullEntity (m_enemy) || game.isNullEntity (m_lastEnemy)) {
|
|
|
|
// forget about it...
|
|
clearTask (Task::Hunt);
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
}
|
|
else if (game.getTeam (m_lastEnemy) == m_team) {
|
|
|
|
// don't hunt down our teammate...
|
|
clearTask (Task::Hunt);
|
|
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
m_lastEnemy = nullptr;
|
|
}
|
|
else if (updateNavigation ()) // reached last enemy pos?
|
|
{
|
|
// forget about it...
|
|
completeTask ();
|
|
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
m_lastEnemyOrigin = nullptr;
|
|
}
|
|
|
|
// do we need to calculate a new path?
|
|
else if (!hasActiveGoal ()) {
|
|
clearSearchNodes ();
|
|
|
|
int destIndex = kInvalidNodeIndex;
|
|
int goal = getTask ()->data;
|
|
|
|
// is there a remembered index?
|
|
if (graph.exists (goal)) {
|
|
destIndex = goal;
|
|
}
|
|
|
|
// find new one instead
|
|
else {
|
|
destIndex = graph.getNearest (m_lastEnemyOrigin);
|
|
}
|
|
|
|
// remember index
|
|
m_prevGoalIndex = destIndex;
|
|
getTask ()->data = destIndex;
|
|
|
|
if (destIndex != m_currentNodeIndex) {
|
|
findPath (m_currentNodeIndex, destIndex, FindPath::Fast);
|
|
}
|
|
}
|
|
|
|
// bots skill higher than 60?
|
|
if (yb_walking_allowed.bool_ () && mp_footsteps.bool_ () && m_difficulty > 2 && !yb_jasonmode.bool_ ()) {
|
|
// then make him move slow if near enemy
|
|
if (!(m_currentTravelFlags & PathFlag::Jump)) {
|
|
if (m_currentNodeIndex != kInvalidNodeIndex) {
|
|
if (m_path->radius < 32.0f && !isOnLadder () && !isInWater () && m_seeEnemyTime + 4.0f > game.time () && m_difficulty < 3) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
}
|
|
|
|
if ((m_lastEnemyOrigin - pev->origin).lengthSq () < cr::square (512.0f)) {
|
|
m_moveSpeed = getShiftSpeed ();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::seekCover_ () {
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (!util.isAlive (m_lastEnemy)) {
|
|
completeTask ();
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
}
|
|
|
|
// reached final waypoint?
|
|
else if (updateNavigation ()) {
|
|
// yep. activate hide behaviour
|
|
completeTask ();
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
|
|
// start hide task
|
|
startTask (Task::Hide, TaskPri::Hide, kInvalidNodeIndex, game.time () + rg.float_ (3.0f, 12.0f), false);
|
|
Vector dest = m_lastEnemyOrigin;
|
|
|
|
// get a valid look direction
|
|
getCampDirection (&dest);
|
|
|
|
m_aimFlags |= AimFlags::Camp;
|
|
m_camp = dest;
|
|
m_campDirection = 0;
|
|
|
|
// chosen waypoint is a camp waypoint?
|
|
if (m_path->flags & NodeFlag::Camp) {
|
|
// use the existing camp node prefs
|
|
if (m_path->flags & NodeFlag::Crouch) {
|
|
m_campButtons = IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons = 0;
|
|
}
|
|
}
|
|
else {
|
|
// choose a crouch or stand pos
|
|
if (m_path->vis.crouch <= m_path->vis.stand) {
|
|
m_campButtons = IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons = 0;
|
|
}
|
|
|
|
// enter look direction from previously calculated positions
|
|
m_path->start = dest;
|
|
m_path->end = dest;
|
|
}
|
|
|
|
if (m_reloadState == Reload::None && getAmmoInClip () < 5 && getAmmo () != 0) {
|
|
m_reloadState = Reload::Primary;
|
|
}
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
}
|
|
else if (!hasActiveGoal ()) // we didn't choose a cover waypoint yet or lost it due to an attack?
|
|
{
|
|
clearSearchNodes ();
|
|
int destIndex = kInvalidNodeIndex;
|
|
|
|
if (getTask ()->data != kInvalidNodeIndex) {
|
|
destIndex = getTask ()->data;
|
|
}
|
|
else {
|
|
destIndex = findCoverNode (usesSniper () ? 256.0f : 512.0f);
|
|
|
|
if (destIndex == kInvalidNodeIndex) {
|
|
m_retreatTime = game.time () + rg.float_ (5.0f, 10.0f);
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
|
|
completeTask ();
|
|
return;
|
|
}
|
|
}
|
|
m_campDirection = 0;
|
|
|
|
m_prevGoalIndex = destIndex;
|
|
getTask ()->data = destIndex;
|
|
|
|
if (destIndex != m_currentNodeIndex) {
|
|
findPath (m_currentNodeIndex, destIndex, FindPath::Fast);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::attackEnemy_ () {
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
if (!game.isNullEntity (m_enemy)) {
|
|
ignoreCollision ();
|
|
|
|
if (isOnLadder ()) {
|
|
pev->button |= IN_JUMP;
|
|
clearSearchNodes ();
|
|
}
|
|
attackMovement ();
|
|
|
|
if (m_currentWeapon == Weapon::Knife && !m_lastEnemyOrigin.empty ()) {
|
|
m_destOrigin = m_lastEnemyOrigin;
|
|
}
|
|
}
|
|
else {
|
|
completeTask ();
|
|
m_destOrigin = m_lastEnemyOrigin;
|
|
}
|
|
m_navTimeset = game.time ();
|
|
}
|
|
|
|
void Bot::pause_ () {
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_navTimeset = game.time ();
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
// is bot blinded and above average difficulty?
|
|
if (m_viewDistance < 500.0f && m_difficulty >= 2) {
|
|
// go mad!
|
|
m_moveSpeed = -cr::abs ((m_viewDistance - 500.0f) * 0.5f);
|
|
|
|
if (m_moveSpeed < -pev->maxspeed) {
|
|
m_moveSpeed = -pev->maxspeed;
|
|
}
|
|
m_camp = getEyesPos () + pev->v_angle.forward () * 500.0f;
|
|
|
|
m_aimFlags |= AimFlags::Override;
|
|
m_wantsToFire = true;
|
|
}
|
|
else {
|
|
pev->button |= m_campButtons;
|
|
}
|
|
|
|
// stop camping if time over or gets hurt by something else than bullets
|
|
if (getTask ()->time < game.time () || m_lastDamageType > 0) {
|
|
completeTask ();
|
|
}
|
|
}
|
|
|
|
void Bot::blind_ () {
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
m_navTimeset = game.time ();
|
|
|
|
// if bot remembers last enemy position
|
|
if (m_difficulty >= 2 && !m_lastEnemyOrigin.empty () && util.isPlayer (m_lastEnemy) && !usesSniper ()) {
|
|
m_lookAt = m_lastEnemyOrigin; // face last enemy
|
|
m_wantsToFire = true; // and shoot it
|
|
}
|
|
|
|
m_moveSpeed = m_blindMoveSpeed;
|
|
m_strafeSpeed = m_blindSidemoveSpeed;
|
|
pev->button |= m_blindButton;
|
|
|
|
if (m_blindTime < game.time ()) {
|
|
completeTask ();
|
|
}
|
|
}
|
|
|
|
void Bot::camp_ () {
|
|
if (!yb_camping_allowed.bool_ ()) {
|
|
completeTask ();
|
|
return;
|
|
}
|
|
|
|
m_aimFlags |= AimFlags::Camp;
|
|
m_checkTerrain = false;
|
|
m_moveToGoal = false;
|
|
|
|
if (m_team == Team::CT && bots.isBombPlanted () && m_defendedBomb && !isBombDefusing (graph.getBombOrigin ()) && !isOutOfBombTimer ()) {
|
|
m_defendedBomb = false;
|
|
completeTask ();
|
|
}
|
|
ignoreCollision ();
|
|
|
|
// half the reaction time if camping because you're more aware of enemies if camping
|
|
setIdealReactionTimers ();
|
|
m_idealReactionTime *= 0.5f;
|
|
|
|
m_navTimeset = game.time ();
|
|
m_timeCamping = game.time ();
|
|
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
findValidNode ();
|
|
|
|
if (m_nextCampDirTime < game.time ()) {
|
|
m_nextCampDirTime = game.time () + rg.float_ (2.0f, 5.0f);
|
|
|
|
if (m_path->flags & NodeFlag::Camp) {
|
|
Vector dest;
|
|
|
|
// switch from 1 direction to the other
|
|
if (m_campDirection < 1) {
|
|
dest = m_path->start;
|
|
m_campDirection ^= 1;
|
|
}
|
|
else {
|
|
dest = m_path->end;
|
|
m_campDirection ^= 1;
|
|
}
|
|
dest.z = 0.0f;
|
|
|
|
// find a visible waypoint to this direction...
|
|
// i know this is ugly hack, but i just don't want to break compatibility :)
|
|
int numFoundPoints = 0;
|
|
|
|
int campPoints[3] = { 0, };
|
|
int distances[3] = { 0, };
|
|
|
|
const Vector &dotA = (dest - pev->origin).normalize2d ();
|
|
|
|
for (int i = 0; i < graph.length (); ++i) {
|
|
// skip invisible waypoints or current waypoint
|
|
if (!graph.isVisible (m_currentNodeIndex, i) || (i == m_currentNodeIndex)) {
|
|
continue;
|
|
}
|
|
const Vector &dotB = (graph[i].origin - pev->origin).normalize2d ();
|
|
|
|
if ((dotA | dotB) > 0.9f) {
|
|
int distance = static_cast <int> ((pev->origin - graph[i].origin).length ());
|
|
|
|
if (numFoundPoints >= 3) {
|
|
for (int j = 0; j < 3; ++j) {
|
|
if (distance > distances[j]) {
|
|
distances[j] = distance;
|
|
campPoints[j] = i;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
campPoints[numFoundPoints] = i;
|
|
distances[numFoundPoints] = distance;
|
|
|
|
++numFoundPoints;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (--numFoundPoints >= 0) {
|
|
m_camp = graph[campPoints[rg.int_ (0, numFoundPoints)]].origin;
|
|
}
|
|
else {
|
|
m_camp = graph[findCampingDirection ()].origin;
|
|
}
|
|
}
|
|
else {
|
|
m_camp = graph[findCampingDirection ()].origin;
|
|
}
|
|
}
|
|
// press remembered crouch button
|
|
pev->button |= m_campButtons;
|
|
|
|
// stop camping if time over or gets hurt by something else than bullets
|
|
if (getTask ()->time < game.time () || m_lastDamageType > 0) {
|
|
completeTask ();
|
|
}
|
|
}
|
|
|
|
void Bot::hide_ () {
|
|
m_aimFlags |= AimFlags::Camp;
|
|
m_checkTerrain = false;
|
|
m_moveToGoal = false;
|
|
|
|
// half the reaction time if camping
|
|
setIdealReactionTimers ();
|
|
m_idealReactionTime *= 0.5f;
|
|
|
|
m_navTimeset = game.time ();
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
findValidNode ();
|
|
|
|
if (hasShield () && !m_isReloading) {
|
|
if (!isShieldDrawn ()) {
|
|
pev->button |= IN_ATTACK2; // draw the shield!
|
|
}
|
|
else {
|
|
pev->button |= IN_DUCK; // duck under if the shield is already drawn
|
|
}
|
|
}
|
|
|
|
// if we see an enemy and aren't at a good camping point leave the spot
|
|
if ((m_states & Sense::SeeingEnemy) || m_inBombZone) {
|
|
if (!(m_path->flags & NodeFlag::Camp)) {
|
|
completeTask ();
|
|
|
|
m_campButtons = 0;
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
|
|
if (!game.isNullEntity (m_enemy)) {
|
|
attackMovement ();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// if we don't have an enemy we're also free to leave
|
|
else if (m_lastEnemyOrigin.empty ()) {
|
|
completeTask ();
|
|
|
|
m_campButtons = 0;
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
|
|
if (getCurrentTaskId () == Task::Hide) {
|
|
completeTask ();
|
|
}
|
|
return;
|
|
}
|
|
|
|
pev->button |= m_campButtons;
|
|
m_navTimeset = game.time ();
|
|
|
|
if (!m_isReloading) {
|
|
checkReload ();
|
|
}
|
|
|
|
// stop camping if time over or gets hurt by something else than bullets
|
|
if (getTask ()->time < game.time () || m_lastDamageType > 0) {
|
|
completeTask ();
|
|
}
|
|
}
|
|
|
|
void Bot::moveToPos_ () {
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (isShieldDrawn ()) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
|
|
// reached destination?
|
|
if (updateNavigation ()) {
|
|
completeTask (); // we're done
|
|
|
|
m_prevGoalIndex = kInvalidNodeIndex;
|
|
m_position = nullptr;
|
|
}
|
|
|
|
// didn't choose goal waypoint yet?
|
|
else if (!hasActiveGoal ()) {
|
|
clearSearchNodes ();
|
|
|
|
int destIndex = kInvalidNodeIndex;
|
|
int goal = getTask ()->data;
|
|
|
|
if (graph.exists (goal)) {
|
|
destIndex = goal;
|
|
}
|
|
else {
|
|
destIndex = graph.getNearest (m_position);
|
|
}
|
|
if (graph.exists (destIndex)) {
|
|
m_prevGoalIndex = destIndex;
|
|
getTask ()->data = destIndex;
|
|
|
|
findPath (m_currentNodeIndex, destIndex, m_pathType);
|
|
}
|
|
else {
|
|
completeTask ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::plantBomb_ () {
|
|
m_aimFlags |= AimFlags::Camp;
|
|
|
|
// we're still got the C4?
|
|
if (m_hasC4) {
|
|
|
|
if (m_currentWeapon != Weapon::C4) {
|
|
selectWeaponByName ("weapon_c4");
|
|
}
|
|
|
|
if (util.isAlive (m_enemy) || !m_inBombZone) {
|
|
completeTask ();
|
|
}
|
|
else {
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
m_navTimeset = game.time ();
|
|
|
|
if (m_path->flags & NodeFlag::Crouch) {
|
|
pev->button |= (IN_ATTACK | IN_DUCK);
|
|
}
|
|
else {
|
|
pev->button |= IN_ATTACK;
|
|
}
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
}
|
|
}
|
|
|
|
// done with planting
|
|
else {
|
|
completeTask ();
|
|
|
|
// tell teammates to move over here...
|
|
if (numFriendsNear (pev->origin, 1200.0f) != 0) {
|
|
pushRadioMessage (Radio::NeedBackup);
|
|
}
|
|
clearSearchNodes ();
|
|
int index = findDefendNode (pev->origin);
|
|
|
|
float guardTime = mp_c4timer.float_ () * 0.5f + mp_c4timer.float_ () * 0.25f;
|
|
|
|
// push camp task on to stack
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + guardTime, true);
|
|
|
|
// push move command
|
|
startTask (Task::MoveToPosition, TaskPri::MoveToPosition, index, game.time () + guardTime, true);
|
|
|
|
if (graph[index].vis.crouch <= graph[index].vis.stand) {
|
|
m_campButtons |= IN_DUCK;
|
|
}
|
|
else {
|
|
m_campButtons &= ~IN_DUCK;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::bombDefuse_ () {
|
|
float fullDefuseTime = m_hasDefuser ? 7.0f : 12.0f;
|
|
float timeToBlowUp = getBombTimeleft ();
|
|
float defuseRemainingTime = fullDefuseTime;
|
|
|
|
if (m_hasProgressBar /*&& isOnFloor ()*/) {
|
|
defuseRemainingTime = fullDefuseTime - game.time ();
|
|
}
|
|
|
|
bool pickupExists = !game.isNullEntity (m_pickupItem);
|
|
const Vector &bombPos = pickupExists ? game.getEntityWorldOrigin (m_pickupItem) : graph.getBombOrigin ();
|
|
|
|
bool defuseError = false;
|
|
|
|
// exception: bomb has been defused
|
|
if (bombPos.empty () || !pickupExists) {
|
|
defuseError = true;
|
|
|
|
if (m_numFriendsLeft != 0 && rg.chance (50)) {
|
|
if (timeToBlowUp <= 3.0) {
|
|
if (yb_radio_mode.int_ () == 2) {
|
|
pushChatterMessage (Chatter::BarelyDefused);
|
|
}
|
|
else if (yb_radio_mode.int_ () == 1) {
|
|
pushRadioMessage (Radio::SectorClear);
|
|
}
|
|
}
|
|
else {
|
|
pushRadioMessage (Radio::SectorClear);
|
|
}
|
|
}
|
|
}
|
|
else if (defuseRemainingTime > timeToBlowUp) {
|
|
defuseError = true;
|
|
}
|
|
else if (m_states & Sense::SeeingEnemy) {
|
|
int friends = numFriendsNear (pev->origin, 768.0f);
|
|
|
|
if (friends < 2 && defuseRemainingTime < timeToBlowUp) {
|
|
defuseError = true;
|
|
|
|
if (defuseRemainingTime + 2.0f > timeToBlowUp) {
|
|
defuseError = false;
|
|
}
|
|
|
|
if (m_numFriendsLeft > friends) {
|
|
pushRadioMessage (Radio::NeedBackup);
|
|
}
|
|
}
|
|
}
|
|
|
|
// one of exceptions is thrown. finish task.
|
|
if (defuseError) {
|
|
m_checkTerrain = true;
|
|
m_moveToGoal = true;
|
|
|
|
m_destOrigin = nullptr;
|
|
m_entity = nullptr;
|
|
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
return;
|
|
}
|
|
|
|
// to revert from pause after reload ting && just to be sure
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_moveSpeed = pev->maxspeed;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
// bot is reloading and we close enough to start defusing
|
|
if (m_isReloading && (bombPos - pev->origin).length2d () < 80.0f) {
|
|
if (m_numEnemiesLeft == 0 || timeToBlowUp < fullDefuseTime + 7.0f || ((getAmmoInClip () > 8 && m_reloadState == Reload::Primary) || (getAmmoInClip () > 5 && m_reloadState == Reload::Secondary))) {
|
|
int weaponIndex = bestWeaponCarried ();
|
|
|
|
// just select knife and then select weapon
|
|
selectWeaponByName ("weapon_knife");
|
|
|
|
if (weaponIndex > 0 && weaponIndex < kNumWeapons) {
|
|
selectWeaponById (weaponIndex);
|
|
}
|
|
m_isReloading = false;
|
|
}
|
|
else {
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
}
|
|
}
|
|
|
|
// head to bomb and press use button
|
|
m_aimFlags |= AimFlags::Entity;
|
|
|
|
m_destOrigin = bombPos;
|
|
m_entity = bombPos;
|
|
|
|
pev->button |= IN_USE;
|
|
|
|
// if defusing is not already started, maybe crouch before
|
|
if (!m_hasProgressBar && m_duckDefuseCheckTime < game.time ()) {
|
|
Vector botDuckOrigin, botStandOrigin;
|
|
|
|
if (pev->button & IN_DUCK) {
|
|
botDuckOrigin = pev->origin;
|
|
botStandOrigin = pev->origin + Vector (0.0f, 0.0f, 18.0f);
|
|
}
|
|
else {
|
|
botDuckOrigin = pev->origin - Vector (0.0f, 0.0f, 18.0f);
|
|
botStandOrigin = pev->origin;
|
|
}
|
|
|
|
float duckLength = (m_entity - botDuckOrigin).lengthSq ();
|
|
float standLength = (m_entity - botStandOrigin).lengthSq ();
|
|
|
|
if (duckLength > 5625.0f || standLength > 5625.0f) {
|
|
if (standLength < duckLength) {
|
|
m_duckDefuse = false; // stand
|
|
}
|
|
else {
|
|
m_duckDefuse = m_difficulty >= 2 && m_numEnemiesLeft != 0; // duck
|
|
}
|
|
}
|
|
m_duckDefuseCheckTime = game.time () + 5.0f;
|
|
}
|
|
|
|
// press duck button
|
|
if (m_duckDefuse || (m_oldButtons & IN_DUCK)) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
else {
|
|
pev->button &= ~IN_DUCK;
|
|
}
|
|
|
|
// we are defusing bomb
|
|
if (m_hasProgressBar || pickupExists || (m_oldButtons & IN_USE)) {
|
|
pev->button |= IN_USE;
|
|
|
|
m_reloadState = Reload::None;
|
|
m_navTimeset = game.time ();
|
|
|
|
// don't move when defusing
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
// notify team
|
|
if (m_numFriendsLeft != 0) {
|
|
pushChatterMessage (Chatter::DefusingBomb);
|
|
|
|
if (numFriendsNear (pev->origin, 512.0f) < 2) {
|
|
pushRadioMessage (Radio::NeedBackup);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
completeTask ();
|
|
}
|
|
}
|
|
|
|
void Bot::followUser_ () {
|
|
if (game.isNullEntity (m_targetEntity) || !util.isAlive (m_targetEntity)) {
|
|
m_targetEntity = nullptr;
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
|
|
if (m_targetEntity->v.button & IN_ATTACK) {
|
|
TraceResult tr;
|
|
game.testLine (m_targetEntity->v.origin + m_targetEntity->v.view_ofs, m_targetEntity->v.v_angle.forward () * 500.0f, TraceIgnore::Everything, ent (), &tr);
|
|
|
|
if (!game.isNullEntity (tr.pHit) && util.isPlayer (tr.pHit) && game.getTeam (tr.pHit) != m_team) {
|
|
m_targetEntity = nullptr;
|
|
m_lastEnemy = tr.pHit;
|
|
m_lastEnemyOrigin = tr.pHit->v.origin;
|
|
|
|
completeTask ();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (m_targetEntity->v.maxspeed != 0 && m_targetEntity->v.maxspeed < pev->maxspeed) {
|
|
m_moveSpeed = m_targetEntity->v.maxspeed;
|
|
}
|
|
|
|
if (m_reloadState == Reload::None && getAmmo () != 0) {
|
|
m_reloadState = Reload::Primary;
|
|
}
|
|
|
|
if ((m_targetEntity->v.origin - pev->origin).lengthSq () > cr::square (130.0f)) {
|
|
m_followWaitTime = 0.0f;
|
|
}
|
|
else {
|
|
m_moveSpeed = 0.0f;
|
|
|
|
if (m_followWaitTime == 0.0f) {
|
|
m_followWaitTime = game.time ();
|
|
}
|
|
else {
|
|
if (m_followWaitTime + 3.0f < game.time ()) {
|
|
// stop following if we have been waiting too long
|
|
m_targetEntity = nullptr;
|
|
|
|
pushRadioMessage (Radio::YouTakeThePoint);
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (yb_walking_allowed.bool_ () && m_targetEntity->v.maxspeed < m_moveSpeed && !yb_jasonmode.bool_ ()) {
|
|
m_moveSpeed = getShiftSpeed ();
|
|
}
|
|
|
|
if (isShieldDrawn ()) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
|
|
// reached destination?
|
|
if (updateNavigation ()) {
|
|
getTask ()->data = kInvalidNodeIndex;
|
|
}
|
|
|
|
// didn't choose goal waypoint yet?
|
|
if (!hasActiveGoal ()) {
|
|
clearSearchNodes ();
|
|
|
|
int destIndex = graph.getNearest (m_targetEntity->v.origin);
|
|
IntArray points = graph.searchRadius (200.0f, m_targetEntity->v.origin);
|
|
|
|
for (auto &newIndex : points) {
|
|
// if waypoint not yet used, assign it as dest
|
|
if (newIndex != m_currentNodeIndex && !isOccupiedNode (newIndex)) {
|
|
destIndex = newIndex;
|
|
}
|
|
}
|
|
|
|
if (graph.exists (destIndex) && graph.exists (m_currentNodeIndex)) {
|
|
m_prevGoalIndex = destIndex;
|
|
getTask ()->data = destIndex;
|
|
|
|
// always take the shortest path
|
|
findPath (m_currentNodeIndex, destIndex, FindPath::Fast);
|
|
}
|
|
else {
|
|
m_targetEntity = nullptr;
|
|
completeTask ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::throwExplosive_ () {
|
|
m_aimFlags |= AimFlags::Grenade;
|
|
Vector dest = m_throw;
|
|
|
|
if (!(m_states & Sense::SeeingEnemy)) {
|
|
m_strafeSpeed = 0.0f;
|
|
m_moveSpeed = 0.0f;
|
|
m_moveToGoal = false;
|
|
}
|
|
else if (!(m_states & Sense::SuspectEnemy) && !game.isNullEntity (m_enemy)) {
|
|
dest = m_enemy->v.origin + m_enemy->v.velocity.get2d () * 0.55f;
|
|
}
|
|
m_isUsingGrenade = true;
|
|
m_checkTerrain = false;
|
|
|
|
ignoreCollision ();
|
|
|
|
if ((pev->origin - dest).lengthSq () < cr::square (400.0f)) {
|
|
// heck, I don't wanna blow up myself
|
|
m_grenadeCheckTime = game.time () + kGrenadeCheckTime;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
m_grenade = calcThrow (getEyesPos (), dest);
|
|
|
|
if (m_grenade.lengthSq () < 100.0f) {
|
|
m_grenade = calcToss (pev->origin, dest);
|
|
}
|
|
|
|
if (m_grenade.lengthSq () <= 100.0f) {
|
|
m_grenadeCheckTime = game.time () + kGrenadeCheckTime;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
}
|
|
else {
|
|
auto grenade = correctGrenadeVelocity ("hegrenade.mdl");
|
|
|
|
if (game.isNullEntity (grenade)) {
|
|
if (m_currentWeapon != Weapon::Explosive && !m_grenadeRequested) {
|
|
if (pev->weapons & cr::bit (Weapon::Explosive)) {
|
|
m_grenadeRequested = true;
|
|
selectWeaponByName ("weapon_hegrenade");
|
|
}
|
|
else {
|
|
m_grenadeRequested = false;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
}
|
|
else if (!(m_oldButtons & IN_ATTACK)) {
|
|
pev->button |= IN_ATTACK;
|
|
m_grenadeRequested = false;
|
|
}
|
|
}
|
|
}
|
|
pev->button |= m_campButtons;
|
|
}
|
|
|
|
void Bot::throwFlashbang_ () {
|
|
m_aimFlags |= AimFlags::Grenade;
|
|
Vector dest = m_throw;
|
|
|
|
if (!(m_states & Sense::SeeingEnemy)) {
|
|
m_strafeSpeed = 0.0f;
|
|
m_moveSpeed = 0.0f;
|
|
m_moveToGoal = false;
|
|
}
|
|
else if (!(m_states & Sense::SuspectEnemy) && !game.isNullEntity (m_enemy)) {
|
|
dest = m_enemy->v.origin + m_enemy->v.velocity.get2d () * 0.55f;
|
|
}
|
|
|
|
m_isUsingGrenade = true;
|
|
m_checkTerrain = false;
|
|
|
|
ignoreCollision ();
|
|
|
|
if ((pev->origin - dest).lengthSq () < cr::square (400.0f)) {
|
|
// heck, I don't wanna blow up myself
|
|
m_grenadeCheckTime = game.time () + kGrenadeCheckTime;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
m_grenade = calcThrow (getEyesPos (), dest);
|
|
|
|
if (m_grenade.lengthSq () < 100.0f) {
|
|
m_grenade = calcToss (pev->origin, dest);
|
|
}
|
|
|
|
if (m_grenade.lengthSq () <= 100.0f) {
|
|
m_grenadeCheckTime = game.time () + kGrenadeCheckTime;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
}
|
|
else {
|
|
auto grenade = correctGrenadeVelocity ("flashbang.mdl");
|
|
|
|
if (game.isNullEntity (grenade)) {
|
|
if (m_currentWeapon != Weapon::Flashbang && !m_grenadeRequested) {
|
|
if (pev->weapons & cr::bit (Weapon::Flashbang)) {
|
|
m_grenadeRequested = true;
|
|
selectWeaponByName ("weapon_flashbang");
|
|
}
|
|
else {
|
|
m_grenadeRequested = false;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
}
|
|
else if (!(m_oldButtons & IN_ATTACK)) {
|
|
pev->button |= IN_ATTACK;
|
|
m_grenadeRequested = false;
|
|
}
|
|
}
|
|
}
|
|
pev->button |= m_campButtons;
|
|
}
|
|
|
|
void Bot::throwSmoke_ () {
|
|
m_aimFlags |= AimFlags::Grenade;
|
|
|
|
if (!(m_states & Sense::SeeingEnemy)) {
|
|
m_strafeSpeed = 0.0f;
|
|
m_moveSpeed = 0.0f;
|
|
m_moveToGoal = false;
|
|
}
|
|
|
|
m_checkTerrain = false;
|
|
m_isUsingGrenade = true;
|
|
|
|
ignoreCollision ();
|
|
|
|
Vector src = m_lastEnemyOrigin - pev->velocity;
|
|
|
|
// predict where the enemy is in 0.5 secs
|
|
if (!game.isNullEntity (m_enemy)) {
|
|
src = src + m_enemy->v.velocity * 0.5f;
|
|
}
|
|
|
|
m_grenade = (src - getEyesPos ()).normalize ();
|
|
|
|
if (getTask ()->time < game.time ()) {
|
|
completeTask ();
|
|
return;
|
|
}
|
|
|
|
if (m_currentWeapon != Weapon::Smoke && !m_grenadeRequested) {
|
|
if (pev->weapons & cr::bit (Weapon::Smoke)) {
|
|
m_grenadeRequested = true;
|
|
|
|
selectWeaponByName ("weapon_smokegrenade");
|
|
getTask ()->time = game.time () + 1.2f;
|
|
}
|
|
else {
|
|
m_grenadeRequested = false;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
}
|
|
else if (!(m_oldButtons & IN_ATTACK)) {
|
|
pev->button |= IN_ATTACK;
|
|
m_grenadeRequested = false;
|
|
}
|
|
pev->button |= m_campButtons;
|
|
}
|
|
|
|
void Bot::doublejump_ () {
|
|
if (!util.isAlive (m_doubleJumpEntity) || (m_aimFlags & AimFlags::Enemy) || (m_travelStartIndex != kInvalidNodeIndex && getTask ()->time + (graph.calculateTravelTime (pev->maxspeed, graph[m_travelStartIndex].origin, m_doubleJumpOrigin) + 11.0f) < game.time ())) {
|
|
resetDoubleJump ();
|
|
return;
|
|
}
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (m_jumpReady) {
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_navTimeset = game.time ();
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
bool inJump = (m_doubleJumpEntity->v.button & IN_JUMP) || (m_doubleJumpEntity->v.oldbuttons & IN_JUMP);
|
|
|
|
if (m_duckForJump < game.time ()) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
else if (inJump && !(m_oldButtons & IN_JUMP)) {
|
|
pev->button |= IN_JUMP;
|
|
}
|
|
const auto &src = pev->origin + Vector (0.0f, 0.0f, 45.0f);
|
|
const auto &dest = src + Vector (0.0f, pev->angles.y, 0.0f).upward () * 256.0f;
|
|
|
|
TraceResult tr;
|
|
game.testLine (src, dest, TraceIgnore::None, ent (), &tr);
|
|
|
|
if (tr.flFraction < 1.0f && tr.pHit == m_doubleJumpEntity && inJump) {
|
|
m_duckForJump = game.time () + rg.float_ (3.0f, 5.0f);
|
|
getTask ()->time = game.time ();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (m_currentNodeIndex == m_prevGoalIndex) {
|
|
m_pathOrigin = m_doubleJumpOrigin;
|
|
m_destOrigin = m_doubleJumpOrigin;
|
|
}
|
|
|
|
if (updateNavigation ()) {
|
|
getTask ()->data = kInvalidNodeIndex;
|
|
}
|
|
|
|
// didn't choose goal waypoint yet?
|
|
if (!hasActiveGoal ()) {
|
|
clearSearchNodes ();
|
|
|
|
int destIndex = graph.getNearest (m_doubleJumpOrigin);
|
|
|
|
if (graph.exists (destIndex)) {
|
|
m_prevGoalIndex = destIndex;
|
|
getTask ()->data = destIndex;
|
|
m_travelStartIndex = m_currentNodeIndex;
|
|
|
|
// always take the shortest path
|
|
findPath (m_currentNodeIndex, destIndex, FindPath::Fast);
|
|
|
|
if (m_currentNodeIndex == destIndex) {
|
|
m_jumpReady = true;
|
|
}
|
|
}
|
|
else {
|
|
resetDoubleJump ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::escapeFromBomb_ () {
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (!bots.isBombPlanted ()) {
|
|
completeTask ();
|
|
}
|
|
|
|
if (isShieldDrawn ()) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
|
|
if (m_currentWeapon != Weapon::Knife && m_numEnemiesLeft == 0) {
|
|
selectWeaponByName ("weapon_knife");
|
|
}
|
|
|
|
// reached destination?
|
|
if (updateNavigation ()) {
|
|
completeTask (); // we're done
|
|
|
|
// press duck button if we still have some enemies
|
|
if (numEnemiesNear (pev->origin, 2048.0f)) {
|
|
m_campButtons = IN_DUCK;
|
|
}
|
|
|
|
// we're reached destination point so just sit down and camp
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true);
|
|
}
|
|
|
|
// didn't choose goal waypoint yet?
|
|
else if (!hasActiveGoal ()) {
|
|
clearSearchNodes ();
|
|
|
|
int lastSelectedGoal = kInvalidNodeIndex, minPathDistance = kInfiniteDistanceLong;
|
|
float safeRadius = rg.float_ (1513.0f, 2048.0f);
|
|
|
|
for (int i = 0; i < graph.length (); ++i) {
|
|
if ((graph[i].origin - graph.getBombOrigin ()).length () < safeRadius || isOccupiedNode (i)) {
|
|
continue;
|
|
}
|
|
int pathDistance = graph.getPathDist (m_currentNodeIndex, i);
|
|
|
|
if (minPathDistance > pathDistance) {
|
|
minPathDistance = pathDistance;
|
|
lastSelectedGoal = i;
|
|
}
|
|
}
|
|
|
|
if (lastSelectedGoal < 0) {
|
|
lastSelectedGoal = graph.getFarest (pev->origin, safeRadius);
|
|
}
|
|
|
|
// still no luck?
|
|
if (lastSelectedGoal < 0) {
|
|
completeTask (); // we're done
|
|
|
|
// we have no destination point, so just sit down and camp
|
|
startTask (Task::Camp, TaskPri::Camp, kInvalidNodeIndex, game.time () + 10.0f, true);
|
|
return;
|
|
}
|
|
m_prevGoalIndex = lastSelectedGoal;
|
|
getTask ()->data = lastSelectedGoal;
|
|
|
|
findPath (m_currentNodeIndex, lastSelectedGoal, FindPath::Fast);
|
|
}
|
|
}
|
|
|
|
void Bot::shootBreakable_ () {
|
|
m_aimFlags |= AimFlags::Override;
|
|
|
|
// breakable destroyed?
|
|
if (!game.isShootableBreakable (m_breakableEntity)) {
|
|
completeTask ();
|
|
return;
|
|
}
|
|
pev->button |= m_campButtons;
|
|
|
|
m_checkTerrain = false;
|
|
m_moveToGoal = false;
|
|
m_navTimeset = game.time ();
|
|
m_camp = m_breakableOrigin;
|
|
|
|
// is bot facing the breakable?
|
|
if (util.getShootingCone (ent (), m_breakableOrigin) >= 0.90f) {
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
if (m_currentWeapon == Weapon::Knife) {
|
|
selectBestWeapon ();
|
|
}
|
|
m_wantsToFire = true;
|
|
m_shootTime = game.time ();
|
|
}
|
|
else {
|
|
m_checkTerrain = true;
|
|
m_moveToGoal = true;
|
|
|
|
completeTask ();
|
|
}
|
|
}
|
|
|
|
void Bot::pickupItem_ () {
|
|
if (game.isNullEntity (m_pickupItem)) {
|
|
m_pickupItem = nullptr;
|
|
completeTask ();
|
|
|
|
return;
|
|
}
|
|
const Vector &dest = game.getEntityWorldOrigin (m_pickupItem);
|
|
|
|
m_destOrigin = dest;
|
|
m_entity = dest;
|
|
|
|
// find the distance to the item
|
|
float itemDistance = (dest - pev->origin).length ();
|
|
|
|
switch (m_pickupType) {
|
|
case Pickup::DroppedC4:
|
|
case Pickup::None:
|
|
break;
|
|
|
|
case Pickup::Weapon:
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
// near to weapon?
|
|
if (itemDistance < 50.0f) {
|
|
int index = 0;
|
|
auto &info = conf.getWeapons ();
|
|
|
|
for (index = 0; index < 7; ++index) {
|
|
if (strcmp (info[index].model, STRING (m_pickupItem->v.model) + 9) == 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (index < 7) {
|
|
// secondary weapon. i.e., pistol
|
|
int wid = 0;
|
|
|
|
for (index = 0; index < 7; ++index) {
|
|
if (pev->weapons & cr::bit (info[index].id)) {
|
|
wid = index;
|
|
}
|
|
}
|
|
|
|
if (wid > 0) {
|
|
selectWeaponById (wid);
|
|
game.botCommand (ent (), "drop");
|
|
|
|
if (hasShield ()) {
|
|
game.botCommand (ent (), "drop"); // discard both shield and pistol
|
|
}
|
|
}
|
|
enteredBuyZone (BuyState::PrimaryWeapon);
|
|
}
|
|
else {
|
|
// primary weapon
|
|
int wid = bestWeaponCarried ();
|
|
|
|
if (wid == Weapon::Shield || wid > 6 || hasShield ()) {
|
|
selectWeaponById (wid);
|
|
game.botCommand (ent (), "drop");
|
|
}
|
|
|
|
if (!wid) {
|
|
m_itemIgnore = m_pickupItem;
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
|
|
break;
|
|
}
|
|
enteredBuyZone (BuyState::PrimaryWeapon);
|
|
}
|
|
checkSilencer (); // check the silencer
|
|
}
|
|
break;
|
|
|
|
case Pickup::Shield:
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (hasShield ()) {
|
|
m_pickupItem = nullptr;
|
|
break;
|
|
}
|
|
|
|
// near to shield?
|
|
else if (itemDistance < 50.0f) {
|
|
// get current best weapon to check if it's a primary in need to be dropped
|
|
int wid = bestWeaponCarried ();
|
|
|
|
if (wid > 6) {
|
|
selectWeaponById (wid);
|
|
game.botCommand (ent (), "drop");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case Pickup::PlantedC4:
|
|
m_aimFlags |= AimFlags::Entity;
|
|
|
|
if (m_team == Team::CT && itemDistance < 80.0f) {
|
|
pushChatterMessage (Chatter::DefusingBomb);
|
|
|
|
// notify team of defusing
|
|
if (m_numFriendsLeft < 3) {
|
|
pushRadioMessage (Radio::NeedBackup);
|
|
}
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
|
|
startTask (Task::DefuseBomb, TaskPri::DefuseBomb, kInvalidNodeIndex, 0.0f, false);
|
|
}
|
|
break;
|
|
|
|
case Pickup::Hostage:
|
|
m_aimFlags |= AimFlags::Entity;
|
|
|
|
if (!util.isAlive (m_pickupItem)) {
|
|
// don't pickup dead hostages
|
|
m_pickupItem = nullptr;
|
|
completeTask ();
|
|
|
|
break;
|
|
}
|
|
|
|
if (itemDistance < 50.0f) {
|
|
float angleToEntity = isInFOV (dest - getEyesPos ());
|
|
|
|
// bot faces hostage?
|
|
if (angleToEntity <= 10.0f) {
|
|
// use game dll function to make sure the hostage is correctly 'used'
|
|
MDLL_Use (m_pickupItem, ent ());
|
|
|
|
if (rg.chance (80)) {
|
|
pushChatterMessage (Chatter::UsingHostages);
|
|
}
|
|
m_hostages.push (m_pickupItem);
|
|
m_pickupItem = nullptr;
|
|
}
|
|
ignoreCollision (); // also don't consider being stuck
|
|
}
|
|
break;
|
|
|
|
case Pickup::DefusalKit:
|
|
m_aimFlags |= AimFlags::Nav;
|
|
|
|
if (m_hasDefuser) {
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
}
|
|
break;
|
|
|
|
case Pickup::Button:
|
|
m_aimFlags |= AimFlags::Entity;
|
|
|
|
if (game.isNullEntity (m_pickupItem) || m_buttonPushTime < game.time ()) {
|
|
completeTask ();
|
|
m_pickupType = Pickup::None;
|
|
|
|
break;
|
|
}
|
|
|
|
// find angles from bot origin to entity...
|
|
float angleToEntity = isInFOV (dest - getEyesPos ());
|
|
|
|
// near to the button?
|
|
if (itemDistance < 90.0f) {
|
|
m_moveSpeed = 0.0f;
|
|
m_strafeSpeed = 0.0f;
|
|
m_moveToGoal = false;
|
|
m_checkTerrain = false;
|
|
|
|
// facing it directly?
|
|
if (angleToEntity <= 10.0f) {
|
|
MDLL_Use (m_pickupItem, ent ());
|
|
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
m_buttonPushTime = game.time () + 3.0f;
|
|
|
|
completeTask ();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Bot::executeTasks () {
|
|
// this is core function that handle task execution
|
|
|
|
switch (getCurrentTaskId ()) {
|
|
// normal task
|
|
default:
|
|
case Task::Normal:
|
|
normal_ ();
|
|
break;
|
|
|
|
// bot sprays messy logos all over the place...
|
|
case Task::Spraypaint:
|
|
spraypaint_ ();
|
|
break;
|
|
|
|
// hunt down enemy
|
|
case Task::Hunt:
|
|
huntEnemy_ ();
|
|
break;
|
|
|
|
// bot seeks cover from enemy
|
|
case Task::SeekCover:
|
|
seekCover_ ();
|
|
break;
|
|
|
|
// plain attacking
|
|
case Task::Attack:
|
|
attackEnemy_ ();
|
|
break;
|
|
|
|
// Bot is pausing
|
|
case Task::Pause:
|
|
pause_ ();
|
|
break;
|
|
|
|
// blinded (flashbanged) behaviour
|
|
case Task::Blind:
|
|
blind_ ();
|
|
break;
|
|
|
|
// camping behaviour
|
|
case Task::Camp:
|
|
camp_ ();
|
|
break;
|
|
|
|
// hiding behaviour
|
|
case Task::Hide:
|
|
hide_ ();
|
|
break;
|
|
|
|
// moves to a position specified in position has a higher priority than task_normal
|
|
case Task::MoveToPosition:
|
|
moveToPos_ ();
|
|
break;
|
|
|
|
// planting the bomb right now
|
|
case Task::PlantBomb:
|
|
plantBomb_ ();
|
|
break;
|
|
|
|
// bomb defusing behaviour
|
|
case Task::DefuseBomb:
|
|
bombDefuse_ ();
|
|
break;
|
|
|
|
// follow user behaviour
|
|
case Task::FollowUser:
|
|
followUser_ ();
|
|
break;
|
|
|
|
// HE grenade throw behaviour
|
|
case Task::ThrowExplosive:
|
|
throwExplosive_ ();
|
|
break;
|
|
|
|
// flashbang throw behavior (basically the same code like for HE's)
|
|
case Task::ThrowFlashbang:
|
|
throwFlashbang_ ();
|
|
break;
|
|
|
|
// smoke grenade throw behavior
|
|
// a bit different to the others because it mostly tries to throw the sg on the ground
|
|
case Task::ThrowSmoke:
|
|
throwSmoke_ ();
|
|
break;
|
|
|
|
// bot helps human player (or other bot) to get somewhere
|
|
case Task::DoubleJump:
|
|
doublejump_ ();
|
|
break;
|
|
|
|
// escape from bomb behaviour
|
|
case Task::EscapeFromBomb:
|
|
escapeFromBomb_ ();
|
|
break;
|
|
|
|
// shooting breakables in the way action
|
|
case Task::ShootBreakable:
|
|
shootBreakable_ ();
|
|
break;
|
|
|
|
// picking up items and stuff behaviour
|
|
case Task::PickupItem:
|
|
pickupItem_ ();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void Bot::checkSpawnConditions () {
|
|
// this function is called instead of ai when buying finished, but freezetime is not yet left.
|
|
|
|
// switch to knife if time to do this
|
|
if (m_checkKnifeSwitch && m_buyingFinished && m_spawnTime + rg.float_ (5.0f, 7.5f) < game.time ()) {
|
|
if (rg.int_ (1, 100) < 2 && yb_spraypaints.bool_ ()) {
|
|
startTask (Task::Spraypaint, TaskPri::Spraypaint, kInvalidNodeIndex, game.time () + 1.0f, false);
|
|
}
|
|
|
|
if (m_difficulty >= 2 && rg.chance (m_personality == Personality::Rusher ? 99 : 50) && !m_isReloading && game.mapIs (MapFlags::HostageRescue | MapFlags::Demolition | MapFlags::Escape | MapFlags::Assassination)) {
|
|
if (yb_jasonmode.bool_ ()) {
|
|
selectSecondary ();
|
|
game.botCommand (ent (), "drop");
|
|
}
|
|
else {
|
|
selectWeaponByName ("weapon_knife");
|
|
}
|
|
}
|
|
m_checkKnifeSwitch = false;
|
|
|
|
if (rg.chance (yb_user_follow_percent.int_ ()) && game.isNullEntity (m_targetEntity) && !m_isLeader && !m_hasC4 && rg.chance (50)) {
|
|
decideFollowUser ();
|
|
}
|
|
}
|
|
|
|
// check if we already switched weapon mode
|
|
if (m_checkWeaponSwitch && m_buyingFinished && m_spawnTime + rg.float_ (3.0f, 4.5f) < game.time ()) {
|
|
if (hasShield () && isShieldDrawn ()) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
else {
|
|
switch (m_currentWeapon) {
|
|
case Weapon::M4A1:
|
|
case Weapon::USP:
|
|
checkSilencer ();
|
|
break;
|
|
|
|
case Weapon::Famas:
|
|
case Weapon::Glock18:
|
|
if (rg.chance (50)) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// movement in freezetime is disabled, so fire movement action if button was hit
|
|
if (pev->button & IN_ATTACK2) {
|
|
runMovement ();
|
|
}
|
|
m_checkWeaponSwitch = false;
|
|
}
|
|
}
|
|
|
|
void Bot::logic () {
|
|
// this function gets called each frame and is the core of all bot ai. from here all other subroutines are called
|
|
|
|
float movedDistance = 2.0f; // length of different vector (distance bot moved)
|
|
|
|
// increase reaction time
|
|
m_actualReactionTime += 0.3f;
|
|
|
|
if (m_actualReactionTime > m_idealReactionTime) {
|
|
m_actualReactionTime = m_idealReactionTime;
|
|
}
|
|
|
|
// bot could be blinded by flashbang or smoke, recover from it
|
|
m_viewDistance += 3.0f;
|
|
|
|
if (m_viewDistance > m_maxViewDistance) {
|
|
m_viewDistance = m_maxViewDistance;
|
|
}
|
|
|
|
if (m_blindTime > game.time ()) {
|
|
m_maxViewDistance = 4096.0f;
|
|
}
|
|
m_moveSpeed = pev->maxspeed;
|
|
|
|
if (m_prevTime <= game.time ()) {
|
|
// see how far bot has moved since the previous position...
|
|
movedDistance = (m_prevOrigin - pev->origin).length ();
|
|
|
|
// save current position as previous
|
|
m_prevOrigin = pev->origin;
|
|
m_prevTime = game.time () + 0.2f;
|
|
}
|
|
|
|
// if there's some radio message to respond, check it
|
|
if (m_radioOrder != 0) {
|
|
checkRadioQueue ();
|
|
}
|
|
|
|
// do all sensing, calculate/filter all actions here
|
|
setConditions ();
|
|
|
|
// some stuff required by by chatter engine
|
|
if (yb_radio_mode.int_ () == 2) {
|
|
if ((m_states & Sense::SeeingEnemy) && !game.isNullEntity (m_enemy)) {
|
|
int hasFriendNearby = numFriendsNear (pev->origin, 512.0f);
|
|
|
|
if (!hasFriendNearby && rg.chance (45) && (m_enemy->v.weapons & cr::bit (Weapon::C4))) {
|
|
pushChatterMessage (Chatter::SpotTheBomber);
|
|
}
|
|
else if (!hasFriendNearby && rg.chance (45) && m_team == Team::Terrorist && util.isPlayerVIP (m_enemy)) {
|
|
pushChatterMessage (Chatter::VIPSpotted);
|
|
}
|
|
else if (!hasFriendNearby && rg.chance (50) && game.getTeam (m_enemy) != m_team && isGroupOfEnemies (m_enemy->v.origin, 2, 384.0f)) {
|
|
pushChatterMessage (Chatter::ScaredEmotion);
|
|
}
|
|
else if (!hasFriendNearby && rg.chance (40) && ((m_enemy->v.weapons & cr::bit (Weapon::AWP)) || (m_enemy->v.weapons & cr::bit (Weapon::Scout)) || (m_enemy->v.weapons & cr::bit (Weapon::G3SG1)) || (m_enemy->v.weapons & cr::bit (Weapon::SG550)))) {
|
|
pushChatterMessage (Chatter::SniperWarning);
|
|
}
|
|
|
|
// if bot is trapped under shield yell for help !
|
|
if (getCurrentTaskId () == Task::Camp && hasShield () && isShieldDrawn () && hasFriendNearby >= 2) {
|
|
pushChatterMessage (Chatter::PinnedDown);
|
|
}
|
|
}
|
|
|
|
// if bomb planted warn teammates !
|
|
if (bots.hasBombSay (BombPlantedSay::Chatter) && bots.isBombPlanted () && m_team == Team::CT) {
|
|
pushChatterMessage (Chatter::GottaFindC4);
|
|
bots.clearBombSay (BombPlantedSay::Chatter);
|
|
}
|
|
}
|
|
Vector src, destination;
|
|
|
|
m_checkTerrain = true;
|
|
m_moveToGoal = true;
|
|
m_wantsToFire = false;
|
|
|
|
avoidGrenades (); // avoid flyings grenades
|
|
m_isUsingGrenade = false;
|
|
|
|
executeTasks (); // execute current task
|
|
updateAimDir (); // choose aim direction
|
|
updateLookAngles (); // and turn to chosen aim direction
|
|
|
|
// the bots wants to fire at something?
|
|
if (m_wantsToFire && !m_isUsingGrenade && m_shootTime <= game.time ()) {
|
|
fireWeapons (); // if bot didn't fire a bullet try again next frame
|
|
}
|
|
|
|
// check for reloading
|
|
if (m_reloadCheckTime <= game.time ()) {
|
|
checkReload ();
|
|
}
|
|
|
|
// set the reaction time (surprise momentum) different each frame according to skill
|
|
setIdealReactionTimers ();
|
|
|
|
// calculate 2 direction vectors, 1 without the up/down component
|
|
const Vector &dirOld = m_destOrigin - (pev->origin + pev->velocity * getFrameInterval ());
|
|
const Vector &dirNormal = dirOld.normalize2d ();
|
|
|
|
m_moveAngles = dirOld.angles ();
|
|
m_moveAngles.clampAngles ();
|
|
m_moveAngles.x *= -1.0f; // invert for engine
|
|
|
|
// do some overriding for special cases
|
|
overrideConditions ();
|
|
|
|
// allowed to move to a destination position?
|
|
if (m_moveToGoal) {
|
|
findValidNode ();
|
|
|
|
// press duck button if we need to
|
|
if ((m_path->flags & NodeFlag::Crouch) && !(m_path->flags & (NodeFlag::Camp | NodeFlag::Goal))) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
m_lastUsedNodesTime = game.time ();
|
|
|
|
// special movement for swimming here
|
|
if (isInWater ()) {
|
|
// check if we need to go forward or back press the correct buttons
|
|
if (isInFOV (m_destOrigin - getEyesPos ()) > 90.0f) {
|
|
pev->button |= IN_BACK;
|
|
}
|
|
else {
|
|
pev->button |= IN_FORWARD;
|
|
}
|
|
|
|
if (m_moveAngles.x > 60.0f) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
else if (m_moveAngles.x < -60.0f) {
|
|
pev->button |= IN_JUMP;
|
|
}
|
|
}
|
|
}
|
|
|
|
// are we allowed to check blocking terrain (and react to it)?
|
|
if (m_checkTerrain) {
|
|
checkTerrain (movedDistance, dirNormal);
|
|
}
|
|
|
|
// check the darkness
|
|
checkDarkness ();
|
|
|
|
// must avoid a grenade?
|
|
if (m_needAvoidGrenade != 0) {
|
|
// don't duck to get away faster
|
|
pev->button &= ~IN_DUCK;
|
|
|
|
m_moveSpeed = -pev->maxspeed;
|
|
m_strafeSpeed = pev->maxspeed * m_needAvoidGrenade;
|
|
}
|
|
|
|
// time to reach waypoint
|
|
if (m_navTimeset + getReachTime () < game.time () && game.isNullEntity (m_enemy)) {
|
|
findValidNode ();
|
|
|
|
m_breakableEntity = nullptr;
|
|
|
|
if (getCurrentTaskId () == Task::PickupItem || (m_states & Sense::PickupItem)) {
|
|
// clear these pointers, bot mingh be stuck getting to them
|
|
if (!game.isNullEntity (m_pickupItem) && !m_hasProgressBar) {
|
|
m_itemIgnore = m_pickupItem;
|
|
}
|
|
|
|
m_itemCheckTime = game.time () + 2.0f;
|
|
m_pickupType = Pickup::None;
|
|
m_pickupItem = nullptr;
|
|
}
|
|
}
|
|
|
|
if (m_duckTime >= game.time ()) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
|
|
if (pev->button & IN_JUMP) {
|
|
m_jumpTime = game.time ();
|
|
}
|
|
|
|
if (m_jumpTime + 0.85f > game.time ()) {
|
|
if (!isOnFloor () && !isInWater ()) {
|
|
pev->button |= IN_DUCK;
|
|
}
|
|
}
|
|
|
|
if (!(pev->button & (IN_FORWARD | IN_BACK))) {
|
|
if (m_moveSpeed > 0.0f) {
|
|
pev->button |= IN_FORWARD;
|
|
}
|
|
else if (m_moveSpeed < 0.0f) {
|
|
pev->button |= IN_BACK;
|
|
}
|
|
}
|
|
|
|
if (!(pev->button & (IN_MOVELEFT | IN_MOVERIGHT))) {
|
|
if (m_strafeSpeed > 0.0f) {
|
|
pev->button |= IN_MOVERIGHT;
|
|
}
|
|
else if (m_strafeSpeed < 0.0f) {
|
|
pev->button |= IN_MOVELEFT;
|
|
}
|
|
}
|
|
|
|
// check if need to use parachute
|
|
checkParachute ();
|
|
|
|
// display some debugging thingy to host entity
|
|
if (!game.isDedicated () && yb_debug.int_ () >= 1) {
|
|
showDebugOverlay ();
|
|
}
|
|
|
|
// save the previous speed (for checking if stuck)
|
|
m_prevSpeed = cr::abs (m_moveSpeed);
|
|
m_lastDamageType = -1; // reset damage
|
|
}
|
|
|
|
void Bot::showDebugOverlay () {
|
|
bool displayDebugOverlay = false;
|
|
|
|
if (game.getLocalEntity ()->v.iuser2 == entindex ()) {
|
|
displayDebugOverlay = true;
|
|
}
|
|
|
|
if (!displayDebugOverlay && yb_debug.int_ () >= 2) {
|
|
Bot *nearest = nullptr;
|
|
|
|
if (util.findNearestPlayer (reinterpret_cast <void **> (&nearest), game.getLocalEntity (), 128.0f, false, true, true, true) && nearest == this) {
|
|
displayDebugOverlay = true;
|
|
}
|
|
}
|
|
|
|
if (displayDebugOverlay) {
|
|
static float timeDebugUpdate = 0.0f;
|
|
static int index, goal, taskID;
|
|
|
|
static Dictionary <int, String, IntHash <int>> tasks;
|
|
static Dictionary <int, String, IntHash <int>> personalities;
|
|
static Dictionary <int, String, IntHash <int>> flags;
|
|
|
|
if (tasks.empty ()) {
|
|
tasks.push (Task::Normal, "Normal");
|
|
tasks.push (Task::Pause, "Pause");
|
|
tasks.push (Task::MoveToPosition, "Move");
|
|
tasks.push (Task::FollowUser, "Follow");
|
|
tasks.push (Task::PickupItem, "Pickup");
|
|
tasks.push (Task::Camp, "Camp");
|
|
tasks.push (Task::PlantBomb, "PlantBomb");
|
|
tasks.push (Task::DefuseBomb, "DefuseBomb");
|
|
tasks.push (Task::Attack, "Attack");
|
|
tasks.push (Task::Hunt, "Hunt");
|
|
tasks.push (Task::SeekCover, "SeekCover");
|
|
tasks.push (Task::ThrowExplosive, "ThrowHE");
|
|
tasks.push (Task::ThrowFlashbang, "ThrowFL");
|
|
tasks.push (Task::ThrowSmoke, "ThrowSG");
|
|
tasks.push (Task::DoubleJump, "DoubleJump");
|
|
tasks.push (Task::EscapeFromBomb, "EscapeFromBomb");
|
|
tasks.push (Task::ShootBreakable, "DestroyBreakable");
|
|
tasks.push (Task::Hide, "Hide");
|
|
tasks.push (Task::Blind, "Blind");
|
|
tasks.push (Task::Spraypaint, "Spray");
|
|
|
|
personalities.push (Personality::Rusher, "Rusher");
|
|
personalities.push (Personality::Normal, "Normal");
|
|
personalities.push (Personality::Careful, "Careful");
|
|
|
|
flags.push (AimFlags::Nav, "Nav");
|
|
flags.push (AimFlags::Camp, "Camp");
|
|
flags.push (AimFlags::PredictPath, "Predict");
|
|
flags.push (AimFlags::LastEnemy, "LastEnemy");
|
|
flags.push (AimFlags::Entity, "Entity");
|
|
flags.push (AimFlags::Enemy, "Enemy");
|
|
flags.push (AimFlags::Grenade, "Grenade");
|
|
flags.push (AimFlags::Override, "Override");;
|
|
}
|
|
|
|
if (!m_tasks.empty ()) {
|
|
if (taskID != getCurrentTaskId () || index != m_currentNodeIndex || goal != getTask ()->data || timeDebugUpdate < game.time ()) {
|
|
taskID = getCurrentTaskId ();
|
|
index = m_currentNodeIndex;
|
|
goal = getTask ()->data;
|
|
|
|
String enemy = "(none)";
|
|
|
|
if (!game.isNullEntity (m_enemy)) {
|
|
enemy = STRING (m_enemy->v.netname);
|
|
}
|
|
else if (!game.isNullEntity (m_lastEnemy)) {
|
|
enemy.assignf ("%s (L)", STRING (m_lastEnemy->v.netname));
|
|
}
|
|
String pickup = "(none)";
|
|
|
|
if (!game.isNullEntity (m_pickupItem)) {
|
|
pickup = STRING (m_pickupItem->v.netname);
|
|
}
|
|
String aimFlags;
|
|
|
|
for (int i = 0; i < 8; ++i) {
|
|
bool hasFlag = m_aimFlags & cr::bit (i);
|
|
|
|
if (hasFlag) {
|
|
aimFlags.appendf (" %s", flags[cr::bit (i)].chars ());
|
|
}
|
|
}
|
|
String weapon = STRING (util.getWeaponAlias (true, nullptr, m_currentWeapon));
|
|
|
|
String debugData;
|
|
debugData.assignf ("\n\n\n\n\n%s (H:%.1f/A:%.1f)- Task: %d=%s Desire:%.02f\nItem: %s Clip: %d Ammo: %d%s Money: %d AimFlags: %s\nSP=%.02f SSP=%.02f I=%d PG=%d G=%d T: %.02f MT: %d\nEnemy=%s Pickup=%s Type=%s\n", STRING (pev->netname), pev->health, pev->armorvalue, taskID, tasks[taskID].chars (), getTask ()->desire, weapon.chars (), getAmmoInClip (), getAmmo (), m_isReloading ? " (R)" : "", m_moneyAmount, aimFlags.trim ().chars (), m_moveSpeed, m_strafeSpeed, index, m_prevGoalIndex, goal, m_navTimeset - game.time (), pev->movetype, enemy.chars (), pickup.chars (), personalities[m_personality].chars ());
|
|
|
|
MessageWriter (MSG_ONE_UNRELIABLE, SVC_TEMPENTITY, nullptr, game.getLocalEntity ())
|
|
.writeByte (TE_TEXTMESSAGE)
|
|
.writeByte (1)
|
|
.writeShort (MessageWriter::fs16 (-1.0f, 13.0f))
|
|
.writeShort (MessageWriter::fs16 (0.0f , 13.0f))
|
|
.writeByte (0)
|
|
.writeByte (m_team == Team::CT ? 0 : 255)
|
|
.writeByte (100)
|
|
.writeByte (m_team != Team::CT ? 0 : 255)
|
|
.writeByte (0)
|
|
.writeByte (255)
|
|
.writeByte (255)
|
|
.writeByte (255)
|
|
.writeByte (0)
|
|
.writeShort (MessageWriter::fu16 (0.0f, 8.0f))
|
|
.writeShort (MessageWriter::fu16 (0.0f, 8.0f))
|
|
.writeShort (MessageWriter::fu16 (1.0f, 8.0f))
|
|
.writeString (debugData.chars ());
|
|
|
|
timeDebugUpdate = game.time () + 1.0f;
|
|
}
|
|
|
|
// green = destination origin
|
|
// blue = ideal angles
|
|
// red = view angles
|
|
game.drawLine (game.getLocalEntity (), getEyesPos (), m_destOrigin, 10, 0, Color (0, 255, 0), 250, 5, 1, DrawLine::Arrow);
|
|
game.drawLine (game.getLocalEntity (), getEyesPos () - Vector (0.0f, 0.0f, 16.0f), getEyesPos () + m_idealAngles.forward () * 300.0f, 10, 0, Color (0, 0, 255), 250, 5, 1, DrawLine::Arrow);
|
|
game.drawLine (game.getLocalEntity (), getEyesPos () - Vector (0.0f, 0.0f, 32.0f), getEyesPos () + pev->v_angle.forward () * 300.0f, 10, 0, Color (255, 0, 0), 250, 5, 1, DrawLine::Arrow);
|
|
|
|
// now draw line from source to destination
|
|
for (size_t i = 0; i < m_pathWalk.length () && i + 1 < m_pathWalk.length (); ++i) {
|
|
game.drawLine (game.getLocalEntity (), graph[m_pathWalk.at (i)].origin, graph[m_pathWalk.at (i + 1)].origin, 15, 0, Color (255, 100, 55), 200, 5, 1, DrawLine::Arrow);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool Bot::hasHostage () {
|
|
for (auto hostage : m_hostages) {
|
|
if (!game.isNullEntity (hostage)) {
|
|
|
|
// don't care about dead hostages
|
|
if (hostage->v.health <= 0.0f || (pev->origin - hostage->v.origin).lengthSq () > cr::square (600.0f)) {
|
|
hostage = nullptr;
|
|
continue;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int Bot::getAmmo () {
|
|
const auto &prop = conf.getWeaponProp (m_currentWeapon);
|
|
|
|
if (prop.ammo1 == -1 || prop.ammo1 > kMaxWeapons - 1) {
|
|
return 0;
|
|
}
|
|
return m_ammo[prop.ammo1];
|
|
}
|
|
|
|
void Bot::takeDamage (edict_t *inflictor, int damage, int armor, int bits) {
|
|
// this function gets called from the network message handler, when bot's gets hurt from any
|
|
// other player.
|
|
|
|
m_lastDamageType = bits;
|
|
updatePracticeValue (damage);
|
|
|
|
if (util.isPlayer (inflictor)) {
|
|
if (yb_tkpunish.bool_ () && game.getTeam (inflictor) == m_team && !util.isFakeClient (inflictor)) {
|
|
// alright, die you teamkiller!!!
|
|
m_actualReactionTime = 0.0f;
|
|
m_seeEnemyTime = game.time ();
|
|
m_enemy = inflictor;
|
|
|
|
m_lastEnemy = m_enemy;
|
|
m_lastEnemyOrigin = m_enemy->v.origin;
|
|
m_enemyOrigin = m_enemy->v.origin;
|
|
|
|
pushChatMessage (Chat::TeamAttack);
|
|
handleChatter ("#Bot_TeamAttack");
|
|
pushChatterMessage (Chatter::FriendlyFire);
|
|
}
|
|
else {
|
|
// attacked by an enemy
|
|
if (pev->health > 60.0f) {
|
|
m_agressionLevel += 0.1f;
|
|
|
|
if (m_agressionLevel > 1.0f) {
|
|
m_agressionLevel += 1.0f;
|
|
}
|
|
}
|
|
else {
|
|
m_fearLevel += 0.03f;
|
|
|
|
if (m_fearLevel > 1.0f) {
|
|
m_fearLevel += 1.0f;
|
|
}
|
|
}
|
|
clearTask (Task::Camp);
|
|
|
|
if (game.isNullEntity (m_enemy) && m_team != game.getTeam (inflictor)) {
|
|
m_lastEnemy = inflictor;
|
|
m_lastEnemyOrigin = inflictor->v.origin;
|
|
|
|
// FIXME - Bot doesn't necessary sees this enemy
|
|
m_seeEnemyTime = game.time ();
|
|
}
|
|
|
|
if (!game.is (GameFlags::CSDM)) {
|
|
updatePracticeDamage (inflictor, armor + damage);
|
|
}
|
|
}
|
|
}
|
|
// hurt by unusual damage like drowning or gas
|
|
else {
|
|
// leave the camping/hiding position
|
|
if (!isReachableNode (graph.getNearest (m_destOrigin))) {
|
|
clearSearchNodes ();
|
|
findBestNearestNode ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::takeBlind (int alpha) {
|
|
// this function gets called by network message handler, when screenfade message get's send
|
|
// it's used to make bot blind from the grenade.
|
|
|
|
m_maxViewDistance = rg.float_ (10.0f, 20.0f);
|
|
m_blindTime = game.time () + static_cast <float> (alpha - 200) / 16.0f;
|
|
|
|
if (m_blindTime < game.time ()) {
|
|
return;
|
|
}
|
|
m_enemy = nullptr;
|
|
|
|
if (m_difficulty <= 2) {
|
|
m_blindMoveSpeed = 0.0f;
|
|
m_blindSidemoveSpeed = 0.0f;
|
|
m_blindButton = IN_DUCK;
|
|
|
|
return;
|
|
}
|
|
|
|
m_blindMoveSpeed = -pev->maxspeed;
|
|
m_blindSidemoveSpeed = 0.0f;
|
|
|
|
if (rg.chance (50)) {
|
|
m_blindSidemoveSpeed = pev->maxspeed;
|
|
}
|
|
else {
|
|
m_blindSidemoveSpeed = -pev->maxspeed;
|
|
}
|
|
|
|
if (pev->health < 85.0f) {
|
|
m_blindMoveSpeed = -pev->maxspeed;
|
|
}
|
|
else if (m_personality == Personality::Careful) {
|
|
m_blindMoveSpeed = 0.0f;
|
|
m_blindButton = IN_DUCK;
|
|
}
|
|
else {
|
|
m_blindMoveSpeed = pev->maxspeed;
|
|
}
|
|
}
|
|
|
|
void Bot::updatePracticeValue (int damage) {
|
|
// gets called each time a bot gets damaged by some enemy. tries to achieve a statistic about most/less dangerous
|
|
// waypoints for a destination goal used for pathfinding
|
|
|
|
if (graph.length () < 1 || graph.hasChanged () || m_chosenGoalIndex < 0 || m_prevGoalIndex < 0) {
|
|
return;
|
|
}
|
|
|
|
// only rate goal waypoint if bot died because of the damage
|
|
// FIXME: could be done a lot better, however this cares most about damage done by sniping or really deadly weapons
|
|
if (pev->health - damage <= 0) {
|
|
graph.setDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex, cr::clamp (graph.getDangerValue (m_team, m_chosenGoalIndex, m_prevGoalIndex) - static_cast <int> (pev->health / 20), -kMaxPracticeGoalValue, kMaxPracticeGoalValue));
|
|
}
|
|
}
|
|
|
|
void Bot::updatePracticeDamage (edict_t *attacker, int damage) {
|
|
// this function gets called each time a bot gets damaged by some enemy. sotores the damage (teamspecific) done by victim.
|
|
|
|
if (!util.isPlayer (attacker)) {
|
|
return;
|
|
}
|
|
|
|
int attackerTeam = game.getTeam (attacker);
|
|
int victimTeam = m_team;
|
|
|
|
if (attackerTeam == victimTeam) {
|
|
return;
|
|
}
|
|
|
|
// if these are bots also remember damage to rank destination of the bot
|
|
m_goalValue -= static_cast <float> (damage);
|
|
|
|
if (bots[attacker] != nullptr) {
|
|
bots[attacker]->m_goalValue += static_cast <float> (damage);
|
|
}
|
|
|
|
if (damage < 20) {
|
|
return; // do not collect damage less than 20
|
|
}
|
|
|
|
int attackerIndex = graph.getNearest (attacker->v.origin);
|
|
int victimIndex = m_currentNodeIndex;
|
|
|
|
if (victimIndex == kInvalidNodeIndex) {
|
|
victimIndex = findNearestNode ();
|
|
}
|
|
|
|
if (pev->health > 20.0f) {
|
|
if (victimTeam == Team::Terrorist || victimTeam == Team::CT) {
|
|
graph.setDangerDamage (victimIndex, victimIndex, victimIndex, cr::clamp (graph.getDangerDamage (victimTeam, victimIndex, victimIndex), 0, kMaxPracticeDamageValue));
|
|
}
|
|
}
|
|
float updateDamage = util.isFakeClient (attacker) ? 10.0f : 7.0f;
|
|
|
|
// store away the damage done
|
|
int damageValue = cr::clamp (graph.getDangerDamage (m_team, victimIndex, attackerIndex) + static_cast <int> (damage / updateDamage), 0, kMaxPracticeDamageValue);
|
|
|
|
if (damageValue > graph.getHighestDamageForTeam (m_team)) {
|
|
graph.setHighestDamageForTeam (m_team, damageValue);
|
|
}
|
|
graph.setDangerDamage (m_team, victimIndex, attackerIndex, damageValue);
|
|
}
|
|
|
|
void Bot::handleChatter (const char *tempMessage) {
|
|
// this function is added to prevent engine crashes with: 'Message XX started, before message XX ended', or something.
|
|
|
|
if ((m_team == Team::CT && strcmp (tempMessage, "#CTs_Win") == 0) || (m_team == Team::Terrorist && strcmp (tempMessage, "#Terrorists_Win") == 0)) {
|
|
if (bots.getRoundMidTime () > game.time ()) {
|
|
pushChatterMessage (Chatter::QuickWonRound);
|
|
}
|
|
else {
|
|
pushChatterMessage (Chatter::WonTheRound);
|
|
}
|
|
}
|
|
|
|
else if (strcmp (tempMessage, "#Bot_TeamAttack") == 0) {
|
|
pushChatterMessage (Chatter::FriendlyFire);
|
|
}
|
|
else if (strcmp (tempMessage, "#Bot_NiceShotCommander") == 0) {
|
|
pushChatterMessage (Chatter::NiceShotCommander);
|
|
}
|
|
else if (strcmp (tempMessage, "#Bot_NiceShotPall") == 0) {
|
|
pushChatterMessage (Chatter::NiceShotPall);
|
|
}
|
|
}
|
|
|
|
void Bot::pushChatMessage (int type, bool isTeamSay) {
|
|
if (!conf.hasChatBank (type) || !yb_chat.bool_ ()) {
|
|
return;
|
|
}
|
|
|
|
prepareChatMessage (conf.pickRandomFromChatBank (type));
|
|
pushMsgQueue (isTeamSay ? BotMsg::SayTeam : BotMsg::Say);
|
|
}
|
|
|
|
void Bot::dropWeaponForUser (edict_t *user, bool discardC4) {
|
|
// this function, asks bot to discard his current primary weapon (or c4) to the user that requsted it with /drop*
|
|
// command, very useful, when i'm don't have money to buy anything... )
|
|
|
|
if (util.isAlive (user) && m_moneyAmount >= 2000 && hasPrimaryWeapon () && (user->v.origin - pev->origin).length () <= 450.0f) {
|
|
m_aimFlags |= AimFlags::Entity;
|
|
m_lookAt = user->v.origin;
|
|
|
|
if (discardC4) {
|
|
selectWeaponByName ("weapon_c4");
|
|
game.botCommand (ent (), "drop");
|
|
}
|
|
else {
|
|
selectBestWeapon ();
|
|
game.botCommand (ent (), "drop");
|
|
}
|
|
|
|
m_pickupItem = nullptr;
|
|
m_pickupType = Pickup::None;
|
|
m_itemCheckTime = game.time () + 5.0f;
|
|
|
|
if (m_inBuyZone) {
|
|
m_ignoreBuyDelay = true;
|
|
m_buyingFinished = false;
|
|
m_buyState = BuyState::PrimaryWeapon;
|
|
|
|
pushMsgQueue (BotMsg::Buy);
|
|
m_nextBuyTime = game.time ();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::startDoubleJump (edict_t *ent) {
|
|
resetDoubleJump ();
|
|
|
|
m_doubleJumpOrigin = ent->v.origin;
|
|
m_doubleJumpEntity = ent;
|
|
|
|
startTask (Task::DoubleJump, TaskPri::DoubleJump, kInvalidNodeIndex, game.time (), true);
|
|
sayTeam (strings.format ("Ok %s, i will help you!", STRING (ent->v.netname)));
|
|
}
|
|
|
|
void Bot::resetDoubleJump () {
|
|
completeTask ();
|
|
|
|
m_doubleJumpEntity = nullptr;
|
|
m_duckForJump = 0.0f;
|
|
m_doubleJumpOrigin = nullptr;
|
|
m_travelStartIndex = kInvalidNodeIndex;
|
|
m_jumpReady = false;
|
|
}
|
|
|
|
void Bot::sayDebug (const char *format, ...) {
|
|
if (game.isDedicated ()) {
|
|
return;
|
|
}
|
|
int level = yb_debug.int_ ();
|
|
|
|
if (level <= 2) {
|
|
return;
|
|
}
|
|
va_list ap;
|
|
auto result = strings.chars ();
|
|
|
|
va_start (ap, format);
|
|
vsnprintf (result, StringBuffer::StaticBufferSize, format, ap);
|
|
va_end (ap);
|
|
|
|
String printBuf;
|
|
printBuf.assignf ("%s: %s", STRING (pev->netname), result);
|
|
|
|
bool playMessage = false;
|
|
|
|
if (level == 3 && !game.isNullEntity (game.getLocalEntity ()) && game.getLocalEntity ()->v.iuser2 == entindex ()) {
|
|
playMessage = true;
|
|
}
|
|
else if (level != 3) {
|
|
playMessage = true;
|
|
}
|
|
if (playMessage && level > 3) {
|
|
logger.message (printBuf.chars ());
|
|
}
|
|
if (playMessage) {
|
|
game.print (printBuf.chars ());
|
|
say (printBuf.chars ());
|
|
}
|
|
}
|
|
|
|
Vector Bot::calcToss (const Vector &start, const Vector &stop) {
|
|
// this function returns the velocity at which an object should looped from start to land near end.
|
|
// returns null vector if toss is not feasible.
|
|
|
|
TraceResult tr;
|
|
float gravity = sv_gravity.float_ () * 0.55f;
|
|
|
|
Vector end = stop - pev->velocity;
|
|
end.z -= 15.0f;
|
|
|
|
if (cr::abs (end.z - start.z) > 500.0f) {
|
|
return nullptr;
|
|
}
|
|
Vector midPoint = start + (end - start) * 0.5f;
|
|
game.testHull (midPoint, midPoint + Vector (0.0f, 0.0f, 500.0f), TraceIgnore::Monsters, head_hull, ent (), &tr);
|
|
|
|
if (tr.flFraction < 1.0f) {
|
|
midPoint = tr.vecEndPos;
|
|
midPoint.z = tr.pHit->v.absmin.z - 1.0f;
|
|
}
|
|
|
|
if (midPoint.z < start.z || midPoint.z < end.z) {
|
|
return nullptr;
|
|
}
|
|
float timeOne = cr::sqrtf ((midPoint.z - start.z) / (0.5f * gravity));
|
|
float timeTwo = cr::sqrtf ((midPoint.z - end.z) / (0.5f * gravity));
|
|
|
|
if (timeOne < 0.1f) {
|
|
return nullptr;
|
|
}
|
|
Vector velocity = (end - start) / (timeOne + timeTwo);
|
|
velocity.z = gravity * timeOne;
|
|
|
|
Vector apex = start + velocity * timeOne;
|
|
apex.z = midPoint.z;
|
|
|
|
game.testHull (start, apex, TraceIgnore::None, head_hull, ent (), &tr);
|
|
|
|
if (tr.flFraction < 1.0f || tr.fAllSolid) {
|
|
return nullptr;
|
|
}
|
|
game.testHull (end, apex, TraceIgnore::Monsters, head_hull, ent (), &tr);
|
|
|
|
if (tr.flFraction != 1.0f) {
|
|
float dot = -(tr.vecPlaneNormal | (apex - end).normalize ());
|
|
|
|
if (dot > 0.7f || tr.flFraction < 0.8f) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
return velocity * 0.777f;
|
|
}
|
|
|
|
Vector Bot::calcThrow (const Vector &start, const Vector &stop) {
|
|
// this function returns the velocity vector at which an object should be thrown from start to hit end.
|
|
// returns null vector if throw is not feasible.
|
|
|
|
Vector velocity = stop - start;
|
|
TraceResult tr;
|
|
|
|
float gravity = sv_gravity.float_ () * 0.55f;
|
|
float time = velocity.length () / 195.0f;
|
|
|
|
if (time < 0.01f) {
|
|
return nullptr;
|
|
}
|
|
else if (time > 2.0f) {
|
|
time = 1.2f;
|
|
}
|
|
velocity = velocity * (1.0f / time);
|
|
velocity.z += gravity * time * 0.5f;
|
|
|
|
Vector apex = start + (stop - start) * 0.5f;
|
|
apex.z += 0.5f * gravity * (time * 0.5f) * (time * 0.5f);
|
|
|
|
game.testHull (start, apex, TraceIgnore::None, head_hull, ent (), &tr);
|
|
|
|
if (tr.flFraction != 1.0f) {
|
|
return nullptr;
|
|
}
|
|
game.testHull (stop, apex, TraceIgnore::Monsters, head_hull, ent (), &tr);
|
|
|
|
if (tr.flFraction != 1.0 || tr.fAllSolid) {
|
|
float dot = -(tr.vecPlaneNormal | (apex - stop).normalize ());
|
|
|
|
if (dot > 0.7f || tr.flFraction < 0.8f) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
return velocity * 0.7793f;
|
|
}
|
|
|
|
edict_t *Bot::correctGrenadeVelocity (const char *model) {
|
|
edict_t *result = nullptr;
|
|
|
|
game.searchEntities ("classname", "grenade", [&] (edict_t *ent) {
|
|
if (ent->v.owner == this->ent () && strcmp (STRING (ent->v.model) + 9, model) == 0) {
|
|
result = ent;
|
|
|
|
// set the correct velocity for the grenade
|
|
if (m_grenade.lengthSq () > 100.0f) {
|
|
ent->v.velocity = m_grenade;
|
|
}
|
|
m_grenadeCheckTime = game.time () + kGrenadeCheckTime;
|
|
|
|
selectBestWeapon ();
|
|
completeTask ();
|
|
|
|
return EntitySearchResult::Break;
|
|
}
|
|
return EntitySearchResult::Continue;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
Vector Bot::isBombAudible () {
|
|
// this function checks if bomb is can be heard by the bot, calculations done by manual testing.
|
|
|
|
if (!bots.isBombPlanted () || getCurrentTaskId () == Task::EscapeFromBomb) {
|
|
return nullptr; // reliability check
|
|
}
|
|
|
|
if (m_difficulty > 2) {
|
|
return graph.getBombOrigin ();
|
|
}
|
|
const Vector &bombOrigin = graph.getBombOrigin ();
|
|
|
|
float timeElapsed = ((game.time () - bots.getTimeBombPlanted ()) / mp_c4timer.float_ ()) * 100.0f;
|
|
float desiredRadius = 768.0f;
|
|
|
|
// start the manual calculations
|
|
if (timeElapsed > 85.0f) {
|
|
desiredRadius = 4096.0f;
|
|
}
|
|
else if (timeElapsed > 68.0f) {
|
|
desiredRadius = 2048.0f;
|
|
}
|
|
else if (timeElapsed > 52.0f) {
|
|
desiredRadius = 1280.0f;
|
|
}
|
|
else if (timeElapsed > 28.0f) {
|
|
desiredRadius = 1024.0f;
|
|
}
|
|
|
|
// we hear bomb if length greater than radius
|
|
if (desiredRadius < (pev->origin - bombOrigin).length2d ()) {
|
|
return bombOrigin;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
uint8 Bot::computeMsec () {
|
|
// estimate msec to use for this command based on time passed from the previous command
|
|
|
|
return static_cast <uint8> ((game.time () - m_lastCommandTime) * 1000.0f);
|
|
}
|
|
|
|
void Bot::runMovement () {
|
|
// the purpose of this function is to compute, according to the specified computation
|
|
// method, the msec value which will be passed as an argument of pfnRunPlayerMove. This
|
|
// function is called every frame for every bot, since the RunPlayerMove is the function
|
|
// that tells the engine to put the bot character model in movement. This msec value
|
|
// tells the engine how long should the movement of the model extend inside the current
|
|
// frame. It is very important for it to be exact, else one can experience bizarre
|
|
// problems, such as bots getting stuck into each others. That's because the model's
|
|
// bounding boxes, which are the boxes the engine uses to compute and detect all the
|
|
// collisions of the model, only exist, and are only valid, while in the duration of the
|
|
// movement. That's why if you get a pfnRunPlayerMove for one boINFt that lasts a little too
|
|
// short in comparison with the frame's duration, the remaining time until the frame
|
|
// elapses, that bot will behave like a ghost : no movement, but bullets and players can
|
|
// pass through it. Then, when the next frame will begin, the stucking problem will arise !
|
|
|
|
m_frameInterval = game.time () - m_lastCommandTime;
|
|
|
|
uint8 msecVal = computeMsec ();
|
|
m_lastCommandTime = game.time ();
|
|
|
|
engfuncs.pfnRunPlayerMove (pev->pContainingEntity, m_moveAngles, m_moveSpeed, m_strafeSpeed, 0.0f, static_cast <uint16> (pev->button), static_cast <uint8> (pev->impulse), msecVal);
|
|
|
|
// save our own copy of old buttons, since bot ai code is not running every frame now
|
|
m_oldButtons = pev->button;
|
|
}
|
|
|
|
void Bot::checkBurstMode (float distance) {
|
|
// this function checks burst mode, and switch it depending distance to to enemy.
|
|
|
|
if (hasShield ()) {
|
|
return; // no checking when shield is active
|
|
}
|
|
|
|
// if current weapon is glock, disable burstmode on long distances, enable it else
|
|
if (m_currentWeapon == Weapon::Glock18 && distance < 300.0f && m_weaponBurstMode == BurstMode::Off) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
else if (m_currentWeapon == Weapon::Glock18 && distance >= 300.0f && m_weaponBurstMode == BurstMode::On) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
|
|
// if current weapon is famas, disable burstmode on short distances, enable it else
|
|
if (m_currentWeapon == Weapon::Famas && distance > 400.0f && m_weaponBurstMode == BurstMode::Off) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
else if (m_currentWeapon == Weapon::Famas && distance <= 400.0f && m_weaponBurstMode == BurstMode::On) {
|
|
pev->button |= IN_ATTACK2;
|
|
}
|
|
}
|
|
|
|
void Bot::checkSilencer () {
|
|
if ((m_currentWeapon == Weapon::USP || m_currentWeapon == Weapon::M4A1) && !hasShield ()) {
|
|
int prob = (m_personality == Personality::Rusher ? 35 : 65);
|
|
|
|
// aggressive bots don't like the silencer
|
|
if (rg.chance (m_currentWeapon == Weapon::USP ? prob / 2 : prob)) {
|
|
// is the silencer not attached...
|
|
if (pev->weaponanim > 6) {
|
|
pev->button |= IN_ATTACK2; // attach the silencer
|
|
}
|
|
}
|
|
else {
|
|
|
|
// is the silencer attached...
|
|
if (pev->weaponanim <= 6) {
|
|
pev->button |= IN_ATTACK2; // detach the silencer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
float Bot::getBombTimeleft () {
|
|
if (!bots.isBombPlanted ()) {
|
|
return 0.0f;
|
|
}
|
|
float timeLeft = ((bots.getTimeBombPlanted () + mp_c4timer.float_ ()) - game.time ());
|
|
|
|
if (timeLeft < 0.0f) {
|
|
return 0.0f;
|
|
}
|
|
return timeLeft;
|
|
}
|
|
|
|
bool Bot::isOutOfBombTimer () {
|
|
if (!game.mapIs (MapFlags::Demolition)) {
|
|
return false;
|
|
}
|
|
|
|
if (m_currentNodeIndex == kInvalidNodeIndex || (m_hasProgressBar || getCurrentTaskId () == Task::EscapeFromBomb)) {
|
|
return false; // if CT bot already start defusing, or already escaping, return false
|
|
}
|
|
|
|
// calculate left time
|
|
float timeLeft = getBombTimeleft ();
|
|
|
|
// if time left greater than 13, no need to do other checks
|
|
if (timeLeft > 13.0f) {
|
|
return false;
|
|
}
|
|
const Vector &bombOrigin = graph.getBombOrigin ();
|
|
|
|
// for terrorist, if timer is lower than 13 seconds, return true
|
|
if (timeLeft < 13.0f && m_team == Team::Terrorist && (bombOrigin - pev->origin).lengthSq () < cr::square (964.0f)) {
|
|
return true;
|
|
}
|
|
bool hasTeammatesWithDefuserKit = false;
|
|
|
|
// check if our teammates has defusal kit
|
|
for (const auto &bot : bots) {
|
|
// search players with defuse kit
|
|
if (bot.get () != this && bot->m_team == Team::CT && bot->m_hasDefuser && (bombOrigin - bot->pev->origin).lengthSq () < cr::square (512.0f)) {
|
|
hasTeammatesWithDefuserKit = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// add reach time to left time
|
|
float reachTime = graph.calculateTravelTime (pev->maxspeed, m_path->origin, bombOrigin);
|
|
|
|
// for counter-terrorist check alos is we have time to reach position plus average defuse time
|
|
if ((timeLeft < reachTime + 8.0f && !m_hasDefuser && !hasTeammatesWithDefuserKit) || (timeLeft < reachTime + 4.0f && m_hasDefuser)) {
|
|
return true;
|
|
}
|
|
|
|
if (m_hasProgressBar && isOnFloor () && ((m_hasDefuser ? 10.0f : 15.0f) > getBombTimeleft ())) {
|
|
return true;
|
|
}
|
|
return false; // return false otherwise
|
|
}
|
|
|
|
void Bot::updateHearing () {
|
|
int hearEnemyIndex = kInvalidNodeIndex;
|
|
float minDistance = kInfiniteDistance;
|
|
|
|
// setup potential visibility set from engine
|
|
auto set = game.getVisibilitySet (this, false);
|
|
|
|
// loop through all enemy clients to check for hearable stuff
|
|
for (int i = 0; i < game.maxClients (); ++i) {
|
|
const Client &client = util.getClient (i);
|
|
|
|
if (!(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.ent == ent () || client.team == m_team || client.noise.last < game.time ()) {
|
|
continue;
|
|
}
|
|
|
|
if (!game.checkVisibility (client.ent, set)) {
|
|
continue;
|
|
}
|
|
float distance = (client.noise.pos - pev->origin).length ();
|
|
|
|
if (distance > client.noise.dist) {
|
|
continue;
|
|
}
|
|
|
|
if (distance < minDistance) {
|
|
hearEnemyIndex = i;
|
|
minDistance = distance;
|
|
}
|
|
}
|
|
edict_t *player = nullptr;
|
|
|
|
if (hearEnemyIndex >= 0 && util.getClient (hearEnemyIndex).team != m_team && !game.is (GameFlags::FreeForAll)) {
|
|
player = util.getClient (hearEnemyIndex).ent;
|
|
}
|
|
|
|
// did the bot hear someone ?
|
|
if (player != nullptr && util.isPlayer (player)) {
|
|
// change to best weapon if heard something
|
|
if (m_shootTime < game.time () - 5.0f && isOnFloor () && m_currentWeapon != Weapon::C4 && m_currentWeapon != Weapon::Explosive && m_currentWeapon != Weapon::Smoke && m_currentWeapon != Weapon::Flashbang && !yb_jasonmode.bool_ ()) {
|
|
selectBestWeapon ();
|
|
}
|
|
|
|
m_heardSoundTime = game.time ();
|
|
m_states |= Sense::HearingEnemy;
|
|
|
|
if (rg.chance (15) && game.isNullEntity (m_enemy) && game.isNullEntity (m_lastEnemy) && m_seeEnemyTime + 7.0f < game.time ()) {
|
|
pushChatterMessage (Chatter::HeardTheEnemy);
|
|
}
|
|
|
|
// didn't bot already have an enemy ? take this one...
|
|
if (m_lastEnemyOrigin.empty () || m_lastEnemy == nullptr) {
|
|
m_lastEnemy = player;
|
|
m_lastEnemyOrigin = player->v.origin;
|
|
}
|
|
|
|
// bot had an enemy, check if it's the heard one
|
|
else {
|
|
if (player == m_lastEnemy) {
|
|
// bot sees enemy ? then bail out !
|
|
if (m_states & Sense::SeeingEnemy) {
|
|
return;
|
|
}
|
|
m_lastEnemyOrigin = player->v.origin;
|
|
}
|
|
else {
|
|
// if bot had an enemy but the heard one is nearer, take it instead
|
|
float distance = (m_lastEnemyOrigin - pev->origin).lengthSq ();
|
|
|
|
if (distance > (player->v.origin - pev->origin).lengthSq () && m_seeEnemyTime + 2.0f < game.time ()) {
|
|
m_lastEnemy = player;
|
|
m_lastEnemyOrigin = player->v.origin;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
extern ConVar yb_shoots_thru_walls;
|
|
|
|
// check if heard enemy can be seen
|
|
if (checkBodyParts (player)) {
|
|
m_enemy = player;
|
|
m_lastEnemy = player;
|
|
m_lastEnemyOrigin = m_enemyOrigin;
|
|
|
|
m_states |= Sense::SeeingEnemy;
|
|
m_seeEnemyTime = game.time ();
|
|
}
|
|
|
|
// check if heard enemy can be shoot through some obstacle
|
|
else {
|
|
if (m_difficulty > 2 && m_lastEnemy == player && m_seeEnemyTime + 3.0f > game.time () && yb_shoots_thru_walls.bool_ () && isPenetrableObstacle (player->v.origin)) {
|
|
m_enemy = player;
|
|
m_lastEnemy = player;
|
|
m_enemyOrigin = player->v.origin;
|
|
m_lastEnemyOrigin = player->v.origin;
|
|
|
|
m_states |= (Sense::SeeingEnemy | Sense::SuspectEnemy);
|
|
m_seeEnemyTime = game.time ();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Bot::enteredBuyZone (int buyState) {
|
|
// this function is gets called when bot enters a buyzone, to allow bot to buy some stuff
|
|
|
|
const int *econLimit = conf.getEconLimit ();
|
|
|
|
// if bot is in buy zone, try to buy ammo for this weapon...
|
|
if (m_seeEnemyTime + 12.0f < game.time () && m_lastEquipTime + 15.0f < game.time () && m_inBuyZone && (bots.getRoundStartTime () + rg.float_ (10.0f, 20.0f) + mp_buytime.float_ () < game.time ()) && !bots.isBombPlanted () && m_moneyAmount > econLimit[EcoLimit::PrimaryGreater]) {
|
|
m_ignoreBuyDelay = true;
|
|
m_buyingFinished = false;
|
|
m_buyState = buyState;
|
|
|
|
// push buy message
|
|
pushMsgQueue (BotMsg::Buy);
|
|
|
|
m_nextBuyTime = game.time ();
|
|
m_lastEquipTime = game.time ();
|
|
}
|
|
}
|
|
|
|
bool Bot::isBombDefusing (const Vector &bombOrigin) {
|
|
// this function finds if somebody currently defusing the bomb.
|
|
|
|
if (!bots.isBombPlanted ()) {
|
|
return false;
|
|
}
|
|
bool defusingInProgress = false;
|
|
|
|
for (const auto &client : util.getClients ()) {
|
|
auto bot = bots[client.ent];
|
|
|
|
if (bot == nullptr || bot == this || !bot->m_notKilled) {
|
|
continue; // skip invalid bots
|
|
}
|
|
|
|
if (m_team != bot->m_team || bot->getCurrentTaskId () == Task::EscapeFromBomb) {
|
|
continue; // skip other mess
|
|
}
|
|
float bombDistance = (client.ent->v.origin - bombOrigin).lengthSq ();
|
|
|
|
if (bombDistance < cr::square (140.0f) && (bot->getCurrentTaskId () == Task::DefuseBomb || bot->m_hasProgressBar)) {
|
|
defusingInProgress = true;
|
|
break;
|
|
}
|
|
|
|
// take in account peoples too
|
|
if (defusingInProgress || !(client.flags & ClientFlags::Used) || !(client.flags & ClientFlags::Alive) || client.team != m_team || util.isFakeClient (client.ent)) {
|
|
continue;
|
|
}
|
|
|
|
if (bombDistance < cr::square (140.0f) && ((client.ent->v.button | client.ent->v.oldbuttons) & IN_USE)) {
|
|
defusingInProgress = true;
|
|
break;
|
|
}
|
|
}
|
|
return defusingInProgress;
|
|
}
|
|
|
|
float Bot::getShiftSpeed () {
|
|
if (getCurrentTaskId () == Task::SeekCover || (pev->flags & FL_DUCKING) || (pev->button & IN_DUCK) || (m_oldButtons & IN_DUCK) || (m_currentTravelFlags & PathFlag::Jump) || (m_path != nullptr && m_path->flags & NodeFlag::Ladder) || isOnLadder () || isInWater () || m_isStuck) {
|
|
return pev->maxspeed;
|
|
}
|
|
return static_cast <float> (pev->maxspeed * 0.4f);
|
|
}
|