Added cvars descriptions and yapb.cfg generation. Finally fixed bomb-defuse problems. Fixed unaligned access in bot compression/decompression. Fixed low-fps aim code falure.
5815 lines
186 KiB
C++
5815 lines
186 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::checkBreablesAround () {
|
|
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 ();
|
|
|
|
bool isPistolMode = tab[25].teamStandard == -1 && tab[3].teamStandard == 2;
|
|
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
|
|
bool isOldGame = game.is (GameFlags::Legacy) && !game.is (GameFlags::Xash3D);
|
|
|
|
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 || (hasPrimaryWeapon () && (pev->weapons & (cr::bit (Weapon::USP) | cr::bit (Weapon::Glock18))) && 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) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 ();
|
|
checkBreablesAround ();
|
|
|
|
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 () || game.isNullEntity (m_pickupItem)) {
|
|
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);
|
|
}
|